最近公司要求提供一個支持 Android 硬件轉(zhuǎn)碼的底層庫波势,所以自己從頭去看了 MediaCodec 相關(guān)的知識翎朱,費了老大的勁終于完成了。目前的硬件轉(zhuǎn)碼使用 MediaCodec 進行解碼和編碼尺铣,然后使用 FFmpeg 進行文件封裝(為了支持文件分塊)拴曲。這篇文章主要介紹一些 MediaCodec 的基礎(chǔ)知識和使用方式,后面會寫如何利用 FFmpeg 封裝 MediaCodec 編碼后的數(shù)據(jù)以及 FFmpeg 分塊封裝的文章迄埃。
MediaCodec 可以用來獲得安卓底層的多媒體編碼疗韵,可以用來編碼和解碼,它是安卓 low-level 多媒體基礎(chǔ)框架的重要組成部分侄非。
MediaCodec 的作用是處理輸入的數(shù)據(jù)生成輸出數(shù)據(jù)蕉汪。首先生成一個輸入數(shù)據(jù)緩沖區(qū),將數(shù)據(jù)填入緩沖區(qū)提供給 codec逞怨,codec 會采用異步的方式處理這些輸入的數(shù)據(jù)者疤,然后將填滿輸出緩沖區(qū)提供給消費者,消費者消費完后將緩沖區(qū)返還給 codec叠赦。
接收的數(shù)據(jù)
MediaCodec 接受三種數(shù)據(jù)格式:壓縮數(shù)據(jù)驹马,原始音頻數(shù)據(jù)和原始視頻數(shù)據(jù)。
這三種數(shù)據(jù)都可以使用 ByteBuffer 作為載體傳輸給 MediaCodec 來處理除秀。但是當(dāng)使用原始視頻數(shù)據(jù)時糯累,最好采用 Surface 作為輸入源來替代 ByteBuffer,這樣效率更高册踩,因為 Surface 使用的更底層的視頻數(shù)據(jù)泳姐,不會映射或復(fù)制到 ByteBuffer 緩沖區(qū)。
壓縮數(shù)據(jù)
壓縮數(shù)據(jù)可以作為解碼器的輸入數(shù)據(jù)或者編碼器的輸出數(shù)據(jù)暂吉,需要指定數(shù)據(jù)格式胖秒,這樣 codec 才能知道如何處理這些壓縮數(shù)據(jù)缎患。
對于視頻數(shù)據(jù)而言,通常是一幀數(shù)據(jù)阎肝;音頻數(shù)據(jù)挤渔,一般是單個處理單元。
原始音頻數(shù)據(jù)
原始音頻數(shù)據(jù)即編碼器的輸入數(shù)據(jù)风题,解碼器的輸出數(shù)據(jù)判导。包含整個 PCM 音頻數(shù)據(jù)幀,這是通道順序中每個通道的一個樣本俯邓。每個采樣都是以本地字節(jié)順序的 16 位有符號整數(shù)骡楼。
原始視頻數(shù)據(jù)
原始視頻數(shù)據(jù)也是編碼器的輸入數(shù)據(jù),解碼器的輸出數(shù)據(jù)稽鞭。即yuv數(shù)據(jù),MediaCodec主要支持的格式為:
- native raw video format : COLOR_FormatSurface引镊,用來處理 Surface 模式的數(shù)據(jù)輸入輸出
- flexible YUV buffers : 例如 COLOR_FormatYUV420Flexible
- specific formats: 支持ByteBuffer模式朦蕴,有一些廠家會定制
使用流程
編解碼器處理輸入數(shù)據(jù)并產(chǎn)生輸出數(shù)據(jù),MediaCodec 使用輸入輸出緩存弟头,異步處理數(shù)據(jù)吩抓。
- 請求一個空的輸入 input buffer
- 填入數(shù)據(jù)、并將其交給 MediaCodec
- MediaCodec 處理數(shù)據(jù)后赴恨,將處理后的數(shù)據(jù)放在一個空的 output buffer
- 獲取填充數(shù)據(jù)了的 output buffer疹娶,得到其中的數(shù)據(jù),然后將其返還給 MediaCodec
首先了解下 MediaCodec 中的生命周期
MediaCodec 大體上分為三種狀態(tài):Stopped伦连、Executing 和 Released雨饺。
創(chuàng)建 MediaCodec
首先是如何創(chuàng)建 MediaCodec,在知道 MimeType 的情況下惑淳,可以通過 createDecoderByType, createEncoderByType, createByCodecName 方法來獲取實例额港。
如果不知道 MimeType,可以使用 MediaCodecList.findDecoderForFormat歧焦、 MediaCodecList.findEncoderForFormat 來獲取移斩。
創(chuàng)建成功之后,MediaCodec 進入 Uninitialized 狀態(tài)绢馍。
Configuration
在創(chuàng)建好 MediaCodec 之后向瓷,需要對其進行設(shè)置,這樣 MediaCodec 的狀態(tài)就可以由 uninitialized 變成 configured
public void configure(
@Nullable MediaFormat format,
@Nullable Surface surface, @Nullable MediaCrypto crypto,
@ConfigureFlag int flags) {
configure(format, surface, crypto, null, flags);
}
public void configure(
@Nullable MediaFormat format, @Nullable Surface surface,
@ConfigureFlag int flags, @Nullable MediaDescrambler descrambler) {
configure(format, surface, null,
descrambler != null ? descrambler.getBinder() : null, flags);
}
這里最重要的參數(shù)是 MediaFormat, 如果某些參數(shù)沒有設(shè)置的話舰涌,會導(dǎo)致 MediaCodec 拋出 IllegalStateException.
Video 所必須的 Format Setting
Encoder | Decoder | |
---|---|---|
KEY_MIME | ?? | ?? |
KEY_BIT_RATE | ?? | ? |
KEY_WIDTH | ?? | ?? |
KEY_HEIGHT | ?? | ?? |
KEY_COLOR_FORMAT | ?? | ? |
KYE_FRAME_RATE | ?? | ? |
KEY_I_FRAME_INTERVAL | ?? | ? |
Audio 所必須的 Format Setting
Encoder | Decoder | |
---|---|---|
KEY_MIME | ?? | ?? |
KEY_BIT_RATE | ?? | ? |
KEY_CHANNEL_COUNT | ?? | ?? |
KEY_SAMPLE_RATE | ?? | ?? |
輸入數(shù)據(jù)與獲取編解碼后的數(shù)據(jù)
從 5.0 開始猖任,首選方法是在調(diào)用 configure 方法之前通過設(shè)置回調(diào)來異步處理數(shù)據(jù)。所以這里就直接介紹異步模式下如何輸入需要編解碼的數(shù)據(jù)舵稠,以及如何獲取編解碼后的數(shù)據(jù)
異步模式
官方示例代碼:
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
// 設(shè)置回調(diào)方法
codec.setCallback(new MediaCodec.Callback() {
/**
* mediacodec 存在可用輸入緩沖
*/
@Override
void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
// 可通過 MediaExtractor 讀取 video 或 audio 數(shù)據(jù)超升,然后填充數(shù)據(jù)到緩沖區(qū)
…
codec.queueInputBuffer(inputBufferId, …);
}
/**
* 輸出緩沖填充完數(shù)據(jù)后
*/
@Override
void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
// 獲取輸出緩沖(其中包含編解碼后數(shù)據(jù))
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId);
// 處理編解碼后的數(shù)據(jù)
…
// 返還輸出緩沖給 codec
codec.releaseOutputBuffer(outputBufferId, …);
}
/**
* 輸出格式發(fā)生變化
*/
@Override
void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
mOutputFormat = format;
}
/**
* 發(fā)生錯誤
*/
@Override
void onError(…) {
…
}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat();
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
看一個幾個重要的方法
ByteBuffer getInputBuffer(int index)
該方法返回一個已清空入宦、可寫入的 input 緩沖區(qū),通過調(diào)用 ByteBuffer.put(data) 方法將 data 中的數(shù)據(jù)放到緩沖區(qū)室琢,然后調(diào)用
/**
* @param index - 緩沖區(qū)索引
* @param offset - 緩沖區(qū)提交數(shù)據(jù)的起始位置
* @param size - 提交的數(shù)據(jù)長度
* @param presentationTimeUs - 時間戳
* @param flags - BUFFER_FLAG_CODEC_CONFIG:配置信息乾闰;
* BUFFER_FLAG_END_OF_STREAM:結(jié)束標(biāo)志;
* BUFFER_FLAG_KEY_FRAME:關(guān)鍵幀
*/
void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags)
就可以將緩沖區(qū)返回給 codec盈滴。
ByteBuffer getOutputBuffer(int index)
該方法返回一個 output 緩沖區(qū)涯肩,包含解碼或編碼后的數(shù)據(jù)。
void releaseOutputBuffer(int index, boolean render)
void releaseOutputBuffer(int index, long renderTimeStampNs)
這兩個方法都會釋放 index 所指向的緩沖區(qū)巢钓。
處理完需要編/解碼的數(shù)據(jù)之后病苗,調(diào)用 stop & release 方法釋放 MediaCodec。