淺析 MediaCodec 工作原理
原文鏈接: https://juejin.cn/post/7032224346491846687
1. MediaCodec工作原理
MediaCodec類Android提供的用于訪問低層多媒體編/解碼器接口,它是Android低層多媒體架構(gòu)的一部分,通常與MediaExtractor、MediaMuxer、AudioTrack結(jié)合使用,能夠編解碼諸如H.264、H.265、AAC、3gp等常見的音視頻格式。
廣義而言,MediaCodec的工作原理就是處理輸入數(shù)據(jù)以產(chǎn)生輸出數(shù)據(jù)。具體來說,MediaCodec在編解碼的過程中使用了一組輸入/輸出緩存區(qū)來同步或異步處理數(shù)據(jù):首先,客戶端向獲取到的編解碼器輸入緩存區(qū)寫入要編解碼的數(shù)據(jù)并將其提交給編解碼器,待編解碼器處理完畢后將其轉(zhuǎn)存到編碼器的輸出緩存區(qū),同時收回客戶端對輸入緩存區(qū)的所有權(quán);
然后,客戶端從獲取到編解碼輸出緩存區(qū)讀取編碼好的數(shù)據(jù)進(jìn)行處理,待處理完畢后編解碼器收回客戶端對輸出緩存區(qū)的所有權(quán)。不斷重復(fù)整個過程,直至編碼器停止工作或者異常退出。

2. MediaCodec編碼過程
在整個編解碼過程中,MediaCodec的使用會經(jīng)歷配置、啟動、數(shù)據(jù)處理、停止、釋放幾個過程,相應(yīng)的狀態(tài)可歸納為停止 (Stopped) ,執(zhí)行 (Executing) 以及釋放(Released)三個狀態(tài),而Stopped狀態(tài)又可細(xì)分為未初始化(Uninitialized)、配置(Configured)、異常( Error),Executing狀態(tài)也可細(xì)分為讀寫數(shù)據(jù)(Flushed)、運行(Running)和流結(jié)束(End-of-Stream)。MediaCodec整個狀態(tài)結(jié)構(gòu)圖如下:

