close

原文鏈接: https://juejin.cn/post/7032224346491846687

1. MediaCodec工作原理

MediaCodec類Android提供的用於訪問低層多媒體編/解碼器接口,它是Android低層多媒體架構的一部分,通常與MediaExtractor、MediaMuxer、AudioTrack結合使用,能夠編解碼諸如H.264、H.265、AAC、3gp等常見的音視頻格式。

廣義而言,MediaCodec的工作原理就是處理輸入數據以產生輸出數據。具體來說,MediaCodec在編解碼的過程中使用了一組輸入/輸出緩存區來同步或異步處理數據:首先,客戶端向獲取到的編解碼器輸入緩存區寫入要編解碼的數據並將其提交給編解碼器,待編解碼器處理完畢後將其轉存到編碼器的輸出緩存區,同時收回客戶端對輸入緩存區的所有權;

然後,客戶端從獲取到編解碼輸出緩存區讀取編碼好的數據進行處理,待處理完畢後編解碼器收回客戶端對輸出緩存區的所有權。不斷重複整個過程,直至編碼器停止工作或者異常退出。

MediaCodec原理2. MediaCodec編碼過程

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

從上圖可知,當MediaCodec被創建後會進入未初始化狀態,待設置好配置信息並調用start()啟動後,MediaCodec會進入運行狀態,並且可進行數據讀寫操作。

如果在這個過程中出現了錯誤,MediaCodec會進入Stopped狀態,我們就是要使用reset方法來重置編解碼器,否則MediaCodec所持有的資源最終會被釋放。

當然,如果MediaCodec正常使用完畢,我們也可以向編解碼器發送EOS指令,同時調用stop和release方法終止編解碼器的使用。

2.1 創建編/解碼器

MediaCodec主要提供了createEncoderByType(String type)、createDecoderByType(String type)兩個方法來創建編解碼器,它們均需要傳入一個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

當然,MediaCodec還提供了一個createByCodecName (String name)方法,支持使用組件的具體名稱來創建編解碼器。但是該方法使用起來有些麻煩,且官方是建議最好是配合MediaCodecList使用,因為MediaCodecList記錄了所有可用的編解碼器。

當然,我們也可以使用該類對傳入的 minmeType 參數進行判斷,以匹配出MediaCodec對該mineType類型的編解碼器是否支持。以指定MIME類型為「video/avc」為例,代碼如下:

