Android原生編解碼接口MediaCodec詳解

PS:有些想法可以先開始,慢慢完善才是好的選擇。
MediaCodec 是 Android 中的編解碼器組件,用來訪問底層提供的編解碼器,通常與 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface 和 AudioTrack 一起使用,MediaCodec 幾乎是 Android 播放器硬解碼的標(biāo)配,但是具體使用的是軟編解碼器還是硬解編解碼器,還是和 MediaCodec 的配置相關(guān),音視頻相關(guān)文章如下:
下面將從以下幾個方面介紹 MediaCodec,主要內(nèi)容如下
MediaCodec處理的類型
MediaCodec編解碼的流程
MediaCodec生命周期
MediaCodec的創(chuàng)建
MediaCodec的初始化
MediaCodec的數(shù)據(jù)處理方式
自適應(yīng)播放支持
MediaCodec的異常處理
MediaCodec處理的類型
MediaCodec 支持處理三種數(shù)據(jù)類型,分別是壓縮數(shù)據(jù)(compressed data)、原始音頻數(shù)據(jù)(raw audio data)、原始視頻數(shù)據(jù)(raw video data),可以使用 ByteBuffer 處理這三種數(shù)據(jù),也就是后文中提到的緩沖區(qū),對于原始視頻數(shù)據(jù),可以使用 Surface 來提高編解碼器性能,但是不能訪問原始視頻數(shù)據(jù),但是可以通過 ImageReader 訪問原始視頻幀,通過 Image 進而獲取到與之對應(yīng)的 YUV 數(shù)據(jù)等其他信息。
壓縮緩沖區(qū):用于解碼器的輸入緩沖區(qū)和用于編碼器的輸出緩沖區(qū)會包含 MediaFormat 的 KEY_MIME 對應(yīng)類型的壓縮數(shù)據(jù),對于視頻類型,通常是單個壓縮視頻幀,對于音頻數(shù)據(jù),這通常是一個編碼的音頻段,通常包含幾毫秒的音頻,因格式類型而定。
原始音頻緩沖區(qū):原始音頻緩沖區(qū)包含 PCM 音頻數(shù)據(jù)的整個幀,這是每一個通道按照通道順序的一個樣本,每個 PCM 音頻樣本都是 16 位帶符號整數(shù)或浮點數(shù)(以本機字節(jié)順序),如果要使用浮點 PCM 編碼的原始音頻緩沖區(qū),需要如下配置:
1mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING,?AudioFormat.ENCODING_PCM_FLOAT);
檢查 MediaFormat 中的浮點 PCM 的方法如下:
1?static?boolean?isPcmFloat(MediaFormat?format)?{
2??return?format.getInteger(MediaFormat.KEY_PCM_ENCODING,?AudioFormat.ENCODING_PCM_16BIT)
3??????==?AudioFormat.ENCODING_PCM_FLOAT;
4?}
提取包含 16 位帶符號整數(shù)音頻數(shù)據(jù)的緩沖區(qū)的一個通道,可以使用以下代碼:
1//?Assumes?the?buffer?PCM?encoding?is?16?bit.
2short[]?getSamplesForChannel(MediaCodec?codec,?int?bufferId,?int?channelIx)?{
3??????ByteBuffer?outputBuffer?=?codec.getOutputBuffer(bufferId);
4??????MediaFormat?format?=?codec.getOutputFormat(bufferId);
5??????ShortBuffer?samples?=?outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
6??????int?numChannels?=?format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
7??????if?(channelIx?0?||?channelIx?>=?numChannels)?{
8????????return?null;
9??????}
10?????short[]?res?=?new?short[samples.remaining()?/?numChannels];
11??????for?(int?i?=?0;?i?12????????res[i]?=?samples.get(i?*?numChannels?+?channelIx);
13??????}
14??????return?res;
15}
原始視頻緩沖區(qū):在 ByteBuffer 模式下,視頻緩沖區(qū)根據(jù)其 MediaFormat 的 KEY_COLOR_FORMAT 設(shè)置的值進行布局,可以從通過 MediaCodecInfo 相關(guān)方法獲取設(shè)備受支持的顏色格式,視頻編解碼器可能支持三種顏色格式:
native raw video format:原始原始視頻格式,由CodecCapabilities 的 COLOR_FormatSurface 常量標(biāo)記,可以與輸入或輸出Surface一起使用。
flexible YUV buffers:靈活的 YUV 緩沖區(qū),如 CodecCapabilities 的 COLOR_FormatYUV420Flexible 常量對應(yīng)的顏色格式,可以通過 getInput、OutputImage 等于與輸入、輸出 Surface 以及 ByteBuffer 模式一起使用。
other specific formats:其他特定格式:通常僅在 ByteBuffer 模式下支持這些格式, 某些顏色格式是特定于供應(yīng)商的,其他在均在 CodecCapabilities 中定義。
自 Android 5.1 開始,所有視頻編解碼器均支持靈活的 YUV 4:2:0 緩沖區(qū)。其中 MediaFormat#KEY_WIDTH 和 MediaFormat#KEY_HEIGHT 鍵指定視頻幀的大小,在大多數(shù)情況下,視頻僅占據(jù)視頻幀的一部分,具體表示如下:

