本篇文章是對(duì)官方的文檔MediaCodec的翻譯,一些地方使用了比較通俗易懂的敘述。算是對(duì)于音視頻方面知識(shí)學(xué)習(xí)的開篇吧。
注:一些英文單詞和翻譯后的對(duì)照
- codec:編解碼器
- input buffer:輸入緩沖區(qū)
- output buffer:輸出緩沖區(qū)
緩沖區(qū)/緩沖區(qū)數(shù)組用代碼表示更清晰:
//緩沖區(qū)
ByteBuffer byteBuffer;
//緩沖區(qū)數(shù)組
ByteBuffer[] mCachedInputBuffers;
MediaCodec類可以用來訪問低級(jí)別的媒體編解碼器,即編碼器/解碼器組件。它是Android低水平的多媒體支持組件的一部分,通常和MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, AudioTrack 一起使用。

廣義上來說,一個(gè)編解碼器處理輸入數(shù)據(jù)生成輸出數(shù)據(jù)。它異步的處理數(shù)據(jù)并使用一組輸入和輸出緩沖區(qū)。在一個(gè)簡(jiǎn)單的層面上,你請(qǐng)求(或接收)一個(gè)空的輸入緩沖區(qū),將其填充數(shù)據(jù)并將其交給編解碼器進(jìn)行處理。編解碼器將輸入的數(shù)據(jù)處理以后會(huì)將輸出數(shù)據(jù)填充到一個(gè)空的輸出緩沖區(qū)進(jìn)行輸出。最后,你請(qǐng)求(或接收)一個(gè)填充了數(shù)據(jù)的輸出緩沖區(qū),消費(fèi)其中的內(nèi)容,然后將這個(gè)輸出緩沖區(qū)釋放給編解碼器。(上面的圖片表達(dá)的更清晰)
數(shù)據(jù)類型
MediaCodec可以處理3中類型的數(shù)據(jù):壓縮的數(shù)據(jù)、原始的音頻數(shù)據(jù)、原始的視頻數(shù)據(jù)。三種類型的數(shù)據(jù)都可以使用ByteBuffers處理,但是你應(yīng)該使用Surface來處理原始視頻數(shù)據(jù),這樣可以提高編解碼器的性能。Surface使用本地(native)視頻緩沖區(qū)沒有將其映射或者拷貝到ByteBuffers,因此會(huì)更高效。當(dāng)使用Surface的時(shí)候,正常情況下你無法訪問原始視頻數(shù)據(jù),但是你可以使用ImageReader來訪問不安全的未編碼的(就是原始類型)的視頻幀(frame)。這肯能仍然比使用ByteBuffers高效,因?yàn)橐恍┍镜?native)的緩沖區(qū)可能被映射到了直接緩沖區(qū)(direct ByteBuffers)。當(dāng)使用ByteBuffer模式的時(shí)候,你可以使用Image類和getInput/OutputImage(int)來訪問原始的視頻幀。
壓縮的數(shù)據(jù)
輸入緩沖區(qū)和輸出緩沖區(qū)根據(jù) format's type包含壓縮數(shù)據(jù)。對(duì)于視頻類型來說,通常是單個(gè)壓縮過的視頻幀。對(duì)音頻數(shù)據(jù)來說通常是一個(gè)單獨(dú)的訪問單元(access unit),但是因?yàn)橐粋€(gè)緩沖區(qū)可能包含多個(gè)編碼的音頻訪問單元,這個(gè)要求可以稍微放松一點(diǎn)。無論哪種情況,緩沖區(qū)都不會(huì)在任意字節(jié)邊界處開始或結(jié)束,而是在幀/訪問單元邊界處開始或結(jié)束,除非使用了BUFFER_FLAG_PARTIAL_FRAME標(biāo)記。
原始的音頻數(shù)據(jù)
原始的音頻緩沖區(qū)包含PCM音頻數(shù)據(jù)的整個(gè)幀,這是按照通道順序的每個(gè)通道的一個(gè)樣本。每個(gè)PCM音頻樣本都是16位帶符號(hào)整數(shù)或浮點(diǎn)數(shù),以本地字節(jié)順序(in native byte order)。只有在MediaCodec在configure(…)期間將MediaFormat#KEY_PCM_ENCODING設(shè)置為AudioFormat#ENCODING_PCM_FLOAT并且在解碼時(shí)由getOutputFormat()確認(rèn)或者在編碼時(shí)由getInputFormat()確認(rèn)時(shí),原始的音頻數(shù)據(jù)才有可能是浮點(diǎn)型PCM編碼。一個(gè)檢查MediaFormat是不是浮點(diǎn)型PCM編碼的簡(jiǎn)單方法如下所示:
static boolean isPcmFloat(MediaFormat format) {
return format.getInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT)
== AudioFormat.ENCODING_PCM_FLOAT;
}
為了從一個(gè)包含16位帶符號(hào)整數(shù)的音頻數(shù)據(jù)緩沖區(qū)中提取一個(gè)信道(channel)的數(shù)據(jù)到一個(gè)short類型的數(shù)組中,下面的代碼可能被用到:(這段英文原文如下:In order to extract, in a short array, one channel of a buffer containing 16 bit signed integer audio data, the following code may be used:)
// Assumes the buffer PCM encoding is 16 bit.
short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
MediaFormat format = codec.getOutputFormat(bufferId);
ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
int numChannels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
if (channelIx < 0 || channelIx >= numChannels) {
return null;
}
short[] res = new short[samples.remaining() / numChannels];
for (int i = 0; i < res.length; ++i) {
res[i] = samples.get(i * numChannels + channelIx);
}
return res;
}
原始的視頻數(shù)據(jù)
在ByteBuffer的模式下,視頻緩沖區(qū)根據(jù)其 color format進(jìn)行存儲(chǔ)(laid out)。你可以從getCodecInfo()、getCapabilitiesForType(…)、colorFormats獲取支持的色彩格式(color format)作為一個(gè)數(shù)組。視頻解碼器可能支持3中色彩格式。
本地原始視頻格式:該類型由CodecCapabilities#COLOR_FormatSurface
標(biāo)記,可以和一個(gè)輸入/輸出Surface一起使用。靈活的YUV緩沖區(qū)(例如CodecCapabilities#COLOR_FormatYUV420Flexible):可以與一個(gè)輸入/輸出
Surface一起使用,也可以在 ByteBuffer 模式下通過getInput/OutputImage(int)使用。其他,特定的格式:這些通常僅在 ByteBuffer 模式下使用。一些色彩格式基于特定的供應(yīng)商。其他的格式定義在CodecCapabilities中。對(duì)于等同于靈活格式(是指靈活的YUV緩沖區(qū)嗎?)的色彩格式,你仍然通過getInput/OutputImage(int)的方式進(jìn)行使用。
從Build.VERSION_CODES.LOLLIPOP_MR1開始,所有的視頻解碼器支持靈活的 YUV 4:2:0 buffers 。
在老的設(shè)備上訪問原始視頻數(shù)據(jù)
在Build.VERSION_CODES.LOLLIPOP和Image
支持之前,你需要使用MediaFormat#KEY_STRIDE和MediaFormat#KEY_SLICE_HEIGHT輸出格式的值來理解原始輸出緩沖區(qū)的布局(layout)。這里的布局我的理解就是存儲(chǔ)方式。
MediaFormat#KEY_WIDTH和MediaFormat#KEY_HEIGHT指定了視頻幀的尺寸;但是大多數(shù)編碼的視頻(圖像)只占視頻幀的一部分。這由剪裁矩形(crop rectangle)表示。
你需要使用以下的鍵(key)從輸出格式(output format)中獲取原始輸出圖像的剪裁矩形。如果這些key沒有對(duì)應(yīng)的值(value),那么視頻(圖像)就占據(jù)整個(gè)視頻幀。注意應(yīng)該在應(yīng)用任何 rotation之前獲取輸出幀的裁剪矩形(The crop rectangle is understood in the context of the output frame before applying any rotation)。
| 格式鍵 | 類型 | 描述 |
|---|---|---|
| crop-left | Integer | 剪裁矩形的左坐標(biāo) |
| crop-top | Integer | 剪裁矩形的頂部坐標(biāo) |
| crop-right | Integer | 剪裁矩形的右坐標(biāo)減1 |
| crop-bottom | Integer | 剪裁矩形的底部坐標(biāo)減1 |
右坐標(biāo)和底坐標(biāo)可以理解為裁剪后的輸出圖像的最右邊有效列/最下面有效行的坐標(biāo)。
視頻幀的尺寸(在旋轉(zhuǎn)之前)可以這樣計(jì)算:
MediaFormat format = decoder.getOutputFormat(…);
int width = format.getInteger(MediaFormat.KEY_WIDTH);
if (format.containsKey("crop-left") && format.containsKey("crop-right")) {
width = format.getInteger("crop-right") + 1 - format.getInteger("crop-left");
}
int height = format.getInteger(MediaFormat.KEY_HEIGHT);
if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) {
height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top");
}
另請(qǐng)注意,BufferInfo.offset在不同設(shè)備之間的含義不一致。在某些設(shè)備上,偏移量是指裁剪矩形的左上像素,而在大多數(shù)設(shè)備上,偏移量是指整個(gè)幀的左上像素。
狀態(tài)
在整個(gè)生命周期中,編解碼器理論上處于以下三個(gè)狀態(tài)之一:停止(Stopped),執(zhí)行(Executing),釋放(Released)。停止的狀態(tài)實(shí)際上是三個(gè)狀態(tài)的集合:未初始化(Uninitialized),已配置(Configured)和錯(cuò)誤(Error)。執(zhí)行狀態(tài)從概念上是在3個(gè)子狀態(tài)之間變化:刷新(Flushed), 運(yùn)行(Running) 和流結(jié)束(End-of-Stream)。