private static MediaCodecInfo selectCodec(String mimeType) { // 獲取所有支持編解碼器數量 int numCodecs = MediaCodecList.getCodecCount(); for (int i = 0; i < numCodecs; i++) { // 編解碼器相關性信息存儲在MediaCodecInfo中 MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); // 判斷是否為編碼器 if (!codecInfo.isEncoder()) { continue; } // 獲取編碼器支持的MIME類型,並進行匹配 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存儲的數據map進行提取,然後調用本地方法native_configure實現對編解碼器的配置工作。

在配置時,configure方法需要傳入format、surface、crypto、flags參數,其中format為MediaFormat的實例,它使用"key-value"鍵值對的形式存儲多媒體數據格式信息;

surface用於指明解碼器的數據源來自於該surface;crypto用於指定一個MediaCrypto對象,以便對媒體數據進行安全解密;flags指明配置的是編碼器(CONFIGURE_FLAG_ENCODE)。

MediaFormat mFormat = MediaFormat.createVideoFormat("video/avc", 640 ,480); // 創建MediaFormatmFormat.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); // 指定關鍵幀時間間隔mVideoEncodec.configure(mFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);

以上代碼是在編碼 H.264 時的配置方法,createVideoFormat("video/avc", 640 ,480)為"video/avc"類型(即H.264)編碼器的MediaFormat對象,需要指定視頻數據的寬高,如果編解碼音頻數據,則調用MediaFormat的createAudioFormat(String mime, int sampleRate,int channelCount)的方法。

除了一些諸如視頻幀率、音頻採樣率等配置參數,這裡需要着重講解一下MediaFormat.KEY_COLOR_FORMAT配置屬性,該屬性用於指明video編碼器的顏色格式,具體選擇哪種顏色格式與輸入的視頻數據源顏色格式有關。

比如,我們都知道Camera預覽採集的圖像流通常為NV21或YV12,那麼編碼器需要指定相應的顏色格式,否則編碼得到的數據可能會出現花屏、疊影、顏色失真等現象。

MediaCodecInfo.CodecCapabilities.存儲了編碼器所有支持的顏色格式,常見顏色格式映射如下:

原始數據 編碼器NV12(YUV420sp) ---------> COLOR_FormatYUV420PackedSemiPlanarNV21 ----------> COLOR_FormatYUV420SemiPlanarYV12(I420) ----------> COLOR_FormatYUV420Planar

當編解碼器配置完畢後,就可以調用 MediaCodec 的start()方法,該方法會調用低層native_start()方法來啟動編碼器,並調用低層方法ByteBuffer[] getBuffers(input)來開闢一系列輸入、輸出緩存區。start()方法源碼如下:

public final void start() { native_start(); synchronized(mBufferLock) { cacheBuffers(true /* input */); cacheBuffers(false /* input */); } }2.3 數據處理

MediaCodec支持兩種模式編解碼器,即同步synchronous、異步asynchronous,所謂同步模式是指編解碼器數據的輸入和輸出是同步的,編解碼器只有處理輸出完畢才會再次接收輸入數據;

而異步編解碼器數據的輸入和輸出是異步的,編解碼器不會等待輸出數據處理完畢才再次接收輸入數據。

這裡,我們主要介紹下同步編解碼,因為這種方式我們用得比較多。我們知道當編解碼器被啟動後,每個編解碼器都會擁有一組輸入和輸出緩存區,但是這些緩存區暫時無法被使用,只有通過MediaCodec的dequeueInputBuffer/dequeueOutputBuffer方法獲取輸入輸出緩存區授權,通過返回的ID來操作這些緩存區。

下面我們通過一段官方提供的代碼,進行擴展分析:

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();

從上面代碼可知,當編解碼器start後,會進入一個for(;;)循環,該循環是一個死循環,以實現不斷地去從編解碼器的輸入緩存池中獲取包含數據的一個緩存區,然後再從輸出緩存池中獲取編解碼好的輸出數據。

獲取編解碼器的輸入緩存區,寫入數據

首先,調用MediaCodec的dequeueInputBuffer(long timeoutUs)方法從編碼器的輸入緩存區集合中獲取一個輸入緩存區,並返回該緩存區的下標index,如果index=-1說明暫時可用緩存區,當timeoutUs=0時dequeueInputBuffer會立馬返回。

接着調用MediaCodec的getInputBuffer(int index),該方法會將index傳入給本地方法getBuffer(true /* input */, index)返回該緩存區的ByteBuffer,並且將獲得的ByteBuffer對象及其index存儲到BufferMap對象中,以便輸入結束後對該緩存區作釋放處理,交還給編解碼器。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; }

然後,在獲得輸入緩衝區後,將數據填入數據並使用queueInputBuffer將其提交到編解碼器中處理,同時將輸入緩存區釋放交還給編解碼器。queueInputBuffer源碼如下:

public final void queueInputBuffer( int index, int offset, int size, long presentationTimeUs, int flags) throws CryptoException { synchronized(mBufferLock) { invalidateByteBuffer(mCachedInputBuffers, index); // 移除輸入緩存區 mDequeuedInputBuffers.remove(index); } try { native_queueInputBuffer( index, offset, size, presentationTimeUs, flags); } catch (CryptoException | IllegalStateException e) { revalidateByteBuffer(mCachedInputBuffers, index); throw e; } }

由上述代碼可知,queueInputBuffer主要通過調用低層方法native_queueInputBuffer實現,該方法需要傳入5個參數,其中index是輸入緩存區的下標,編解碼器就是通過index找到緩存區的位置;

offset為有效數據存儲在buffer中的偏移量;size為有效輸入原始數據的大小;presentationTimeUs為緩衝區顯示時間戳,通常為0;flags為輸入緩存區標誌,通常設置為 BUFFER_FLAG_END_OF_STREAM。

獲取編解碼器的輸出緩存區,讀出數據

 首先,與上述通過dequeueInputBuffer和getInputBuffer獲取輸入緩存區類似,MediaCodec也提供了dequeueOutputBuffer和getOutputBuffer方法用來幫助我們獲取編解碼器的輸出緩存區。

但是與dequeueInputBuffer不同的是,dequeueOutputBuffer還需要傳入一個MediaCodec.BufferInfo對象。MediaCodec.BufferInfo是MediaCodec的一個內部類,它記錄了編解碼好的數據在輸出緩存區中的偏移量和大小。

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; // 緩存區有效數據大小 public long presentationTimeUs; // 顯示時間戳 public int flags; // 緩存區標誌 @NonNull public BufferInfo dup() { BufferInfo copy = new BufferInfo(); copy.set(offset, size, presentationTimeUs, flags); return copy; } };

然後,通過dequeueOutputBuffer的源碼可知,當dequeueOutputBuffer返回值>=0時,輸出緩存區的數據才是有效的。

當調用本地方法native_dequeueOutputBuffer返回INFO_OUTPUT_BUFFERS_CHANGED時,會調用cacheBuffers方法重新獲取一組輸出緩存區mCachedOutputBuffers(ByteBuffer[])。

這就解釋了如果我們使用getOutputBuffers方法(API21後被棄用,使用getOutputBuffer(index)代替)來獲取編解碼器的輸出緩存區,那麼就需要在調用dequeueOutputBuffer判斷其返回值,如果返回值為MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED,則需要重新獲取輸出緩存區集合。

此外,這裡還要dequeueOutputBuffer的另外兩個返回值:MediaCodec.INFO_TRY_AGAIN_LATER、MediaCodec.INFO_OUTPUT_FORMAT_CHANGED,前者表示獲取編解碼器輸出緩存區超時,後者表示編解碼器數據輸出格式改變,隨後輸出的數據將使用新的格式。

因此,我們需要在調用dequeueOutputBuffer判斷返回值是否為INFO_OUTPUT_FORMAT_CHANGED,需要通過MediaCodec的getOutputFormat重新設置MediaFormt對象。

public final int dequeueOutputBuffer( @NonNull BufferInfo info, long timeoutUs) { int res = native_dequeueOutputBuffer(info, timeoutUs); synchronized(mBufferLock) { if (res == INFO_OUTPUT_BUFFERS_CHANGED) { // 將會調用getBuffers()底層方法 cacheBuffers(false /* input */); } else if (res >= 0) { validateOutputByteBuffer(mCachedOutputBuffers, res, info); if (mHasSurface) { mDequeuedOutputInfos.put(res, info.dup()); } } } return res; }

最後,當輸出緩存區的數據被處理完畢後,通過調用MediaCodec的releaseOutputBuffer釋放輸出緩存區,並交還給編解碼器,該輸出緩存區將不能被使用,直到下一次通過dequeueOutputBuffer獲取。

releaseOutputBuffer方法接收兩個參數:Index、render,其中,Index為輸出緩存區索引;render表示當配置編碼器時指定了surface,那麼應該置為true,輸出緩存區的數據將被傳遞到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 */); }

推薦閱讀:

Android FFmpeg 實現帶濾鏡的微信小視頻錄製功能

全網最全的 Android 音視頻和 OpenGL ES 乾貨,都在這了

一文掌握 YUV 圖像的基本處理

抖音傳送帶特效是怎麼實現的?

所有你想要的圖片轉場效果,都在這了

面試官:如何利用 Shader 實現 RGBA 到 NV21 圖像格式轉換?

我用 OpenGL ES 給小姐姐做了幾個抖音濾鏡

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()