PS:有些想法可以先開始,慢慢完善才是好的選擇。
MediaCodec 是 Android 中的編解碼器組件,用來訪問底層提供的編解碼器,通常與 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface 和 AudioTrack 一起使用,MediaCodec 幾乎是 Android 播放器硬解碼的標配,但是具體使用的是軟編解碼器還是硬解編解碼器,還是和 MediaCodec 的配置相關,下面將從以下幾個方面介紹 MediaCodec,主要內容如下
- MediaCodec 處理的類型
- MediaCodec 編解碼的流程
- MediaCodec 生命週期
- MediaCodec 的創建
- MediaCodec 的初始化
- MediaCodec 的數據處理方式
- 自適應播放支持
- MediaCodec 的異常處理
MediaCodec 處理的類型#
MediaCodec 支持處理三種數據類型,分別是壓縮數據(compressed data)、原始音頻數據(raw audio data)、原始視頻數據(raw video data),可以使用 ByteBuffer 處理這三種數據,也就是後文中提到的緩衝區,對於原始視頻數據,可以使用 Surface 來提高編解碼器性能,但是不能訪問原始視頻數據,但是可以通過 ImageReader 訪問原始視頻幀,通過 Image 進而獲取到與之對應的 YUV 數據等其他信息。
壓縮緩衝區:用於解碼器的輸入緩衝區和用於編碼器的輸出緩衝區會包含 MediaFormat 的 KEY_MIME 對應類型的壓縮數據,對於視頻類型,通常是單個壓縮視頻幀,對於音頻數據,這通常是一個編碼的音頻段,通常包含幾毫秒的音頻,因格式類型而定。
原始音頻緩衝區:原始音頻緩衝區包含 PCM 音頻數據的整個幀,這是每一個通道按照通道順序的一個樣本,每個 PCM 音頻樣本都是 16 位帶符號整數或浮點數(以本機字節順序),如果要使用浮點 PCM 編碼的原始音頻緩衝區,需要如下配置:
mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_FLOAT);
檢查 MediaFormat 中的浮點 PCM 的方法如下:
static boolean isPcmFloat(MediaFormat format) {
return format.getInteger(MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT)
== AudioFormat.ENCODING_PCM_FLOAT;
}
提取包含 16 位帶符號整數音頻數據的緩衝區的一個通道,可以使用以下代碼:
// 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;
}
原始視頻緩衝區:在 ByteBuffer 模式下,視頻緩衝區根據其 MediaFormat 的 KEY_COLOR_FORMAT 設置的值進行佈局,可以從通過 MediaCodecInfo 相關方法獲取設備受支持的顏色格式,視頻編解碼器可能支持三種顏色格式:
-
native raw video format:原始原始視頻格式,由 CodecCapabilities 的 COLOR_FormatSurface 常量標記,可以與輸入或輸出 Surface 一起使用。
-
flexible YUV buffers:靈活的 YUV 緩衝區,如 CodecCapabilities 的 COLOR_FormatYUV420Flexible 常量對應的顏色格式,可以通過 getInput、OutputImage 等於與輸入、輸出 Surface 以及 ByteBuffer 模式一起使用。
-
other specific formats:其他特定格式:通常僅在 ByteBuffer 模式下支持這些格式,某些顏色格式是特定於供應商的,其他在均在 CodecCapabilities 中定義。
自 Android 5.1 開始,所有視頻編解碼器均支持靈活的 YUV 4:2:0 緩衝區。其中 MediaFormat#KEY_WIDTH 和 MediaFormat#KEY_HEIGHT 鍵指定視頻幀的大小,在大多數情況下,視頻僅佔據視頻幀的一部分,具體表示如下:
需要使用以下鍵從輸出格式獲取原始輸出圖像的裁剪矩形,如果輸出格式中不存在這些鍵,則視頻將佔據整個視頻幀,在使用任何 MediaFormat#KEY_ROTATION 之前,也就是在設置旋轉之前,可以使用下面的方式計算視頻幀的大小,參考如下:
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");
}
MediaCodec 編解碼的流程#
MediaCodec 首先獲取一個空的輸入緩衝區,填充要編碼或解碼的數據,再將填充數據的輸入緩衝區送到 MediaCodec 進行處理,處理完數據後會釋放這個填充數據的輸入緩衝區,最後獲取已經編碼或解碼的輸出緩衝區,使用完畢後釋放輸出緩衝區,其編解碼的流程示意圖如下:
各個階段對應的 API 如下:
// 獲取可用的輸入緩衝區的索引
public int dequeueInputBuffer (long timeoutUs)
// 獲取輸入緩衝區
public ByteBuffer getInputBuffer(int index)
// 將填滿數據的inputBuffer提交到編碼隊列
public final void queueInputBuffer(int index,int offset, int size, long presentationTimeUs, int flags)
// 獲取已成功編解碼的輸出緩衝區的索引
public final int dequeueOutputBuffer(BufferInfo info, long timeoutUs)
// 獲取輸出緩衝區
public ByteBuffer getOutputBuffer(int index)
// 釋放輸出緩衝區
public final void releaseOutputBuffer(int index, boolean render)
MediaCodec 生命週期#
MediaCodec 有三種狀態,分別是執行 (Executing)、停止 (Stopped) 和釋放 (Released),其中執行和停止分別有三個子狀態,執行的三個字狀態分別是 Flushed、Running 和 Stream-of-Stream,停止的三個子狀態分別是 Uninitialized、Configured 和 Error,MediaCodec 生命週期示意圖如下:
同步模式下的生命週期 | 異步模式模式下的生命週期 |
---|---|
) |
如上圖所示,三種狀態的切換都是由 start、stop、reset、release 等觸發,根據 MediaCodec 處理數據方式的不同,其生命週期會略有不同,如在異步模式下 start 之後立即進入 Running 子狀態,如果已經處於 Flushed 子狀態,則需再次調用 start 進入 Running 子狀態,下面是各個子狀態切換對應的關鍵 API 如下:
- 停止狀態 (Stopped)
// 創建MediaCodec進入Uninitialized子狀態
public static MediaCodec createByCodecName (String name)
public static MediaCodec createEncoderByType (String type)
public static MediaCodec createDecoderByType (String type)
// 配置MediaCodec進入Configured子狀態,crypto和descrambler會在後文中進行說明
public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
public void configure(MediaFormat format, @Nullable Surface surface,int flags, MediaDescrambler descrambler)
// Error
// 編解碼過程中遇到錯誤進入Error子狀態
- 執行狀態 (Executing)
// start之後立即進入Flushed子狀態
public final void start()
// 第一个输入缓冲区出队的时候进入Running子状态
public int dequeueInputBuffer (long timeoutUs)
// 输入缓冲区与流结束标记排队时,编解码器将转换为End-of-Stream子状态
// 此时MediaCodec将不接受其他输入缓冲区,但会生成输出缓冲区
public void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)
- 釋放狀態 (Released)
// 編解碼完成結束後釋放MediaCodec進入釋放狀態(Released)
public void release ()
MediaCodec 的創建#
前面已經提到過當創建 MediaCodec 的時候進入 Uninitialized 子狀態,其創建方式如下:
// 創建MediaCodec
public static MediaCodec createByCodecName (String name)
public static MediaCodec createEncoderByType (String type)
public static MediaCodec createDecoderByType (String type)
使用 createByCodecName 時可以借助 MediaCodecList 獲取支持的編解碼器,下面是獲取指定 MIME 類型的編碼器:
/**
* 查詢指定MIME類型的編碼器
*/
fun selectCodec(mimeType: String): MediaCodecInfo? {
val mediaCodecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val codeInfos = mediaCodecList.codecInfos
for (codeInfo in codeInfos) {
if (!codeInfo.isEncoder) continue
val types = codeInfo.supportedTypes
for (type in types) {
if (type.equals(mimeType, true)) {
return codeInfo
}
}
}
return null
}
當然 MediaCodecList 也提供了相應的獲取編解碼器的方法,如下:
// 獲取指定格式的編碼器
public String findEncoderForFormat (MediaFormat format)
// 獲取指定格式的解碼器
public String findDecoderForFormat (MediaFormat format)
對於上述方法中的參數 MediaFormat 格式中不能包含任何幀率的設置,如果已經設置了幀率需要將其清除再使用。
上面提到了 MediaCodecList,這裡簡單說一下,使用 MediaCodecList 可以方便的列出當前設備支持的所有的編解碼器,創建 MediaCodec 的時候要選擇當前格式支持的編解碼器,也就是選擇的編解碼器需支持對應的 MediaFormat,每個編解碼器都被包裝成一個 MediaCodecInfo 對象,據此可以查看該編碼器的一些特性,比如是否支持硬件加速、是軟解還是硬解編解碼器等,常用的簡單如下:
// 是否軟解
public boolean isSoftwareOnly ()
// 是Android平台提供(false)還是廠商提供(true)的編解碼器
public boolean isVendor ()
// 是否支持硬件加速
public boolean isHardwareAccelerated ()
// 是編碼器還是解碼器
public boolean isEncoder ()
// 獲取當前編解碼器支持的合適
public String[] getSupportedTypes ()
// ...
軟解和硬解應該是音視頻開發中必須掌握的,當使用 MediaCodec 的時候不能說全是硬解,到底使用硬解還是軟解還是要看使用的編碼器,一般廠商提供的編解碼器都是硬解編解碼器,比如高通 (qcom) 等,一般如系統提供的則是軟解編解碼器,如帶有 android 字樣的編解碼器,下面是本人(MI 10 Pro)自己手機的部分編解碼器:
// 硬解編解碼器
OMX.qcom.video.encoder.heic
OMX.qcom.video.decoder.avc
OMX.qcom.video.decoder.avc.secure
OMX.qcom.video.decoder.mpeg2
OMX.google.gsm.decoder
OMX.qti.video.decoder.h263sw
c2.qti.avc.decoder
...
// 軟解編解碼器
c2.android.aac.decoder
c2.android.aac.decoder
c2.android.aac.encoder
c2.android.aac.encoder
c2.android.amrnb.decoder
c2.android.amrnb.decoder
...
MediaCodec 初始化#
創建 MediaCodec 之後進入 Uninitialized 子狀態,此時需要對其進行一些設置如指定 MediaFormat、如果使用的是異步處理數據的方式,在 configure 之前要設置 MediaCodec.Callback,關鍵 API 如下:
// 1. MediaFormat
// 創建MediaFormat
public static final MediaFormat createVideoFormat(String mime,int width,int height)
// 開啟或關閉功能,具體參見MediaCodeInfo.CodecCapabilities
public void setFeatureEnabled(@NonNull String feature, boolean enabled)
// 參數設置
public final void setInteger(String name, int value)
// 2. setCallback
// 如果使用的是異步處理數據的方式,在configure 之前要設置 MediaCodec.Callback
public void setCallback (MediaCodec.Callback cb)
public void setCallback (MediaCodec.Callback cb, Handler handler)
// 3. 配置
public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags)
public void configure(MediaFormat format, @Nullable Surface surface,int flags, MediaDescrambler descrambler)
上面 configure 配置中涉及到幾個參數,其中 surface 表示解碼器要渲染的 Surface,flags 則是指定當前編解碼器是作為編碼器還是解碼器來使用的,crypto 和 descrambler 都和解密有關,比如某些 vip 視頻就需要特定的密鑰來配合解碼,只有用戶登錄校驗後才會對視頻內容進行解密,要不然某些需要付費才能觀看的視頻下載之後就能隨意傳播了,更多細節可以查看音視頻中的數字版權技術。
此外某些特定格式比如 AAC 音頻以及 MPEG4、H.264、H.265 視頻格式,這些格式的含一些用於 MediaCodec 的初始化特定的數據,當解碼處理這些壓縮格式時,必須在 start 之後且在任何幀數據處理之前將這些特定數據提交給 MediaCodec,即在對 queueInputBuffer 的調用中使用標誌 BUFFER_FLAG_CODEC_CONFIG 標記此類數據,這些特定的數據也可以通過 MediaFormat 設置 ByteBuffer 的方式進行配置,如下:
// csd-0、csd-1、csd-2同理
val bytes = byteArrayOf(0x00.toByte(), 0x01.toByte())
mediaFormat.setByteBuffer("csd-0", ByteBuffer.wrap(bytes))
其中 csd-0、csd-1 這些鍵可以從 MediaExtractor#getTrackFormat 獲取的 MediaFormat 中獲取,這些特定的數據會在 start 時自動提交給 MediaCodec,無需直接提交此數據,如果在輸出緩衝區或格式更改之前調用了 flush,則會丟失提交的特定數據,就需要在 queueInputBuffer 的調用中使用標誌 BUFFER_FLAG_CODEC_CONFIG 標記這類數據。
Android 使用以下特定於編解碼器的數據緩衝區,為了正確配置 MediaMuxer 軌道,還需要將它們設置為軌道格式,每個參數集和標有(*)的編解碼器專用數據部分必須以 “\ x00 \ x00 \ x00 \ x01” 的起始代碼開頭,參考如下:
編碼器在收到這些信息後將會同樣輸出帶有BUFFER_FLAG_CODEC_CONFIG
標記的 outputbuffer,此時這些數據就是特定數據,不是媒體數據。
MediaCodec 數據處理方式#
每個創建已經創建的編解碼器都維護一組輸入緩衝區,有兩種處理數據的方式,同步和異步方式,根據 API 版本不同有所區別,在 API 21 也就是從 Android5.0 開始,推薦使用 ButeBuffer 的方式進行數據的處理,在此之前只能使用 ButeBuffer 數組的方式進行數據的處理,如下:
MediaCodec,也就是編解碼器的數據處理,主要是獲取輸入、輸出緩衝區、提交數據給編解碼器、釋放輸出緩衝區這幾個過程,同步方式和異步方式的不同點在於輸入緩衝區和輸出緩衝區的其關鍵 API 如下:
// 獲取輸入緩衝區(同步)
public int dequeueInputBuffer (long timeoutUs)
public ByteBuffer getInputBuffer (int index)
// 獲取輸出緩衝區(同步)
public int dequeueOutputBuffer (MediaCodec.BufferInfo info, long timeoutUs)
public ByteBuffer getOutputBuffer (int index)
// 輸入、輸出緩衝區索引從MediaCodec.Callback的回調中獲取,在獲取對應的輸入、輸出緩衝區(異步)
public void setCallback (MediaCodec.Callback cb)
public void setCallback (MediaCodec.Callback cb, Handler handler)
// 提交數據
public void queueInputBuffer (int index, int offset, int size, long presentationTimeUs, int flags)
public void queueSecureInputBuffer (int index, int offset, MediaCodec.CryptoInfo info, long presentationTimeUs, int flags)
// 釋放輸出緩衝區
public void releaseOutputBuffer (int index, boolean render)
public void releaseOutputBuffer (int index, long renderTimestampNs)
下面主要介紹適用於 Android 5.0 之後的 ButeBuffer 的方式,
Android 5.0 開始 Deprecated 了 ButeBuffer 數組的方式,官網上提到 ButeBuffer 相較 ButeBuffer 數組的方式做了一定優化,故在設備滿足條件的情況下盡量使用 ButeBuffer 對應的 API,且推薦使用異步模式處理數據,同步和異步處理方式代碼參考如下:
- 同步處理模式
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(…);
// 使用有效數據填充輸入緩衝區
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat與outputFormat是相同的
// 輸出緩衝區已準備後被處理或渲染了
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 輸出格式改變,後續採用新格式,此時使用getOutputFormat()獲取新格式
// 如果使用getOutputFormat(outputBufferId)獲取特定緩衝區的格式,則無需監聽格式變化
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
具體可以參考上一篇文章中的案例:Camera2、MediaCodec 錄製 mp4。
- 異步處理模式
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(…) {
…
}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
當要處理的數據結束時 (End-of-stream),需要標記流的結束,可以在最後一個有效的輸入緩衝區上使用 queueInputBuffer 提交數據的時候指定 flags 為 BUFFER_FLAG_END_OF_STREAM 標記其結束,也可以在最後一個有效輸入緩衝區之後提交一個空的設置了流結束標誌的輸入緩衝區來標記其結束,此時不能夠再提交輸入緩衝區,除非編解碼器被 flush、stop、restart,輸出緩衝區繼續返回直到最終通過在 dequeueOutputBuffer 或通過 Callback#onOutputBufferAvailable 返回的 BufferInfo 中指定相同的流結束標誌,最終通知輸出流結束為止。
如果使用了一個輸入 Surface 作為編解碼器的輸入,此時沒有可訪問的輸入緩衝區,輸入緩衝區會自動從這個 Surface 提交給編解碼器,相當於省略了輸入的這個過程,這個輸入 Surface 可由 createInputSurface 方法創建,此時調用 signalEndOfInputStream 將發送流結束的信號,調用後,輸入表面將立即停止向編解碼器提交數據,關鍵 API 如下:
// 創建輸入Surface,需在configure之後、start之前調用
public Surface createInputSurface ()
// 設置輸入Surface
public void setInputSurface (Surface surface)
// 發送流結束的信號
public void signalEndOfInputStream ()
同理如果使用了輸出 Surface,則與之相關的輸出緩衝區的相關功能將會被代替,可以通過 setOutputSurface 設置一個 Surface 作為編解碼器的輸出,可以選擇是否在輸出 Surface 上渲染每一個輸出緩衝區,關鍵 API 如下:
// 設置輸出Surface
public void setOutputSurface (Surface surface)
// false表示不渲染這個buffer,true表示使用默認的時間戳渲染這個buffer
public void releaseOutputBuffer (int index, boolean render)
// 使用指定的時間戳渲染這個buffer
public void releaseOutputBuffer (int index, long renderTimestampNs)
自適應播放支持#
當 MediaCodec 作為視頻解碼器的時候,可以通過如下方式檢查解碼器是否支持自適應播放,也就是此時解碼器是否支持無縫的分辨率修改:
// 是否支持某項功能,CodecCapabilities#FEATURE_AdaptivePlayback對應對應自適應播放支持
public boolean isFeatureSupported (String name)
此時只有在將解碼器配置在 Surface 上解碼時,自適應播放的功能才會被激活,視頻解碼時當 strat 或 flush 調用後,只有關鍵幀(key-frame)才能完全獨立解碼,也就是通常說的 I 幀,其他幀都是據此來解碼的,不同格式對應關鍵幀如下:
不同的解碼器對自適應播放的支持能力不同,其 seek 操作後處理也是不同,這部分內容暫時留到後續具體實踐後再做整理。
MediaCodec 的異常處理#
關於 MediaCodec 使用過程中的異常處理,這裡提一下 CodecException 異常,一般是由編解碼器內部異常導致的,比如媒體內容損壞、硬件故障、資源耗盡等,可以通過如下方法判斷以做進一步的處理:
// true表示可以通過stop、configure、start來恢復
public boolean isRecoverable ()
// true表示暫時性問題,編碼或解碼操作會在後續重試進行
public boolean isTransient ()
如果 isRecoverable 和 isTransient 都是返回 false,則需要通過 reset 或 release 操作釋放資源後重新工作,兩者不可能同時返回 true。