當(dāng)你使用某個(gè)工廠方法創(chuàng)建編解碼器的時(shí)候,編解碼器處于未初始化的狀態(tài)。首先,你需要通過configure(…)來配置它,使它進(jìn)入已配置的狀態(tài),然后調(diào)用start()方法使它進(jìn)入到執(zhí)行狀態(tài)。當(dāng)編解碼器處于執(zhí)行狀態(tài)的時(shí)候,你可以通過上述緩沖區(qū)隊(duì)列操作來處理數(shù)據(jù)。
執(zhí)行狀態(tài)有3個(gè)子狀態(tài):刷新(Flushed), 運(yùn)行(Running) 和流結(jié)束(End-of-Stream)。在調(diào)用start()
方法之后,編解碼器立即進(jìn)入 Flushed 子狀態(tài),在此狀態(tài)之下,編解碼器持有所有的緩沖區(qū)。一旦第一個(gè)輸入緩沖區(qū)出隊(duì)(dequeued),編解碼器轉(zhuǎn)移到 Running 子狀態(tài),編解碼器的整個(gè)生命周期大部分時(shí)間處于此狀態(tài)。當(dāng)你使用end-of-stream marker標(biāo)記入隊(duì)一個(gè)輸入緩沖區(qū)的時(shí)候,編解碼器轉(zhuǎn)移到 End-of-Stream 子狀態(tài)。在此狀態(tài)下,編解碼器不再接收新的輸入緩沖區(qū),但是依然會(huì)生成輸出緩沖區(qū),直到在輸出端達(dá)到流結(jié)束為止。當(dāng)編解碼器處于執(zhí)行狀態(tài)的時(shí)候,你可以在任何時(shí)間使用flush()方法返回到 Flushed 子狀態(tài)。
調(diào)用stop()方法可以使編解碼器回到未初始化的狀態(tài),然后可以被重新配置。當(dāng)你使用編解碼器完成了所有的操作,你必須調(diào)用release()方法來釋放它。
在極少數(shù)情況下,編解碼器可能會(huì)遇到錯(cuò)誤并進(jìn)入 Error 狀態(tài)。在入隊(duì)操作的時(shí)候通過返回一個(gè)不合法的值或者通過一個(gè)異常來傳達(dá)此信息。通過調(diào)用reset()方法來使編解碼器可以重新使用。你可以從任何狀態(tài)下調(diào)用此方法來讓編解碼器回到未初始化的狀態(tài)。也可以調(diào)用release()方法將編解碼器移動(dòng)到已釋放的狀態(tài)。
創(chuàng)建
使用MediaCodecList來為指定的MediaFormat格式 創(chuàng)建一個(gè) MediaCodec 。 當(dāng)解碼一個(gè)文件或流的時(shí)候,你可以從MediaExtractor.getTrackFormat獲取想要的格式。使用MediaFormat.setFeatureEnabled注入你想要添加的特定功能,然后調(diào)用MediaCodecList.findDecoderForFormat方法來獲取可以處理特定媒體格式的編解碼器的名稱。最后,使用createByCodecName(String)來創(chuàng)建編解碼器。
注意:在Build.VERSION_CODES.LOLLIPOP版本上,MediaCodecList.findDecoder/EncoderForFormat不能包含幀率(frame rate)。使用format.setString(MediaFormat.KEY_FRAME_RATE, null)來清除媒體格式中和幀率相關(guān)的設(shè)置。
你也可以使用createDecoder/EncoderByType(java.lang.String)為特定的MIME類型創(chuàng)建首選編碼器。但是,這不能用于注入功能,并且可能無法處理特定的媒體格式。
創(chuàng)建安全的解碼器
在Build.VERSION_CODES.KITKAT_WATCH以及更早的版本上,安全編解碼器可能未在MediaCodecList中列出,但在系統(tǒng)上仍然可用。安全編解碼器只能通過名稱實(shí)例化,通過在常規(guī)的編解碼器的名稱后面添加".secure"(安全的編解碼器必須以".secure"結(jié)尾),當(dāng)系統(tǒng)中沒有相應(yīng)的編解碼器的時(shí)候createByCodecName(String)方法會(huì)拋出一個(gè)IOException。
從Build.VERSION_CODES.LOLLIPOP開始,你使用媒體格式中的CodecCapabilities#FEATURE_SecurePlayback
功能來創(chuàng)建一個(gè)安全的解碼器。
初始化
在編解碼器創(chuàng)建之后,你如果想異步處理數(shù)據(jù)的話,你可以使用setCallback方法設(shè)置一個(gè)回調(diào)。然后使用指定的媒體格式配置(configure)編解碼器。這時(shí)候你可以你可以為視頻生產(chǎn)者(生成原始視頻數(shù)據(jù)的編解碼器)指定輸出Surface。這時(shí)候你也可以為安全的編解碼器(see MediaCrypto)
指定解密參數(shù)。最后,由于一個(gè)編解碼器可以運(yùn)行多種模式,你必須指定你想讓編解碼器作為一個(gè)解碼器還是作為一個(gè)編碼器工作。
在Build.VERSION_CODES.LOLLIPOP之后,你可以在已配置的狀態(tài)下查詢輸入和輸出格式。你可以使用這種方式在啟動(dòng)編解碼器之前來驗(yàn)證配置的結(jié)果,例如色彩模式等等。
如果你想使用一個(gè)視頻編解碼器以本地方式(natively)處理原始的視頻輸入,你可以在配置狀態(tài)之后使用createInputSurface()為輸入數(shù)據(jù)創(chuàng)建一個(gè)目標(biāo)Surface(destination Surface)。
或者通過調(diào)用setInputSurface(Surface)來設(shè)置編解碼器使用先前創(chuàng)建的持久性輸入surface(persistent input surface)。
編解碼器特定的數(shù)據(jù)(Codec-specific Data)
一些格式,尤其是AAC音頻和MPEG4、H.264和H.265視頻格式,要求實(shí)際數(shù)據(jù)以包含設(shè)置數(shù)據(jù)(setup data)或編解碼器特定數(shù)據(jù)的多個(gè)緩沖區(qū)作為前綴。處理此類壓縮格式時(shí),必須在調(diào)用start()方法之后和在提交任何幀(Frame)數(shù)據(jù)之前將此數(shù)據(jù)提交給編解碼器。在調(diào)用queueInputBuffer方法的時(shí)候,此類數(shù)據(jù)必須使用BUFFER_FLAG_CODEC_CONFIG進(jìn)行標(biāo)記。
編碼器特定數(shù)據(jù)也可以在包含在傳遞給configure方法的MediaFormat中。這些特定數(shù)據(jù)是MediaFormat中以"csd-0", "csd-1"等鍵對(duì)應(yīng)的ByteBuffer條目。這些鍵通常包含在從MediaExtractor中獲取到達(dá)軌道格式中(track MediaFormat)。
編解碼器特定數(shù)據(jù)在編解碼器start()之后會(huì)自動(dòng)提交給編解碼器。你必須不能顯式提交這些數(shù)據(jù)。如果格式不包含編碼器特定數(shù)據(jù),你可以根據(jù)格式要求,選擇使用特定數(shù)字的緩沖區(qū)以正確的順序提交它(編碼器特定數(shù)據(jù))。對(duì)于H.264 AVC,您還可以連接所有編解碼器特定數(shù)據(jù),并將其作為單個(gè)編解碼器配置緩沖區(qū)提交。
Android使用以下特定于編解碼器的數(shù)據(jù)緩沖區(qū)。為了正確的MediaMuxer軌道配置,這些格式也被要求設(shè)置到軌道格式中。每個(gè)參數(shù)集和標(biāo)有(*)的編解碼器特定數(shù)據(jù)段均必須以"\x00\x00\x00\x01"開頭。
具體的表格可以參考原文:Codec-specific Data
注意:如果在start()之后,在任何輸出任何緩沖區(qū)或輸出格式改變返回之前刷新(flushed)編解碼器的時(shí)候要格外小心,因?yàn)榫幗獯a器特定數(shù)據(jù)在刷新過程中可能會(huì)丟失。你必須在這樣的刷新操作之后,使用以BUFFER_FLAG_CODEC_CONFIG標(biāo)記的緩沖區(qū)來重新提交編解碼器特定數(shù)據(jù),以保證編解碼器正常工作。
編碼器(或生成壓縮數(shù)據(jù)的編解碼器)將在的輸出緩沖區(qū)中的任何有效輸出緩沖區(qū)之前,創(chuàng)建并返回標(biāo)有codec-config flag標(biāo)志的編解碼器特定數(shù)據(jù)(通俗一點(diǎn)說,編碼器最先輸出的是有codec-config flag標(biāo)記的編解碼特定數(shù)據(jù))。包含編解碼器特定數(shù)據(jù)的緩沖區(qū)沒有有意義的時(shí)間戳。
數(shù)據(jù)處理
每個(gè)編解碼器維護(hù)一組輸入和輸出緩沖區(qū),在API調(diào)用的時(shí)候,可以通過緩沖區(qū)ID(buffer-ID)獲取一個(gè)輸入/輸出緩沖區(qū)。類似這樣:
private ByteBuffer[] mCachedInputBuffers;
private ByteBuffer[] mCachedOutputBuffers;
//通過buffer-ID獲取一個(gè)ByteBuffer
int bufferID = ...;
ByteBuffer buffer = mCachedInputBuffers[bufferID]
成功調(diào)用start()方法后,客戶端并沒有“擁有”輸入緩沖區(qū),也沒有“擁有”輸出緩沖區(qū)。在同步模式下,調(diào)用dequeueInput/OutputBuffer(…)從編解碼器獲取一個(gè)輸入或者輸出緩沖區(qū)。在異步模式下,你會(huì)通過MediaCodec.Callback.onInput/OutputBufferAvailable(…)這兩個(gè)回調(diào)自動(dòng)獲取可以用的緩沖區(qū)。
獲取一個(gè)輸入緩沖區(qū),向其中填充數(shù)據(jù)然后使用queueInputBuffer方法將輸入緩沖區(qū)提交給編解碼器。如果使用解密(decryption)的話,可以使用queueSecureInputBuffer方法提交。不要提交帶有相同時(shí)間戳的多個(gè)輸入緩沖區(qū)(除非是標(biāo)記為編解碼器特定數(shù)據(jù))。
反過來,編解碼器會(huì)返回一個(gè)只讀的輸出緩沖區(qū),在異步模式下,在onOutputBufferAvailable回調(diào)中返回,同步模式下通過調(diào)用dequeueOutputBuffer返回。在輸出緩沖區(qū)已經(jīng)被處理完畢以后,調(diào)用releaseOutputBuffer系列方法之一將輸出緩沖區(qū)返回給編解碼器。
盡管不需要立即將緩沖區(qū)重新提交/釋放給編解碼器,但是持有輸入和/或輸出緩沖區(qū)可能會(huì)使編解碼器停頓,并且此行為與設(shè)備無關(guān)。具體來說,編解碼器可能會(huì)推遲生成輸出緩沖區(qū),直到所有未完成的緩沖區(qū)都已釋放/重新提交為止 。因此,請(qǐng)嘗試盡可能少的持有可用緩沖區(qū)。(通俗一點(diǎn)說:通過bufferID獲取一個(gè)輸入緩沖區(qū)以后,填充完數(shù)據(jù)后應(yīng)該立即提交給編解碼器,通過bufferID獲取一個(gè)輸出緩沖區(qū)后,消費(fèi)了其中的數(shù)據(jù)以后也要盡快釋放給編解碼器,用完給別人用,不要老是占著,哈哈)。
根據(jù)API版本,你有三種方式可以處理數(shù)據(jù)。
| 處理模式 | API version <= 20Jelly Bean/KitKat | API version >= 21Lollipop and later |
|---|---|---|
| 同步模式,使用緩沖區(qū)數(shù)組 | 支持 | 過時(shí) |
| 同步模式,使用緩沖區(qū) | 不可用 | 支持 |
| 異步模式,使用緩沖區(qū) | 不可用 | 支持 |
異步處理,使用緩沖區(qū)
在Build.VERSION_CODES.LOLLIPOP以后,異步處理數(shù)據(jù)是首選的方式,通過在調(diào)用configure之前設(shè)置一個(gè)回調(diào)。異步模式稍微改變了編解碼器的狀態(tài)轉(zhuǎn)換流程,因?yàn)槟?br>
必選在flush()之后調(diào)用start()方法將編解碼器的狀態(tài)轉(zhuǎn)變到運(yùn)行子狀態(tài)來開始接受輸入緩沖區(qū)。類似的,首次調(diào)用start()方法也會(huì)將編解碼器的狀態(tài)直接轉(zhuǎn)變到運(yùn)行子狀態(tài)并開始通過回調(diào)傳遞可用的輸出緩沖區(qū)。