需要使用以下鍵從輸出格式獲取原始輸出圖像的裁剪矩形,如果輸出格式中不存在這些鍵,則視頻將占據(jù)整個視頻幀,在使用任何 MediaFormat#KEY_ROTATION 之前,也就是在設(shè)置旋轉(zhuǎn)之前,可以使用下面的方式計算視頻幀的大小,參考如下:
1?MediaFormat?format?=?decoder.getOutputFormat(…);
2?int?width?=?format.getInteger(MediaFormat.KEY_WIDTH);
3?if?(format.containsKey("crop-left")?&&?format.containsKey("crop-right"))?{
4????width?=?format.getInteger("crop-right")?+?1?-?format.getInteger("crop-left");
5?}
6?int?height?=?format.getInteger(MediaFormat.KEY_HEIGHT);
7?if?(format.containsKey("crop-top")?&&?format.containsKey("crop-bottom"))?{
8????height?=?format.getInteger("crop-bottom")?+?1?-?format.getInteger("crop-top");
9?}
MediaCodec編解碼的流程
MediaCodec 首先獲取一個空的輸入緩沖區(qū),填充要編碼或解碼的數(shù)據(jù),再將填充數(shù)據(jù)的輸入緩沖區(qū)送到 MediaCodec 進行處理,處理完數(shù)據(jù)后會釋放這個填充數(shù)據(jù)的輸入緩沖區(qū),最后獲取已經(jīng)編碼或解碼的輸出緩沖區(qū),使用完畢后釋放輸出緩沖區(qū),其編解碼的流程示意圖如下:

各個階段對應(yīng)的 API 如下:
1//?獲取可用的輸入緩沖區(qū)的索引
2public?int?dequeueInputBuffer?(long?timeoutUs)
3//?獲取輸入緩沖區(qū)
4public?ByteBuffer?getInputBuffer(int?index)
5//?將填滿數(shù)據(jù)的inputBuffer提交到編碼隊列
6public?final?void?queueInputBuffer(int?index,int?offset,?int?size,?long?presentationTimeUs,?int?flags)
7//?獲取已成功編解碼的輸出緩沖區(qū)的索引
8public?final?int?dequeueOutputBuffer(BufferInfo?info,?long?timeoutUs)
9//?獲取輸出緩沖區(qū)
10public?ByteBuffer?getOutputBuffer(int?index)
11//?釋放輸出緩沖區(qū)
12public?final?void?releaseOutputBuffer(int?index,?boolean?render)?
13
MediaCodec生命周期
MediaCodec 有三種狀態(tài),分別是執(zhí)行(Executing)、停止(Stopped)和釋放(Released),其中執(zhí)行和停止分別有三個子狀態(tài),執(zhí)行的三個字狀態(tài)分別是 Flushed、Running 和 Stream-of-Stream,停止的三個子狀態(tài)分別是 Uninitialized、Configured 和 Error,MediaCodec 生命周期示意圖如下:

停止?fàn)顟B(tài)(Stopped)
1//?創(chuàng)建MediaCodec進入Uninitialized子狀態(tài)
2public?static?MediaCodec?createByCodecName?(String?name)
3public?static?MediaCodec?createEncoderByType?(String?type)
4public?static?MediaCodec?createDecoderByType?(String?type)
5//?配置MediaCodec進入Configured子狀態(tài),crypto和descrambler會在后文中進行說明
6public?void?configure(MediaFormat?format,?Surface?surface,?MediaCrypto?crypto,?int?flags)
7public?void?configure(MediaFormat?format,?@Nullable?Surface?surface,int?flags,?MediaDescrambler?descrambler)
8//?Error
9//?編解碼過程中遇到錯誤進入Error子狀態(tài)
執(zhí)行狀態(tài)(Executing)
1//?start之后立即進入Flushed子狀態(tài)
2public?final?void?start()
3//?第一個輸入緩沖區(qū)出隊的時候進入Running子狀態(tài)
4public?int?dequeueInputBuffer?(long?timeoutUs)
5//?輸入緩沖區(qū)與流結(jié)束標(biāo)記排隊時,編解碼器將轉(zhuǎn)換為End-of-Stream子狀態(tài)
6//?此時MediaCodec將不接受其他輸入緩沖區(qū),但會生成輸出緩沖區(qū)
7public?void?queueInputBuffer?(int?index,?int?offset,?int?size,?long?presentationTimeUs,?int?flags)
釋放狀態(tài)(Released)
1//?編解碼完成結(jié)束后釋放MediaCodec進入釋放狀態(tài)(Released)
2public?void?release?()
MediaCodec的創(chuàng)建
前面已經(jīng)提到過當(dāng)創(chuàng)建 MediaCodec 的時候進入Uninitialized 子狀態(tài),其創(chuàng)建方式如下:
1//?創(chuàng)建MediaCodec
2public?static?MediaCodec?createByCodecName?(String?name)
3public?static?MediaCodec?createEncoderByType?(String?type)
4public?static?MediaCodec?createDecoderByType?(String?type)
5
使用 createByCodecName 時可以借助 MediaCodecList 獲取支持的編解碼器,下面是獲取指定 MIME 類型的編碼器:
1/**
2?*?查詢指定MIME類型的編碼器
3?*/
4fun?selectCodec(mimeType:?String):?MediaCodecInfo??{
5????val?mediaCodecList?=?MediaCodecList(MediaCodecList.REGULAR_CODECS)
6????val?codeInfos?=?mediaCodecList.codecInfos
7????for?(codeInfo?in?codeInfos)?{
8????????if?(!codeInfo.isEncoder)?continue
9????????val?types?=?codeInfo.supportedTypes
10????????for?(type?in?types)?{
11????????????if?(type.equals(mimeType,?true))?{
12????????????????return?codeInfo
13????????????}
14????????}
15????}
16????return?null
17}
當(dāng)然 MediaCodecList 也提供了相應(yīng)的獲取編解碼器的方法,如下:
1//?獲取指定格式的編碼器
2public?String?findEncoderForFormat?(MediaFormat?format)
3//?獲取指定格式的解碼器
4public?String?findDecoderForFormat?(MediaFormat?format)
5
對于上述方法中的參數(shù) MediaFormat 格式中不能包含任何幀率的設(shè)置,如果已經(jīng)設(shè)置了幀率需要將其清除再使用。
上面提到了 MediaCodecList,這里簡單說一下,使用 MediaCodecList 可以方便的列出當(dāng)前設(shè)備支持的所有的編解碼器,創(chuàng)建 MediaCodec 的時候要選擇當(dāng)前格式支持的編解碼器,也就是選擇的編解碼器需支持對應(yīng)的 MediaFormat,每個編解碼器都被包裝成一個 MediaCodecInfo 對象,據(jù)此可以查看該編碼器的一些特性,比如是否支持硬件加速、是軟解還是硬解編解碼器等,常用的簡單如下:
1//?是否軟解
2public?boolean?isSoftwareOnly?()
3//?是Android平臺提供(false)還是廠商提供(true)的編解碼器
4public?boolean?isVendor?()
5//?是否支持硬件加速
6public?boolean?isHardwareAccelerated?()
7//?是編碼器還是解碼器
8public?boolean?isEncoder?()
9//?獲取當(dāng)前編解碼器支持的合適
10public?String[]?getSupportedTypes?()
11//?...
軟解和硬解應(yīng)該是音視頻開發(fā)中必須掌握的,當(dāng)使用 MediaCodec 的時候不能說全是硬解,到底使用硬解還是軟解還是要看使用的編碼器,一般廠商提供的編解碼器都是硬解編解碼器,比如高通(qcom)等,一般如系統(tǒng)提供的則是軟解編解碼器,如帶有 android 字樣的編解碼器,下面是本人(MI 10 Pro)自己手機的部分編解碼器:
1//?硬解編解碼器
2OMX.qcom.video.encoder.heic
3OMX.qcom.video.decoder.avc
4OMX.qcom.video.decoder.avc.secure
5OMX.qcom.video.decoder.mpeg2
6OMX.google.gsm.decoder
7OMX.qti.video.decoder.h263sw
8c2.qti.avc.decoder
9...
10//?軟解編解碼器
11c2.android.aac.decoder
12c2.android.aac.decoder
13c2.android.aac.encoder
14c2.android.aac.encoder
15c2.android.amrnb.decoder
16c2.android.amrnb.decoder
17...
MediaCodec初始化
創(chuàng)建 MediaCodec 之后進入 Uninitialized 子狀態(tài),此時需要對其進行一些設(shè)置如指定 MediaFormat、如果使用的是異步處理數(shù)據(jù)的方式,在 configure 之前要設(shè)置 MediaCodec.Callback,關(guān)鍵 API 如下:
1//?1.?MediaFormat
2//?創(chuàng)建MediaFormat
3public?static?final?MediaFormat?createVideoFormat(String?mime,int?width,int?height)
4//?開啟或關(guān)閉功能,具體參見MediaCodeInfo.CodecCapabilities
5public?void?setFeatureEnabled(@NonNull?String?feature,?boolean?enabled)
6//?參數(shù)設(shè)置
7public?final?void?setInteger(String?name,?int?value)
8
9//?2.?setCallback
10//?如果使用的是異步處理數(shù)據(jù)的方式,在configure?之前要設(shè)置?MediaCodec.Callback
11public?void?setCallback?(MediaCodec.Callback?cb)
12public?void?setCallback?(MediaCodec.Callback?cb,?Handler?handler)
13
14//?3.?配置
15public?void?configure(MediaFormat?format,?Surface?surface,?MediaCrypto?crypto,?int?flags)
16public?void?configure(MediaFormat?format,?@Nullable?Surface?surface,int?flags,?MediaDescrambler?descrambler)
上面 configure 配置中涉及到幾個參數(shù),其中 surface 表示解碼器要渲染的 Surface,flags 則是指定當(dāng)前編解碼器是作為編碼器還是解碼器來使用的,crypto 和 descrambler 都和解密有關(guān),比如某些 vip 視頻就需要特定的密鑰來配合解碼,只有用戶登錄校驗后才會對視頻內(nèi)容進行解密,要不然某些需要付費才能觀看的視頻下載之后就能隨意傳播了,更多細節(jié)可以查看音視頻中的數(shù)字版權(quán)技術(shù)。
此外某些特定格式比如 AAC 音頻以及 MPEG4、H.264、H.265 視頻格式,這些格式包含一些用于 MediaCodec 的初始化特定的數(shù)據(jù),當(dāng)解碼處理這些壓縮格式時,必須在 start 之后且在任何幀數(shù)據(jù)處理之前將這些特定數(shù)據(jù)提交給 MediaCodec,即在對 queueInputBuffer 的調(diào)用中使用標(biāo)志 BUFFER_FLAG_CODEC_CONFIG 標(biāo)記此類數(shù)據(jù),這些特定的數(shù)據(jù)也可以通過 MediaFormat 設(shè)置 ByteBuffer 的方式進行配置,如下:
1//?csd-0、csd-1、csd-2同理
2val?bytes?=?byteArrayOf(0x00.toByte(),?0x01.toByte())
3mediaFormat.setByteBuffer("csd-0",?ByteBuffer.wrap(bytes))
其中 csd-0、csd-1 這些鍵可以從 MediaExtractor#getTrackFormat 獲取的MediaFormat中獲取,這些特定的數(shù)據(jù)會在start 時自動提交給 MediaCodec,無需直接提交此數(shù)據(jù),如果在輸出緩沖區(qū)或格式更改之前調(diào)用了 flush,則會丟失提交的特定數(shù)據(jù),就需要在 ?queueInputBuffer 的調(diào)用中使用標(biāo)志 BUFFER_FLAG_CODEC_CONFIG 標(biāo)記這類數(shù)據(jù)。
Android 使用以下特定于編解碼器的數(shù)據(jù)緩沖區(qū),為了正確配置 MediaMuxer 軌道,還需要將它們設(shè)置為軌道格式,每個參數(shù)集和標(biāo)有(*)的編解碼器專用數(shù)據(jù)部分必須以“ \ x00 \ x00 \ x00 \ x01”的起始代碼開頭,參考如下:

