출처 : http://thdev.net/576


Android API 중 4.1부터 추가된 API인 MediaCodec이 있습니다. Android 4.3 부터 추가 API인 MediaMuxer가 추가되었고, Surface를 통한 인코딩 기능이 사용가능합니다. MediaCodec과 MediaMuxer에 대한 간단한 설명을 작성하고, 주요 API에 대한 설명을 작성합니다.


Android API

 - MediaCodec

 - MediaMuxer

 - MediaExtractor



MediaCodec

 - Android 4.1 이상에서 제공됩니다.

 - raw byte를 이용하여 인코딩/디코딩을 직접 처리할 수 있는 API 입니다.

 - Android 4.3 부터 MediaMuxer를 이용하여 MP4 파일을 출력할 수 있습니다.

 - Android 4.3 부터 Surface를 이용하여 인코딩이 가능합니다.

 - Android 4.3 부터 VP8, VP9 인코딩/디코딩 가능합니다.

 - MediaExtractor를 이용하여 파일을 열고, 디코딩이 가능합니다.



MediaMuxer

 - Android 4.3 이상에서 제공됩니다.

 - MediaCodec과 함께 사용해야 합니다.

 - 현재는 MP4 파일 output이 가능합니다.



MediaCodec 지원 가능한 코덱 - Video

 일부 코덱은 SW코덱으로 동작합니다.

  • "video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm)
  • "video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm)
  • "video/avc" - H.264/AVC video
  • "video/mp4v-es" - MPEG4 video
  • "video/3gpp" - H.263 video


MediaCodec 지원 가능한 코덱 - Audio

 일부 코덱은 SW코덱으로 동작합니다.

  • "audio/3gpp" - AMR narrowband audio
  • "audio/amr-wb" - AMR wideband audio
  • "audio/mpeg" - MPEG1/2 audio layer III
  • "audio/mp4a-latm" - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
  • "audio/vorbis" - vorbis audio
  • "audio/g711-alaw" - G.711 alaw audio
  • "audio/g711-mlaw" - G.711 ulaw audio



MediaCodec 사용하기

 MediaCodec을 이용하여 H.264를 인코딩 디코딩 할 수 있습니다. 이 H.264 인코딩에 사용되는 색공간은 YUV 입니다. YUV 색공간 중 안드로이드에서 인코딩/디코딩 가능한 색공간은 NV12와 I420 을 사용합니다. Surface를 사용할 경우 RGB 데이터를 사용하면 되지만 RGB가 아닌 경우에는 YUV를 직접 변환하는 작업이 필요합니다.


 YUV로 변환하는 작업을 위한 라이브러는 libYUV를 사용하시는것을 추천드립니다. Android Full Source에 포함되어 있는것을 사용하셔도 되고, 별도로 다운로드 받아 설치하셔도 됩니다.

 libYUV : https://code.google.com/p/libyuv/


이미지 출처 : Wikipedia YUV





MediaCodec 초기화

 MediaCodec을 초기화 하기 위한 코드로 아래와 같이 초기화 합니다. createDecoderByType 또는 createEncoderByType를 이용하여 Codec을 MediaCodec 객체를 생성합니다. 여기에서 말하는 Type은 MediaCodec의 지원 코덱 이름이 됩니다.

1
MediaCodec codec = MediaCodec.createDecoderByType(type);



 Codec을 configure를 통해 Surface 초기화와 Media 암호화, flags를 사용합니다.

 MediaFormat : 인코딩/디코딩에 따라서 초기화 하는 코드가 다르게 됩니다. 간단하게 인코딩시에 설정해야 할 Format을 살펴보겠습니다.

 Surface : Surface의 경우는 디코딩시에만 적용하시면 됩니다. 디코딩시 Surface를 적용하면 화면 랜더링을 포함하여 출력되게 됩니다.

 flags : 인코딩과 디코딩 flags를 지정해주어야 합니다. 인코딩시에는 MediaCodec.CONFIGURE_FLAG_ENCODE을 지정하고, 디코딩 시에는 0을 설정하면 됩니다.