MediaCodec在異步模式下通常這樣使用:
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
codec.setCallback(new MediaCodec.Callback() {
@Override
void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
@Override
void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is equivalent to mOutputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
}
@Override
void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
mOutputFormat = format; // option B
}
@Override
void onError(…) {
…
}
});
//在調(diào)用config之前先設(shè)置回調(diào)
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
同步處理,使用緩沖區(qū)
從Build.VERSION_CODES.LOLLIPOP以后,即使在同步模式下,你也應(yīng)該使用getInput/OutputBuffer(int)和/或getInput/OutputImage(int)來獲取輸入和輸出緩沖區(qū)。
這允許框架進(jìn)行某些優(yōu)化,例如處理動(dòng)態(tài)內(nèi)容的時(shí)候。如果你調(diào)用getInput/OutputBuffers()這種優(yōu)化會(huì)被禁止。
注意: 不要同時(shí)使用緩沖區(qū)(buffers)和緩沖區(qū)數(shù)組(buffer arrays)的方法。特別地,只能在start()方法之后或者在編解碼器已經(jīng)出隊(duì)了一個(gè)有INFO_OUTPUT_FORMAT_CHANGED值的輸出緩沖區(qū)之后,直接調(diào)用getInput/OutputBuffers。(這段暫時(shí)不明白)
在同步模式下,MediaCodec的用法大概這樣:
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();
同步處理,使用緩沖區(qū)數(shù)組( Buffer Arrays) 已經(jīng)過時(shí)了
在Build.VERSION_CODES.KITKAT_WATCH版本及以前,輸入和輸出緩沖區(qū)的集合由ByteBuffer[]數(shù)組表示。成功調(diào)用start()后,使用getInput/OutputBuffers()來獲取緩沖區(qū)數(shù)組。使用緩沖區(qū)ID作為緩沖區(qū)數(shù)組中的下標(biāo),如下例所示。請(qǐng)注意,雖然數(shù)組大小提供了一個(gè)上限,但數(shù)組大小與系統(tǒng)使用的輸入和輸出緩沖區(qū)的數(shù)量之間沒有內(nèi)在的相關(guān)性。
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
//緩沖區(qū)ID
int inputBufferId = codec.dequeueInputBuffer(…);
if (inputBufferId >= 0) {
// fill inputBuffers[inputBufferId] with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
// outputBuffers[outputBufferId] is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers();
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
MediaFormat format = codec.getOutputFormat();
}
}
codec.stop();
codec.release();
流結(jié)束處理
當(dāng)?shù)竭_(dá)輸入數(shù)據(jù)的末尾時(shí),必須調(diào)用queueInputBuffer方法并指定BUFFER_FLAG_END_OF_STREAM標(biāo)記來通知編解碼器。你可以在最后一個(gè)有效的輸入緩沖區(qū)上執(zhí)行此操作,也可以額外提交一個(gè)設(shè)置了BUFFER_FLAG_END_OF_STREAM標(biāo)記的空的輸入緩沖區(qū)來實(shí)現(xiàn)。如果使用空的緩沖區(qū)的話,會(huì)忽略時(shí)間戳。
編解碼器將繼續(xù)返回輸出緩沖區(qū),直到它在dequeueOutputBuffer中的輸出緩沖區(qū)設(shè)置了BUFFER_FLAG_END_OF_STREAM標(biāo)記或者在onOutputBufferAvailable回調(diào)中設(shè)置了BUFFER_FLAG_END_OF_STREAM標(biāo)記。你可以在最后一個(gè)有效的輸出緩沖區(qū)上執(zhí)行此操作,也可以額外輸出一個(gè)設(shè)置了BUFFER_FLAG_END_OF_STREAM標(biāo)記的空的輸出緩沖區(qū)來實(shí)現(xiàn)。如果使用空的緩沖區(qū)的話,會(huì)忽略時(shí)間戳。
除非刷新、停止或重新啟動(dòng)了編解碼器,否則請(qǐng)?jiān)诎l(fā)出輸入流結(jié)束信號(hào)后不要提交其他輸入緩沖區(qū)。
使用輸出表面(Surface)
使用輸出Surface處理數(shù)據(jù)的方式和使用ByteBuffer模式幾乎一致;但是,沒有可訪問的輸出緩沖區(qū),getOutputBuffer/Image(int)會(huì)返回null,getOutputBuffers()返回的緩沖區(qū)數(shù)組中的值也都是null。
當(dāng)使用使用輸出表面的時(shí)候,你可以選擇每個(gè)輸出緩沖區(qū)是否渲染到Surface上。你有三種選擇:
不渲染:調(diào)用releaseOutputBuffer(bufferId, false)
使用默認(rèn)的時(shí)間戳渲染:調(diào)用releaseOutputBuffer(bufferId, true)
使用指定的時(shí)間戳渲染:調(diào)用releaseOutputBuffer(bufferId, timestamp)
自Build.VERSION_CODES.M以后,默認(rèn)的時(shí)間戳是緩沖區(qū)presentation timestamp顯示時(shí)間戳(轉(zhuǎn)化成微妙)。在Build.VERSION_CODES.M之前沒有定義。
另外,從Build.VERSION_CODES.M以后,可以調(diào)setOutputSurface方法來動(dòng)態(tài)改變輸出表面。
將輸出緩沖區(qū)渲染到Surface的時(shí)候,可以配置Surface丟棄過多的幀(沒有被Surface及時(shí)消耗掉的幀)?;蛘吲渲貌粊G棄過多的幀。如果配置不丟棄過多的幀,如果Surface不能快速的消耗輸出幀,最終會(huì)阻塞解碼器。在Build.VERSION_CODES.Q之前,除了視圖Surface(SurfaceView 或 TextureView)總是丟棄過多的幀意外,沒有定義Surface準(zhǔn)確的行為。在Build.VERSION_CODES.Q之后,默認(rèn)的行為是丟棄過多的幀。應(yīng)用可以為非視圖Surface(例如ImageReader 或 SurfaceTexture)取消這種默認(rèn)行為。實(shí)現(xiàn)方式是將target sdk 指定為Build.VERSION_CODES.Q并在配置格式的時(shí)候,將MediaFormat#KEY_ALLOW_FRAME_DROP鍵設(shè)置為0
渲染到表面的變換
如果編解碼器配置為Surface模式,則將自動(dòng)應(yīng)用任何裁剪矩形,旋轉(zhuǎn)和視頻縮放模式,但以下情況除外:
在Build.VERSION_CODES.M發(fā)布之前,軟件解碼器在渲染到Surface上時(shí)可能尚未應(yīng)用旋轉(zhuǎn)。不幸的是,沒有標(biāo)準(zhǔn)且簡(jiǎn)單的方法來識(shí)別軟件解碼器,或者是否通過嘗試旋轉(zhuǎn)來應(yīng)用旋轉(zhuǎn)。
也有一些警告:
請(qǐng)注意,在將輸出顯示到Surface時(shí)不考慮像素長(zhǎng)寬比。這意味著,如果您使用的是VIDEO_SCALING_MODE_SCALE_TO_FIT模式,則必須對(duì)輸出Surface進(jìn)行定位,以使其最終具正確的顯示寬高比。相反,您只能將VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式用于具有正方形像素(像素長(zhǎng)寬比或1:1)的內(nèi)容。
另請(qǐng)注意,自Build.VERSION_CODES.N正式版本以后,VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式可能無法和旋轉(zhuǎn)90度或270度的視頻正常配合使用。
設(shè)置視頻縮放模式時(shí),請(qǐng)注意,每次更改輸出緩沖區(qū)后必須將其重置。由于INFO_OUTPUT_BUFFERS_CHANGED事件已經(jīng)廢棄了,因此可以在每次輸出格式更改后執(zhí)行此操作。
使用輸入表面
使用輸入Surface時(shí),沒有可訪問的輸入緩沖區(qū),因?yàn)榫彌_區(qū)會(huì)自動(dòng)從輸入Surface傳遞到編解碼器。調(diào)用dequeueInputBuffer將拋出IllegalStateException,getInputBuffers()返回一個(gè)不能寫入的假的ByteBuffer[]數(shù)組。
調(diào)用signalEndOfInputStream()通知流結(jié)束。調(diào)用后,輸入表面將立即停止向編解碼器提交數(shù)據(jù)。
快進(jìn)和自適應(yīng)性播放支持
視頻解碼器在快進(jìn)(seek)和格式更改方面的行為不同,無論它們是否支持并配置為自適應(yīng)播放。你可以通過CodecCapabilities.isFeatureSupported(String)檢查一個(gè)解碼器是否支持adaptive playback。只有將解碼器配置為將數(shù)據(jù)解碼到Surface的時(shí)候,視頻解碼器的自適應(yīng)播放才會(huì)被激活。
流邊界和關(guān)鍵幀
在調(diào)用start()或flush()之后,在一個(gè)合適的流邊界開始輸入數(shù)據(jù)是非常重要的:第一幀必須是一個(gè)關(guān)鍵幀。一個(gè)關(guān)鍵幀可以完全獨(dú)立的被解碼(對(duì)大多數(shù)編解碼器來說這意味著一個(gè)I幀),并且在一個(gè)關(guān)鍵幀之后顯示的幀不會(huì)參考/引用(refer to)此關(guān)鍵幀之前的幀。
下表總結(jié)了適用于各種視頻格式的關(guān)鍵幀。
參考原文:Stream Boundary and Key Frames
對(duì)于不支持自適應(yīng)播放(包括不是將數(shù)據(jù)解碼到一個(gè)Surface上)的解碼器
為了解碼和先前提交的數(shù)據(jù)不相鄰的數(shù)據(jù)(例如在快進(jìn)之后)你必須刷新解碼器。由于所有輸出緩沖區(qū)都會(huì)在刷新時(shí)立即被撤銷,因此您可能想在調(diào)用flush之前先發(fā)出信號(hào),然后等待流結(jié)束。刷新后的輸入數(shù)據(jù)在合適的流邊界/關(guān)鍵幀處開始很重要。
注意:刷新后提交的數(shù)據(jù)格式不得更改;flush()不支持格式不連續(xù);因此如果格式發(fā)生了變化,一個(gè)完整的stop()- configure(…)-start()周期是必要的。
也請(qǐng)注意:如果在start()之后過早刷新編解碼器(通常在收到第一個(gè)輸出緩沖區(qū)或輸出格式更改之前),則需要將編解碼器特定數(shù)據(jù)重新提交給編解碼器。有關(guān)更多信息,請(qǐng)參見編解碼器特定數(shù)據(jù)部分。
對(duì)于支持并配置為自適應(yīng)播放的解碼器
為了開始解碼與先前提交的數(shù)據(jù)不相鄰的數(shù)據(jù)(例如在快進(jìn)之后),沒有必要刷新解碼器;但是,不連續(xù)之后的輸入數(shù)據(jù)必須從合適的流邊界/關(guān)鍵幀開始。
對(duì)于某些視頻格式-即H.264,H.265,VP8和VP9-也可以在流中間更改圖像大小或配置。為此,您必須將整個(gè)新的編解碼器特定的配置數(shù)據(jù)與關(guān)鍵幀一起打包到一個(gè)緩沖區(qū)(包括任何起始代碼)中,并將其作為常規(guī)輸入緩沖區(qū)提交。
在圖像大小發(fā)生更改之后,在返回具有新的尺寸的任何幀之前, 您將在dequeueOutputBuffer
或onOutputFormatChanged回調(diào)中收到INFO_OUTPUT_FORMAT_CHANGED的返回值。
注意:
就像編解碼器特定數(shù)據(jù)一樣,在更改圖像大小后立即調(diào)用flush()時(shí)要小心 。如果您尚未收到圖像尺寸更改的確認(rèn),則需要重新請(qǐng)求新的圖像尺寸。
錯(cuò)誤處理
createByCodecName和createDecoder/EncoderByType在失敗的時(shí)候會(huì)拋出IOException,你必須捕獲或者向上傳遞。當(dāng)在編解碼器不允許的狀態(tài)調(diào)用這些方法的時(shí)候,MediaCodec會(huì)拋出IllegalStateException;這通常是因?yàn)椴徽_的應(yīng)用API使用。和安全緩沖區(qū)相關(guān)的方法可能會(huì)拋出CryptoException,可以從CryptoException#getErrorCode獲取進(jìn)一步的錯(cuò)誤信息。
即使應(yīng)用正確使用了API,也可能因?yàn)槊襟w內(nèi)容損壞,硬件錯(cuò)誤,資源耗盡等,導(dǎo)致內(nèi)部編解碼器錯(cuò)誤,內(nèi)部編解碼器錯(cuò)誤會(huì)拋出CodecException。推薦的操作是在收到
CodecException的時(shí)候通過調(diào)用CodecException#isRecoverable和CodecException#isTransient確定是哪種類型的錯(cuò)誤。
- 可恢復(fù)的錯(cuò)誤:如果
isRecoverable()返回true,可以調(diào)用stop(),configure(…), 和 start()從失敗中恢復(fù)。 - 暫時(shí)錯(cuò)誤:如果
isTransient()返回true,表明是資源暫時(shí)不可用,相應(yīng)的方法會(huì)重試。 - 致命錯(cuò)誤:如果
isRecoverable()和isTransient()同時(shí)返回false,那么CodecException就是致命錯(cuò)誤,編解碼器必須被重置(reset)和釋放(released)。
isRecoverable()和isTransient()不會(huì)同時(shí)返回true。
參考鏈接: