[삽질기록] RAW H.264(AVC)에 MP4컨테이너 씌우기 #1

2019. 5. 29. 09:40Programming/Android

반응형

h264는 코덱이고 mp4는 컨테이너이다. 컨테이너와 코덱의 차이점에 대해서는 코덱이 뭔가요? (코덱과 컨테이너의 차이) by Michael Fitzer, 교양있는 교양채널에 잘 기술되어있으니 참조하도록 하자. 단순하게 말하면 h264로 압축된 데이터를 mp4파일로 만들기 위해서는, mp4컨테이너 파일을 만들어주기만 하면 된다. 몹시 심플한 얘기처럼 들리지만, 생각보다 산 넘어 산인 상황이 반복되고 있다. 이 내용을 기록해두면 언젠가 쓸모있을 것 같아서, 시간이 날 때마다 조금씩 기록하기로 했다.

 

Android의 MediaCodec, MediaExtractor, MediaMuxer.

컨테이너를 생성하고, 컨테이너에 영상과 오디오 데이터를 집어넣는 과정을 먹싱(Muxing)이라고 한다. 먹싱에 대한 자세한 설명은 동영상의 기본적인 이해 (컨테이너 포맷이란?, 동영상의 변환이란?), 신벗드의 블로그에 잘 설명되어있다. Android문서를 찾다보니 MediaMuxer라는 녀석이 있었다. MediaMuxer, Android Developers를 참조하면 예제코드가 있다.

MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
 // More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
 // or MediaExtractor.getTrackFormat().
 MediaFormat audioFormat = new MediaFormat(...);
 MediaFormat videoFormat = new MediaFormat(...);
 int audioTrackIndex = muxer.addTrack(audioFormat);
 int videoTrackIndex = muxer.addTrack(videoFormat);
 ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
 boolean finished = false;
 BufferInfo bufferInfo = new BufferInfo();

 muxer.start();
 while(!finished) {
   // getInputBuffer() will fill the inputBuffer with one frame of encoded
   // sample from either MediaCodec or MediaExtractor, set isAudioSample to
   // true when the sample is audio data, set up all the fields of bufferInfo,
   // and return true if there are no more samples.
   finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
   if (!finished) {
     int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
     muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
   }
 };
 muxer.stop();
 muxer.release();

 

getInputBuffer()에 대한 설명을 잘 보면 MediaMuxer.writeSampleData()에 ByteBuffer타입으로 한 프레임씩 복사한다는 것을 알 수 있다. 보통의 경우에는 MediaCodec과 함께 사용하기 때문에, 예제코드에서 사용되는 getInputBuffer()는 일반적으로 MediaCodecgetInputBuffer()이다. 나의 경우에는 MediaCodec을 사용할 수는 없었지만, 영상을 한 프레임 byteArray로 가져오는 것은 가능했다. 따라서 MediaMuxer의 사용이 가능하다고 판단해서, muxer.writeSampleData(0, ByteBuffer.wrap(byteArray 프레임 데이터), bufferInfo)와 같은 방법으로 함수를 호출했다.

일단 Exception이 발생하지 않는 것을 보고 안심했으나, 문제는 muxer.stop()을 호출했을 때 발생했다.

02-13 10:41:36.271: E/AndroidRuntime(11768): java.lang.IllegalStateException: Failed to stop the muxer
02-13 10:41:36.271: E/AndroidRuntime(11768):    at android.media.MediaMuxer.nativeStop(Native Method)
02-13 10:41:36.271: E/AndroidRuntime(11768):    at android.media.MediaMuxer.stop(MediaMuxer.java:226)

당시의 에러 로그를 저장해놓지 않았기 때문에, StackOverflow에서 가장 비슷한 에러 문구를 찾아봤다. muxer.stop()을 호출했을 시 IllegalStateException이 발생하는 경우가 종종 있었는데, muxer.start()를 호출하지 않고 muxer.stop()을 호출하는 경우에 많이 발생한다고 한다. Android MediaMuxer failed to stop, StackOverflow

 

몇일동안 끙끙거리고 머리를 싸매봤지만 정확한 원인은 알 수 없었다. MediaCodec.BufferInfo.set()을 이용해서 값을 세팅해줄 때 newFlags값을 I-Frame/P-Frame에 따라서 바꿔주기도 해보고, 여러가지 시도를 해봤지만 증상은 완화되지 않았다. FFmpeg를 사용해서 영상을 인코딩하기 때문에 가급적이면 FFmpeg를 사용하지 않으려고 했지만, 뾰족한 방법이 없을 것 같았다.


MediaExtractor를 사용해서 .h264파일을 읽어온 후, MediaFormatBufferInfo값을 MediaExtractor로부터 얻어오려는 시도를 해봤다. 아쉽게도 파일경로를 MediaExtractor에게 전달하자 Exception이 발생했는데, mp4컨테이너가 씌워진 파일을 전달했을 경우 정상동작하는 것으로 봐서는 H264 RAW파일은 지원하지 않는 듯 하다. Linux Command line으로 ffmpeg를 실행시켜서 mp4컨테이너를 씌웠을 때 재생이 되는 것으로 봐서는, 파일에 이상은 없는 것 같다. 설령 파일에 이상이 있다 치더라도, ffmpeg를 사용해서 Muxing이 정상동작하는 것으로 봐서는 ffmpeg를 사용하는 수밖에 없을 듯 했다.


일단 Encoding이 아니라 Muxing이기 때문에, FFmpeg을 사용하더라도 큰 문제는 없을 것 같았다. 여러가지 키워드를 사용해서 FFmpegWrapper-Android, OpenWatch라던가 mp4parser, sannies를 사용하려고 시도해봤지만, 번번히 실패했다.

 

ffmpeg를 이용해서 muxing하는 예제는 많이 없었는데, 며칠을 검색해서 찾다보니 ffmpeg-muxer, Akagi201를 발견할 수 있었다. 우선 Akagi201의 코드에는 H265에 대한 고려는 빠져있기 때문에, ffmpeg bitstream filter, ffmpeg DOC페이지를 참조해서 비트스트림 필터를 H265로 변경해봤다. 잘 동작한다! 다만, iOS에서는 기본 플레이어가 H265코덱을 지원하지 않기 때문에, 앨범에서 재생도 할 수 없고 다른 사용자에게 전송도 불가능하다고 한다. Android와 iOS 양쪽을 모두 생각한다면, 고민이 좀 더 필요한 부분인 듯 했다.

반응형