如果在這個過程中出現(xiàn)了錯誤,MediaCodec會進(jìn)入Stopped狀態(tài),我們就是要使用reset方法來重置編解碼器,否則MediaCodec所持有的資源最終會被釋放。
當(dāng)然,如果MediaCodec正常使用完畢,我們也可以向編解碼器發(fā)送EOS指令,同時調(diào)用stop和release方法終止編解碼器的使用。
2.1 創(chuàng)建編/解碼器
MediaCodec主要提供了createEncoderByType(String type)、createDecoderByType(String type)兩個方法來創(chuàng)建編解碼器,它們均需要傳入一個MIME類型多媒體格式。常見的MIME類型多媒體格式如下:
● "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
● "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
當(dāng)然,MediaCodec還提供了一個createByCodecName (String name)方法,支持使用組件的具體名稱來創(chuàng)建編解碼器。但是該方法使用起來有些麻煩,且官方是建議最好是配合MediaCodecList使用,因為MediaCodecList記錄了所有可用的編解碼器。
當(dāng)然,我們也可以使用該類對傳入的 minmeType 參數(shù)進(jìn)行判斷,以匹配出MediaCodec對該mineType類型的編解碼器是否支持。以指定MIME類型為“video/avc”為例,代碼如下:
private static MediaCodecInfo selectCodec(String mimeType) {
// 獲取所有支持編解碼器數(shù)量
int numCodecs = MediaCodecList.getCodecCount();
for (int i = 0; i < numCodecs; i++) {
// 編解碼器相關(guān)性信息存儲在MediaCodecInfo中
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
// 判斷是否為編碼器
if (!codecInfo.isEncoder()) {
continue;
}
// 獲取編碼器支持的MIME類型,并進(jìn)行匹配
String[] types = codecInfo.getSupportedTypes();
for (int j = 0; j < types.length; j++) {
if (types[j].equalsIgnoreCase(mimeType)) {
return codecInfo;
}
}
}
return null;
}
2.2 配置、啟動編/解碼器
編解碼器配置使用的是MediaCodec的configure方法,該方法首先對MediaFormat存儲的數(shù)據(jù)map進(jìn)行提取,然后調(diào)用本地方法native_configure實現(xiàn)對編解碼器的配置工作。
在配置時,configure方法需要傳入format、surface、crypto、flags參數(shù),其中format為MediaFormat的實例,它使用"key-value"鍵值對的形式存儲多媒體數(shù)據(jù)格式信息;
surface用于指明解碼器的數(shù)據(jù)源來自于該surface;crypto用于指定一個MediaCrypto對象,以便對媒體數(shù)據(jù)進(jìn)行安全解密;flags指明配置的是編碼器(CONFIGURE_FLAG_ENCODE)。
MediaFormat mFormat = MediaFormat.createVideoFormat("video/avc", 640 ,480); // 創(chuàng)建MediaFormat
mFormat.setInteger(MediaFormat.KEY_BIT_RATE,600); // 指定比特率
mFormat.setInteger(MediaFormat.KEY_FRAME_RATE,30); // 指定幀率
mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mColorFormat); // 指定編碼器顏色格式
mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,10); // 指定關(guān)鍵幀時間間隔
mVideoEncodec.configure(mFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
以上代碼是在編碼 H.264 時的配置方法,createVideoFormat("video/avc", 640 ,480)為"video/avc"類型(即H.264)編碼器的MediaFormat對象,需要指定視頻數(shù)據(jù)的寬高,如果編解碼音頻數(shù)據(jù),則調(diào)用MediaFormat的createAudioFormat(String mime, int sampleRate,int channelCount)的方法。
除了一些諸如視頻幀率、音頻采樣率等配置參數(shù),這里需要著重講解一下MediaFormat.KEY_COLOR_FORMAT配置屬性,該屬性用于指明video編碼器的顏色格式,具體選擇哪種顏色格式與輸入的視頻數(shù)據(jù)源顏色格式有關(guān)。
比如,我們都知道Camera預(yù)覽采集的圖像流通常為NV21或YV12,那么編碼器需要指定相應(yīng)的顏色格式,否則編碼得到的數(shù)據(jù)可能會出現(xiàn)花屏、疊影、顏色失真等現(xiàn)象。
MediaCodecInfo.CodecCapabilities.存儲了編碼器所有支持的顏色格式,常見顏色格式映射如下:
原始數(shù)據(jù) 編碼器
NV12(YUV420sp) ---------> COLOR_FormatYUV420PackedSemiPlanar
NV21 ----------> COLOR_FormatYUV420SemiPlanar
YV12(I420) ----------> COLOR_FormatYUV420Planar
當(dāng)編解碼器配置完畢后,就可以調(diào)用 MediaCodec 的start()方法,該方法會調(diào)用低層native_start()方法來啟動編碼器,并調(diào)用低層方法ByteBuffer[] getBuffers(input)來開辟一系列輸入、輸出緩存區(qū)。start()方法源碼如下:
public final void start() {
native_start();
synchronized(mBufferLock) {
cacheBuffers(true /* input */);
cacheBuffers(false /* input */);
}
}
2.3 數(shù)據(jù)處理
MediaCodec支持兩種模式編解碼器,即同步synchronous、異步asynchronous,所謂同步模式是指編解碼器數(shù)據(jù)的輸入和輸出是同步的,編解碼器只有處理輸出完畢才會再次接收輸入數(shù)據(jù);
而異步編解碼器數(shù)據(jù)的輸入和輸出是異步的,編解碼器不會等待輸出數(shù)據(jù)處理完畢才再次接收輸入數(shù)據(jù)。
這里,我們主要介紹下同步編解碼,因為這種方式我們用得比較多。我們知道當(dāng)編解碼器被啟動后,每個編解碼器都會擁有一組輸入和輸出緩存區(qū),但是這些緩存區(qū)暫時無法被使用,只有通過MediaCodec的dequeueInputBuffer/dequeueOutputBuffer方法獲取輸入輸出緩存區(qū)授權(quán),通過返回的ID來操作這些緩存區(qū)。
下面我們通過一段官方提供的代碼,進(jìn)行擴(kuò)展分析:
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
從上面代碼可知,當(dāng)編解碼器start后,會進(jìn)入一個for(;;)循環(huán),該循環(huán)是一個死循環(huán),以實現(xiàn)不斷地去從編解碼器的輸入緩存池中獲取包含數(shù)據(jù)的一個緩存區(qū),然后再從輸出緩存池中獲取編解碼好的輸出數(shù)據(jù)。
獲取編解碼器的輸入緩存區(qū),寫入數(shù)據(jù)
首先,調(diào)用MediaCodec的dequeueInputBuffer(long timeoutUs)方法從編碼器的輸入緩存區(qū)集合中獲取一個輸入緩存區(qū),并返回該緩存區(qū)的下標(biāo)index,如果index=-1說明暫時可用緩存區(qū),當(dāng)timeoutUs=0時dequeueInputBuffer會立馬返回。
接著調(diào)用MediaCodec的getInputBuffer(int index),該方法會將index傳入給本地方法getBuffer(true /* input */, index)返回該緩存區(qū)的ByteBuffer,并且將獲得的ByteBuffer對象及其index存儲到BufferMap對象中,以便輸入結(jié)束后對該緩存區(qū)作釋放處理,交還給編解碼器。getInputBuffer(int index)源碼如下:
@Nullable
public ByteBuffer getInputBuffer(int index) {
ByteBuffer newBuffer = getBuffer(true /* input */, index);
synchronized(mBufferLock) {
invalidateByteBuffer(mCachedInputBuffers, index);
// mDequeuedInputBuffers是BufferMap的實例
mDequeuedInputBuffers.put(index, newBuffer);
}
return newBuffer;
}
然后,在獲得輸入緩沖區(qū)后,將數(shù)據(jù)填入數(shù)據(jù)并使用queueInputBuffer將其提交到編解碼器中處理,同時將輸入緩存區(qū)釋放交還給編解碼器。queueInputBuffer源碼如下:
public final void queueInputBuffer(
int index,
int offset, int size, long presentationTimeUs, int flags)
throws CryptoException {
synchronized(mBufferLock) {
invalidateByteBuffer(mCachedInputBuffers, index);
// 移除輸入緩存區(qū)
mDequeuedInputBuffers.remove(index);
}
try {
native_queueInputBuffer(
index, offset, size, presentationTimeUs, flags);
} catch (CryptoException | IllegalStateException e) {
revalidateByteBuffer(mCachedInputBuffers, index);
throw e;
}
}
由上述代碼可知,queueInputBuffer主要通過調(diào)用低層方法native_queueInputBuffer實現(xiàn),該方法需要傳入5個參數(shù),其中index是輸入緩存區(qū)的下標(biāo),編解碼器就是通過index找到緩存區(qū)的位置;
offset為有效數(shù)據(jù)存儲在buffer中的偏移量;size為有效輸入原始數(shù)據(jù)的大?。籶resentationTimeUs為緩沖區(qū)顯示時間戳,通常為0;flags為輸入緩存區(qū)標(biāo)志,通常設(shè)置為 BUFFER_FLAG_END_OF_STREAM。
獲取編解碼器的輸出緩存區(qū),讀出數(shù)據(jù)
?首先,與上述通過dequeueInputBuffer和getInputBuffer獲取輸入緩存區(qū)類似,MediaCodec也提供了dequeueOutputBuffer和getOutputBuffer方法用來幫助我們獲取編解碼器的輸出緩存區(qū)。
但是與dequeueInputBuffer不同的是,dequeueOutputBuffer還需要傳入一個MediaCodec.BufferInfo對象。MediaCodec.BufferInfo是MediaCodec的一個內(nèi)部類,它記錄了編解碼好的數(shù)據(jù)在輸出緩存區(qū)中的偏移量和大小。
public final static class BufferInfo {
public void set(
int newOffset, int newSize, long newTimeUs, @BufferFlag int newFlags) {
offset = newOffset;
size = newSize;
presentationTimeUs = newTimeUs;
flags = newFlags;
}
public int offset // 偏移量
public int size; // 緩存區(qū)有效數(shù)據(jù)大小
public long presentationTimeUs; // 顯示時間戳
public int flags; // 緩存區(qū)標(biāo)志
@NonNull
public BufferInfo dup() {
BufferInfo copy = new BufferInfo();
copy.set(offset, size, presentationTimeUs, flags);
return copy;
}
};
然后,通過dequeueOutputBuffer的源碼可知,當(dāng)dequeueOutputBuffer返回值>=0時,輸出緩存區(qū)的數(shù)據(jù)才是有效的。
當(dāng)調(diào)用本地方法native_dequeueOutputBuffer返回INFO_OUTPUT_BUFFERS_CHANGED時,會調(diào)用cacheBuffers方法重新獲取一組輸出緩存區(qū)mCachedOutputBuffers(ByteBuffer[])。
這就解釋了如果我們使用getOutputBuffers方法(API21后被棄用,使用getOutputBuffer(index)代替)來獲取編解碼器的輸出緩存區(qū),那么就需要在調(diào)用dequeueOutputBuffer判斷其返回值,如果返回值為MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED,則需要重新獲取輸出緩存區(qū)集合。
此外,這里還要dequeueOutputBuffer的另外兩個返回值:MediaCodec.INFO_TRY_AGAIN_LATER、MediaCodec.INFO_OUTPUT_FORMAT_CHANGED,前者表示獲取編解碼器輸出緩存區(qū)超時,后者表示編解碼器數(shù)據(jù)輸出格式改變,隨后輸出的數(shù)據(jù)將使用新的格式。
因此,我們需要在調(diào)用dequeueOutputBuffer判斷返回值是否為INFO_OUTPUT_FORMAT_CHANGED,需要通過MediaCodec的getOutputFormat重新設(shè)置MediaFormt對象。
public final int dequeueOutputBuffer(
@NonNull BufferInfo info, long timeoutUs) {
int res = native_dequeueOutputBuffer(info, timeoutUs);
synchronized(mBufferLock) {
if (res == INFO_OUTPUT_BUFFERS_CHANGED) {
// 將會調(diào)用getBuffers()底層方法
cacheBuffers(false /* input */);
} else if (res >= 0) {
validateOutputByteBuffer(mCachedOutputBuffers, res, info);
if (mHasSurface) {
mDequeuedOutputInfos.put(res, info.dup());
}
}
}
return res;
}
最后,當(dāng)輸出緩存區(qū)的數(shù)據(jù)被處理完畢后,通過調(diào)用MediaCodec的releaseOutputBuffer釋放輸出緩存區(qū),并交還給編解碼器,該輸出緩存區(qū)將不能被使用,直到下一次通過dequeueOutputBuffer獲取。
releaseOutputBuffer方法接收兩個參數(shù):Index、render,其中,Index為輸出緩存區(qū)索引;render表示當(dāng)配置編碼器時指定了surface,那么應(yīng)該置為true,輸出緩存區(qū)的數(shù)據(jù)將被傳遞到surface中。源碼如下:
public final void releaseOutputBuffer(int index, boolean render) {
BufferInfo info = null;
synchronized(mBufferLock) {
invalidateByteBuffer(mCachedOutputBuffers, index);
mDequeuedOutputBuffers.remove(index);
if (mHasSurface) {
info = mDequeuedOutputInfos.remove(index);
}
}
releaseOutputBuffer(index, render, false /* updatePTS */, 0 /* dummy */);
}
推薦閱讀:
全網(wǎng)最全的 Android 音視頻和 OpenGL ES 干貨,都在這了