編碼器在收到這些信息后將會同樣輸出帶有BUFFER_FLAG_CODEC_CONFIG標(biāo)記的 outputbuffer,此時這些數(shù)據(jù)就是特定數(shù)據(jù),不是媒體數(shù)據(jù)。
MediaCodec數(shù)據(jù)處理方式
每個創(chuàng)建已經(jīng)創(chuàng)建的編解碼器都維護一組輸入緩沖區(qū),有兩種處理數(shù)據(jù)的方式,同步和異步方式,根據(jù) API 版本不同有所區(qū)別,在 API 21 也就是從 Android5.0 開始,推薦使用 ButeBuffer 的方式進行數(shù)據(jù)的處理,在此之前只能使用 ButeBuffer ?數(shù)組的方式進行數(shù)據(jù)的處理,如下:

MediaCodec,也就是編解碼器的數(shù)據(jù)處理,主要是獲取輸入、輸出緩沖區(qū)、提交數(shù)據(jù)給編解碼器、釋放輸出緩沖區(qū)這幾個過程,同步方式和異步方式的不同點在于輸入緩沖區(qū)和輸出緩沖區(qū)的其關(guān)鍵 API 如下:
1//?獲取輸入緩沖區(qū)(同步)
2public?int?dequeueInputBuffer?(long?timeoutUs)
3public?ByteBuffer?getInputBuffer?(int?index)
4//?獲取輸出緩沖區(qū)(同步)
5public?int?dequeueOutputBuffer?(MediaCodec.BufferInfo?info,?long?timeoutUs)
6public?ByteBuffer?getOutputBuffer?(int?index)
7//?輸入、輸出緩沖區(qū)索引從MediaCodec.Callback的回調(diào)中獲取,在獲取對應(yīng)的輸入、輸出緩沖區(qū)(異步)
8public?void?setCallback?(MediaCodec.Callback?cb)
9public?void?setCallback?(MediaCodec.Callback?cb,?Handler?handler)
10//?提交數(shù)據(jù)
11public?void?queueInputBuffer?(int?index,?int?offset,?int?size,?long?presentationTimeUs,?int?flags)
12public?void?queueSecureInputBuffer?(int?index,?int?offset,?MediaCodec.CryptoInfo?info,?long?presentationTimeUs,?int?flags)
13//?釋放輸出緩沖區(qū)
14public?void?releaseOutputBuffer?(int?index,?boolean?render)
15public?void?releaseOutputBuffer?(int?index,?long?renderTimestampNs)
16
下面主要介紹介紹適用于 Android 5.0 之后的 ButeBuffer 的方式,
同步處理模式
1MediaCodec?codec?=?MediaCodec.createByCodecName(name);
2?codec.configure(format,?…);
3?MediaFormat?outputFormat?=?codec.getOutputFormat();?//?option?B
4?codec.start();
5?for?(;;)?{
6??int?inputBufferId?=?codec.dequeueInputBuffer(timeoutUs);
7??if?(inputBufferId?>=?0)?{
8????ByteBuffer?inputBuffer?=?codec.getInputBuffer(…);
9????//?使用有效數(shù)據(jù)填充輸入緩沖區(qū)
10????…
11????codec.queueInputBuffer(inputBufferId,?…);
12??}
13??int?outputBufferId?=?codec.dequeueOutputBuffer(…);
14??if?(outputBufferId?>=?0)?{
15????ByteBuffer?outputBuffer?=?codec.getOutputBuffer(outputBufferId);
16????MediaFormat?bufferFormat?=?codec.getOutputFormat(outputBufferId);?//?option?A
17????//?bufferFormat與outputFormat是相同的
18????//?輸出緩沖區(qū)已準(zhǔn)備后被處理或渲染了
19????…
20????codec.releaseOutputBuffer(outputBufferId,?…);
21??}?else?if?(outputBufferId?==?MediaCodec.INFO_OUTPUT_FORMAT_CHANGED)?{
22????//?輸出格式改變,后續(xù)采用新格式,此時使用getOutputFormat()獲取新格式
23????//?如果使用getOutputFormat(outputBufferId)獲取特定緩沖區(qū)的格式,則無需監(jiān)聽格式變化
24????outputFormat?=?codec.getOutputFormat();?//?option?B
25??}
26?}
27?codec.stop();
28?codec.release();
異步處理模式
1MediaCodec?codec?=?MediaCodec.createByCodecName(name);
2?codec.configure(format,?…);
3?MediaFormat?outputFormat?=?codec.getOutputFormat();?//?option?B
4?codec.start();
5?for?(;;)?{
6??int?inputBufferId?=?codec.dequeueInputBuffer(timeoutUs);
7??if?(inputBufferId?>=?0)?{
8????ByteBuffer?inputBuffer?=?codec.getInputBuffer(…);
9????//?使用有效數(shù)據(jù)填充輸入緩沖區(qū)
10????…
11????codec.queueInputBuffer(inputBufferId,?…);
12??}
13??int?outputBufferId?=?codec.dequeueOutputBuffer(…);
14??if?(outputBufferId?>=?0)?{
15????ByteBuffer?outputBuffer?=?codec.getOutputBuffer(outputBufferId);
16????MediaFormat?bufferFormat?=?codec.getOutputFormat(outputBufferId);?//?option?A
17????//?bufferFormat與outputFormat是相同的
18????//?輸出緩沖區(qū)已準(zhǔn)備后被處理或渲染了
19????…
20????codec.releaseOutputBuffer(outputBufferId,?…);
21??}?else?if?(outputBufferId?==?MediaCodec.INFO_OUTPUT_FORMAT_CHANGED)?{
22????//?輸出格式改變,后續(xù)采用新格式,此時使用getOutputFormat()獲取新格式
23????//?如果使用getOutputFormat(outputBufferId)獲取特定緩沖區(qū)的格式,則無需監(jiān)聽格式變化
24????outputFormat?=?codec.getOutputFormat();?//?option?B
25??}
26?}
27?codec.stop();
28?codec.release();
1//?創(chuàng)建輸入Surface,需在configure之后、start之前調(diào)用
2public?Surface?createInputSurface?()
3//?設(shè)置輸入Surface
4public?void?setInputSurface?(Surface?surface)
5//?發(fā)送流結(jié)束的信號
6public?void?signalEndOfInputStream?()
7
1//?設(shè)置輸出Surface
2public?void?setOutputSurface?(Surface?surface)
3//?false表示不渲染這個buffer,true表示使用默認的時間戳渲染這個buffer
4public?void?releaseOutputBuffer?(int?index,?boolean?render)
5//?使用指定的時間戳渲染這個buffer
6public?void?releaseOutputBuffer?(int?index,?long?renderTimestampNs)
7
自適應(yīng)播放支持
1//?是否支持某項功能,CodecCapabilities#FEATURE_AdaptivePlayback對應(yīng)對應(yīng)自適應(yīng)播放支持
2public?boolean?isFeatureSupported?(String?name)
3

MediaCodec的異常處理
1//?true表示可以通過stop、configure、start來恢復(fù)
2public?boolean?isRecoverable?()
3//?true表示暫時性問題,編碼或解碼操作會在后續(xù)重試進行
4public?boolean?isTransient?()

技術(shù)交流,歡迎加我微信:ezglumes ,拉你入技術(shù)交流群。
推薦閱讀:
開通專輯 | 細數(shù)那些年寫過的技術(shù)文章專輯
覺得不錯,點個在看唄~

