티스토리 뷰

 Android MediaCodec을 이용하여 비디오 디코하는 예제를 작성해보았습니다. 예제는 이미 오래전에 올려두고 블로그에 정리하는것이지만... 이번 글에서는 디코딩만 진행합니다.



지금까지 블로그에 포스팅한 내용


그사이 발표도 2번 진행하였고, 그에 대한 정리를 올리지 못하였습니다. 그래서 순차적으로 올려보려고 합니다.



Video Decoder를 하기 위한 조건

  • Android 4.1 이상
  • MediaCodec을 이용하여 동작하지 않는 단말기도 있을 수 있어서 듀얼 코어 이상을 추천드립니다.
    • 일부 기기에서 사운드와 비디오를 동시에 디코딩할 경우 동작하지 않는 경우가 있었습니다.
  • MP4 동영상 또는 H.264, OS 버전과 제조사 지원에 따라서 추가 코덱이 지원될 수 있습니다.
    • H.264 데이터를 다룰 때에는 비디오에 대한 화면 사이즈를 정확하게 알아야 합니다.
    • MP4 영상의 경우 파일로 저장되어있는 영상을 불러오게 되므로 MediaExtractor API를 사용합니다.
  • VP8/VP9 역시 지원됩니다.



Video Decoder를 사용하기 위한 API



 각 API에 대한 간단한 예제를 살펴보겠습니다. 이 예제들은 Android API 문서에 설명되어 있는 내용이며, 이를 기초로 한다면 디코딩하는 코드는 아래의 Github에 올려둔 예제와 같이 동작할 수 있습니다.

 Github 비디오 디코딩 예제


 이번 글에서는 MediaCodec을 통한 디코딩에 대한 코드 설명과 MediaExtractor API를 사용하여 mp4 파일을 읽어들이는 방법을 간단하게 설명하겠습니다. 자세한 예제는 github 주소를 참고하세요.



MediaCodec의 초기화

 MediaCodec을 초기화 하기 위해서는 MediaFormat을 알아야 합니다. 이 MediaFormat은 직접 작성하여도 되고, MediaExtractor를 이용하여 값을 가져올 수도 있습니다. 디코딩에서 필요한 정보는 화면의 사이즈 정보만 알면 됩니다.


 가장 간단한 방법은 아래와 같습니다. 이중 mime Type은 H.264를 나타내는 "video/avc"를 적고, 넓이와 높이값을 적어주시면 됩니다.

MediaFormat format = MediaFormat.createVideoFormat(String mime, int width, int height);

 

 MediaCodec을 초기화 할 때에도 mime type을 넘겨주어야 합니다. MediaFormat을 수동으로 셋팅하였을 때와 동일한 값이 들어가게 됩니다.

MediaCodec codec = MediaCodec.createDecoderByType(String mime);


 MediaFormat 정보를 셋팅해주어야 하는 단계입니다. 셋팅하기 위한 값은 아래와 같습니다.

 - MediaFormat : 위에서 설정한 MediaFormat의 설정값(MediaExtractor를 이용할 경우 해당 MediaFormat 정보를 return 받아 올 수 있습니다.)

 - Surface : 디코딩에서만 사용하며 화면을 그리기 위한 View를 넘겨주면 됩니다.

 - MediaCrypto : 암호화된 Media data를 다룰 때 사용하는 옵션입니다. 일반적으로 null로 초기화 해서 사용합니다.(저도 아직 어떤 경우에 사용해야 하는지를 모르겠네요)

 - flags : 인코딩시에만 다음의 값(MediaCodec.CONFIGURE_FLAG_ENCODE)을 초기화 하고, 그외에는 0으로 초기화 합니다.

codec.configure (MediaFormat format, Surface surface, MediaCrypto crypto, int flags);


준비가 완료되었다면 MediaCodec start를 호출합니다.

codec.start();




MediaCodec을 이용한 디코딩 진행

 MediaCodec을 사용하는 방법이 5.0을 기준으로 조금 변경되었습니다. 아래 구글에서 제공하는 주석에서도 보면 알 수 있겠지만 Encode/Decode를 할 때 굳이 알필요 없는 정보이기도 합니다.


 그 알 필요없는 정보는 시스템 내부의 프레임웍에서 MediaCodec에서 사용할 Buffer를 몇개를 사용할지를 정하는 부분에 해당되는데 실제 기존 API 상으로는 Buffer를 몇개 사용하고 있는지에 대한 정보를 사용자가 알더라도 줄이거나, 늘리거나 하는 동작이 불가능하였습니다. 이에 대한 API를 5.0에서는 @deprecated 하였습니다. 5.0을 개발할 경우가 아니라면 기존 방식 그대로 사용해야합니다.


 MediaCodec에서 사용할 inputBuffer와 outputBuffer를 초기화 합니다. 이 작업은 5.0에서 @deprecated 되었습니다.

// if API level <= 20, get input and output buffer arrays here

ByteBuffer[] inputBuffers = codec.getInputBuffers();

ByteBuffer[] outputBuffers = codec.getOutputBuffers();


 위와 같이 사용할 Buffer를 미리 알아두어야 합니다. ByteBuffer에 대한 모든 정보는 MediaCodec의 프레임웍단에서 알고 있습니다. 그렇기에 실제 몇개가 생성되는지는 알 수 없습니다. 제가 테스트할 때는 많으면 20개까지도 생성되는것을 확인하였습니다.

 참고 : Surface를 통해서 Output을 하거나, Input을 하는 경우에는 ByteBuffer는 1개가 생성되며, 반대쪽이 여러개가 생성됩니다.



아래 데이터 입력과 출력 부분은 루프(for, while 등)를 통해서 동작하는 부분입니다.


데이터 입력 부분

 데이터를 입력하는 부분을 먼저 설명하겠습니다. MediaCodec에 입력되어야 할 데이터는 2가지 형태가 있습니다.

  • YUV의 색상정보 데이터 : 다음 링크 글의 MediaCodec 사용하기 부분을 참고하세요 http://thdev.net/576
  • Surface : 인코딩시에만 해당되며 4.3이상에서만 사용할 수 있습니다.

 입력되어야 할 데이터는 2가지 형태가 필요하지만 지금은 Decoder를 할 경우이기 때문에 MediaExtractor에서 데이터를 가지고와서 처리해야합니다. 가지고와서 집어넣는 부분은 github의 예제를 참고해주세요.



1. 데이터를 넣을 Buffer index 가져오기

 buffer index는 dequeueInputBuffer 함수를 통해서 가지고오게됩니다. 해당 Buffer index가 -1 보다 큰 경우에만 실제 데이터를 writer 하여 Decoder 요청할 수 있습니다.

  int inputBufferIndex = codec.dequeueInputBuffer(timeoutUs);


2. 가지고 온 Buffer index에 데이터 채워넣기

 21 이상 : Lollipop에서만 사용할 것이라면 ByteBuffer 를 직접 받아올 수 있는 함수가 제공됩니다.

   - getInputBuffer(index);

 20 미만 : Lollipop이 아닌 이전 방식은 위에서 생성한 inputBuffers를 활용하여야 합니다.

   - ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; 를 return 받아 이 buffer를 사용하면 됩니다.

  if (inputBufferIndex >= 0) {

    // if API level >= 21, get input buffer here

    ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex);

    // fill inputBuffers[inputBufferIndex] with valid data

    ...

    codec.queueInputBuffer(inputBufferIndex, ...);

  }


 Buffer를 채워넣었다면 마지막 함수를 호출합니다. (위에 나와있는데 좀 더 자세히 보기위해서 분리하였습니다.)

 - indexdequeueInputBuffer 에서 return 받은 index 번호를 넣습니다.

 - offset : 항상 0이겠지만 Buffer에 채워넣은 데이터의 시작 점을 지정할 수 있습니다.

 - size : Buffer에 채워넣은 데이터 사이즈 정보

 - presentationTimeUs : 디코딩의 경우 Play 할 데이터의 시간(마이크로 초)

 - flags : 읽은 버퍼의 정보가 설정값인지 BUFFER_FLAG_CODEC_CONFIG, 마지막 데이터인지BUFFER_FLAG_END_OF_STREAM에 대한 정보를 초기화 할 수 있습니다.

              대부분은 0을 채워넣고 마지막 데이터를 알리기 위해서는 BUFFER_FLAGS_END_OF_STREAM을 넣습니다.

queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)



3. 디코딩 된 데이터 가져오기

 2번의 과정과 동일합니다. 함수 명만 output 이라는 이름만 포함되어 있습니다. InputBuffer index를 가져오는 방법과 동일한대 아래와 같이 OutputBuffer를 가져올 수 있습니다. outputBuffer에서는 BufferInfo라는 함수를 추가로 가져올 수도 있습니다.


BufferInfo API : http://developer.android.com/reference/android/media/MediaCodec.BufferInfo.html

BufferInfo에는 flags, size, offset, presentationTimeUs 정보가 포함되어 있습니다.

 int outputBufferIndex = codec.dequeueOutputBuffer(timeoutUs);

 또는  

 int outputBufferIndex = codec.dequeueOutputBuffer(timeoutUs, bufferInfo);


Output의 경우는 다음 4가지 사항이 나타나게 됩니다.

MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED

 Buffer 정보가 1번 변경되게 됩니다. API 21인 Lollipop 부터는 이 @deprecated 되었기에 불필요하지만 이전 API에서는 꼭 필요한 정보입니다. 이게 호출되면 처음에 생성한 ByteBuffer[] 배열의 변화가 일어나게 됩니다.


MediaCodec.INFO_OUTPUT_FORMAT_CHANGED

 처음에 생성하였든 MediaFormat을 기억하시는지요. 그 MediaFormat이 변경된 정보를 알려주게됩니다. 이 경우는 Encoder에서만 주로 사용하고, 디코더에서는 사용할 일은 없습니다.


MediaCodec.INFO_TRY_AGAIN_LATER

 이 함수가 호출되는 경우라면 사실 무시하여도 됩니다. 아래 새로 올라온 API 예제에는 해당 함수가 표시되지 않았으나. 필요에 따라 사용할 수 있습니다.


outputBufferIndex >= 0

 이 경우에 실제 디코딩 된 데이터가 들어오는 경우에 해당됩니다. 디코딩된 데이터를 초기화때 사용한 Surface에 바로 그리려면 아래와 같이 할 수 있습니다.

 2번째 함수에 true를 하게되면 Surface에 데이터를 바로 그리게 됩니다.

codec.releaseOutputBuffer(outIndex, true /* Surface init */);


또는 직접 데이터를 다룬다면 아래와 같이 할 수 있습니다.

해당 되는 ByteBuffer를 return 받고, 이를 활용하면 됩니다.

if (outputBufferIndex >= 0) {

  // if API level >= 21, get output buffer here

  ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferIndex);

  // outputBuffer is ready to be processed or rendered.

  ...

  codec.releaseOutputBuffer(outputBufferIndex, ...);

}



코덱의 종료

 열기가 있었으면 닫기도 필요합니다. MediaCodec은 네이티브로 동작합니다. 그래서 stop을 호출해주지 않으면 문제가 발생하게 됩니다. 아래와 같이 3개의 함수를 호출하게 되는데.. 실제로 따라가보면 codec.release()만 호출하여도 문제는 없습니다.

codec.stop();

codec.release();

codec = null;



간단하게 MediaCodec 사용방법을 알아보았습니다. 이제는 데이터를 읽어오는 API를 간단하게 살펴보겠습니다.



MediaExtroactor API 사용하기

 데이터를 가장 쉽게 불러올 수 있는 방법은 MediaExtroactor를 이용하는 방법입니다. 제한적이기는 하나 Android API에서 간단하게 MP4 동영상을 가져와 플레이 해보기에는 가장 좋은 방법입니다.

 참고 : 만약 MP4 동영상이 아니라면 H.264 데이터를 직접 input 하여 MediaCodec과 연결하여 사용하셔도됩니다.



MediaExtractor 초기화

 초기화는 간단합니다. setDataSource 함수에 직접 파일 경로를 셋팅할 수도 있고, xml에 있는 파일을 읽어올 수도 있습니다.

 MediaExtractor extractor = new MediaExtractor();

 extractor.setDataSource(...);



트랙 찾기

 MP4 파일에는 트랙이 1개만 있을 수도 있고, 여러개가 있을 수 있습니다. 가장 일반적인 mp4 파일은 Audio/Video를 가지는 2 트랙이 가장 많이 있습니다. 이중에서 비디오 데이터만 찾는 방법은 String 찾기를 통해서 할 수 있습니다.


일단 몇개의 트랙이 있는지를 찾습니다. 읽어들인 파일의 트랙 갯수는 getTrackCount() 함수를 통하면 됩니다.

 int numTracks = extractor.getTrackCount();


 읽어 드린 트랙 정보는 MediaFormat과 mime Type을 가지고 올 수 있습니다. 이 2가지 정보를 이용하여 위에서 설명한 MediaCodec에 각각 넣어주면 됩니다. 문제는 해당 Track이 Audio 인지 Video인지 알고 있어야 쉽게 사용이 가능하다는 점입니다.


 간단하게 mime.startWith("video/"); 함수를 통해서 Video 정보인지 구분하시면 되며, 해당 트랙을 사용할 때에는(비디오 트랙이라고 가정하고 사용한다면) selectTrack(index); 를 설정하면 됩니다.


 for (int i = 0; i < numTracks; ++i) {

   MediaFormat format = extractor.getTrackFormat(i);

   String mime = format.getString(MediaFormat.KEY_MIME);

   if (weAreInterestedInThisTrack) {

     extractor.selectTrack(i);

   }

 }



종료

 역시 사용을 완료하였다면 종료하는게 필요합니다.

 extractor.release();

 extractor = null;



2개의 API를 사용하여 간단한 예제는 github에 올려두었으나 여기에 아래와 같이 사용할 수 있습니다.

아래 예제는 Video 트랙을 찾아서 디코딩 준비하는 예입니다.

try {

mExtractor = new MediaExtractor();

mExtractor.setDataSource(filePath);

for (int i = 0; i < mExtractor.getTrackCount(); i++) {

MediaFormat format = mExtractor.getTrackFormat(i);

String mime = format.getString(MediaFormat.KEY_MIME);

if (mime.startsWith(VIDEO)) {

mExtractor.selectTrack(i); // Video 트랙을 선택합니다.

mDecoder = MediaCodec.createDecoderByType(mime); // Decoder를 초기화 합니다.

try {

Log.d(TAG, "format : " + format);

mDecoder.configure(format, surface, null, 0 /* Decoder */); // 사용할 Format 정보를 셋팅합니다.

} catch (IllegalStateException e) {

Log.e(TAG, "codec '" + mime + "' failed configuration. " + e);

return false;

}

mDecoder.start(); // 문제가 없다면 Decoder를 시작합니다.

break;

}

}

} catch (IOException e) {

e.printStackTrace();

}



마무리

 이를 사용한 디코딩 예제는 github에 올려두었으니 파일 경로만 변경하면 바로 테스트가 가능합니다. 싱크를 어느정도 맞게 작업해두어서 문제는 없으리라고 생각됩니다. 어디까지나 예제이니... ^^;



댓글