1
2
codec.configure(format, …);
public void configure (MediaFormat format, Surface surface, MediaCrypto crypto, int flags);


Video 인코딩 시 MediaFormat

 이 중 Color format은 스마트폰마다 색상값이 달라지게 됩니다. 별도의 코드를 이용하여 색상값을 찾도록 해야 합니다.

1
2
3
4
5
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 320, 240);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);


 MediaFormat을 초기화 할 경우 안드로이드 기기의 색상값 확인이 필요합니다. Android의 색상값별로 구분을 하면 아래와 같습니다.(참고 : https://gist.github.com/bryanwang/7380901)

  • COLOR_FormatYUV420Planar (I420)
  • COLOR_FormatYUV420PackedPlanar (also I420)
  • COLOR_FormatYUV420SemiPlanar (NV12)
  • COLOR_FormatYUV420PackedSemiPlanar (also NV12)
  • COLOR_TI_FormatYUV420PackedSemiPlanar (also also NV12)


H.264의 경우의 코드이며 아래와 같은 코드로 색상값을 확인할 수 있습니다.


selectCodec 코드는 API의 http://developer.android.com/reference/android/media/MediaCodecInfo.html 를 참고하시면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
 * Returns the first codec capable of encoding the specified MIME type, or
 * null if no match was found.
 */
private MediaCodecInfo selectCodec(String mimeType) {
    int numCodecs = MediaCodecList.getCodecCount();
    for (int i = 0; i < numCodecs; i++) {
        MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
 
        if (!codecInfo.isEncoder()) {
            continue;
        }
 
        for (String type: codecInfo.getSupportedTypes()) {
            if (type.equalsIgnoreCase(mimeType)) {
                Log.i("selectCodec", "SelectCodec : " + codecInfo.getName());
                return codecInfo;
            }
        }
    }
    return null;
}
 
/**
 * Retruns a color format that is supported by the codec and by this test
 * code. If no match is found, this throws a test failure -- the set of
 * formats known to the test should be expanded for new platforms.
 */
protected int selectColorFormat(String mimeType) {
    MediaCodecInfo codecInfo = selectCodec(mimeType);
    if (codecInfo == null) {
        throw new RuntimeException("Unable to find an appropriate codec for " + mimeType);
    }
     
    MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType);
    for (int i = 0; i < capabilities.colorFormats.length; i++) {
        int colorFormat = capabilities.colorFormats[i];
        if (isRecognizedFormat(colorFormat)) {
            Log.d("ColorFomar", "Find a good color format for " + codecInfo.getName() + " / " + mimeType);
            return colorFormat;
        }
    }
    return -1;
}
 
/**
 * Returns true if this is a color format that this test code understands
 * (i.e. we know how to read and generate frames in this format).
 */
private boolean isRecognizedFormat(int colorFormat) {
    switch (colorFormat) {
    // these are the formats we know how to handle for this test
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
    case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
    case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
        return true;
    default:
        return false;
    }
}


Audio 인코딩 시 MediaFormat

1
2
3
4
5
6
MediaFormat format  = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 8000);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
format.setInteger(MediaFormat.KEY_BIT_RATE, 128000);

초기화가 완료되면 Codec를 start하면 됩니다.

1
codec.start();



MediaMuxer 초기화

 MediaMuxer는 파일이름과 OUTPUT Type을 지정하면 됩니다.

1
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);



MediaCodec과 MediaMuxer 인코딩 예제

 MediaCodec과 MediaMuxer를 함께 사용하기 위한 예제입니다. MediaCodec의 InputBuffer와 OutputBuffer를 생성해야 합니다.

1
2
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();


 생성된 InputBuffer가 1개 이상이기 때문에 dequeueInputBuffer를 통하여 사용가능한 TempBuffer의 Number를 가져와 처리해야 합니다. 그리고 Buffer를 put 하고, queueInputBuffer를 추가하면 됩니다.


 queueInputBuffer의 원문은 아래와 같습니다. index는 dequeueInputBuffer에서 return된 번호이며, offset은 입력된 Byte의 offset 입니다. 기본적으로는 0을 지정하시면 됩니다. 입력해서 넣어주는 Buffer의 크기를 지정하고, presentationTimeUs는 마이크로 초를 입력하시면 됩니다. 동기화를 위한 시간으로 Muxer를 사용할 경우에는 꼭 추가해야 합니다.

 flags는 기본은 0으로 작업하시면 되며, MediaCodec API를 참고하셔서 추가하시면 됩니다.

1
public final void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)


InputBuffer 예제

1
2
3
4
5
6
int inputBufferIndex = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferIndex >= 0) {
  // fill inputBuffers[inputBufferIndex] with valid data
  ...
  codec.queueInputBuffer(inputBufferIndex, ...);
}


OutputBuffer 예제

 OutputBuffer 예제로 해당 가이드라인 코드는 MediaMuxer를 함께 사용하기 위한 코드를 추가하였습니다. MediaMuxer를 사용하기 위해서는 BufferInfo class를 함께 사용해야 합니다. InputBuffer과 마찬가지로 dequeueOutputBuffer 를 통하여 OutputBuffer Index를 return 받아 처리하면 됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
BufferInfo bufferInfo = new BufferInfo();
int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, timeoutUs);
if (outputBufferIndex >= 0) { // 0 이상일 경우에 인코딩/디코딩 데이터가 출력됩니다.
ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
// muxer에 Sample data를 입력합니다.(파일로 저장)
muxer.writeSampleData(outputBufferIndex, outputBuffer, bufferInfo);
// outputBuffer is ready to be processed or rendered.
// release OutputBuffer의 2번째 boolean 값은 Surface를
// 통한 디코딩시에 true, 그렇지 않으면 false 처리합니다.
codec.releaseOutputBuffer(outputBufferIndex, true/false);
 
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
// Output buffer changed가 호출되면 outputBuffers 를 다시 호출합니다.(Temp buffer의 수가 변경될 수 있음)
outputBuffers = codec.getOutputBuffers();
 
} else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
 // More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
// or MediaExtractor.getTrackFormat().
 
// Muxer를 초기화 필요할 경우 아래와 같은 코드를 추가해주면 됩니다.
// Audio/Video를 모두 Muxer에 추가할 경우 2개를 아래와 같이 따로 따로 추가해야 하며,
// Codec 역시 2개를 별도로 생성해주어야 합니다.
MediaFormat audioFormat = audioCodec.getOutputFormat();
MediaFormat videoFormat = videoCodec.getOutputFormat();
// MediaFormat에 따라서 Track을 추가하게되면, TrackIndex가 생성됩니다.
int audioTrackIndex = muxer.addTrack(audioFormat);
int videoTrackIndex = muxer.addTrack(videoFormat);
// 트랙 추가가 완료되면 start를 호출하여 muxer를 시작합니다.
muxer.start();
}



코덱 사용 종료

 코덱을 사용하였으면 stop, release 를 순서대로 호출하면 됩니다.

1
2
3
codec.stop();
codec.release();
codec = null;



Muxer 종료

 Muxer는 꼭 stop을 호출해줘야 합니다. MP4파일의 가장 중요한 head 정보가 쓰여지게 됩니다. stop이 완료되지 않으면 HEAD 정보가 포함되지 않아 재생이 불가능한 MP4 파일이 생성이 되게 됩니다.

1
2
3
muxer.stop();
muxer.release();
muxer = null;



마무리

 간단하게 MediaCodec과 MediaMuxer API를 살펴보았습니다. 추후에 MediaCodec과 MediaMuxer를 사용하는 예제를 작성할 예정이며, Audio를 byte를 통하여 디코딩할경우의 예제는 아래 글을 참고하시면 됩니다.


 Video를 byte를 통한 직접 디코딩하는것과는 좀 다르게 별도의 코덱 정보를 추가로 작성해주어야 합니다. 이에 대한 예제 코드를 담고 있으며, 가져다가 사용하시면 됩니다.


MediaCodec AAC 디코딩 하기 : http://thdev.net/567

Posted by Real_G