Android 官方的 MediaCodec API
MediaCodec 是Android 4.1(api 16)版本引入的編解碼接口氮凝,Developer 官網(wǎng)上描述的已經(jīng)很清楚了窥淆。可以配合中文翻譯一起看碎连。理解更深刻。
MediaCodec 基本介紹
MediaCodec類可用于訪問Android底層的多媒體編解碼器,例如综苔,編碼器/解碼器組件。它是Android底層多媒體支持基礎(chǔ)架構(gòu)的一部分(通常與MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, 以及AudioTrack一起使用)位岔。
Android 底層多媒體模塊采用的是 OpenMax 框架如筛,任何 Android 底層編解碼模塊的實(shí)現(xiàn),都必須遵循 OpenMax 標(biāo)準(zhǔn)抒抬。Google 官方默認(rèn)提供了一系列的軟件編解碼器:包括:OMX.google.h264.encoder杨刨,OMX.google.h264.encoder, OMX.google.aac.encoder瞧剖, OMX.google.aac.decoder 等等拭嫁,而硬件編解碼功能,則需要由芯片廠商依照 OpenMax 框架標(biāo)準(zhǔn)來完成抓于,所以做粤,一般采用不同芯片型號(hào)的手機(jī),硬件編解碼的實(shí)現(xiàn)和性能是不同的
Android 應(yīng)用層統(tǒng)一由 MediaCodec API 來提供各種音視頻編解碼功能捉撮,由參數(shù)配置來決定采用何種編解碼算法怕品、是否采用硬件編解碼加速等
MediaCodec的工作流程:
從上圖可以看出 MediaCodec 架構(gòu)上采用了2個(gè)緩沖區(qū)隊(duì)列,異步處理數(shù)據(jù)巾遭,并且使用了一組輸入輸出緩存肉康。
你請(qǐng)求或接收到一個(gè)空的輸入緩存(input buffer),向其中填充滿數(shù)據(jù)并將它傳遞給編解碼器處理灼舍。編解碼器處理完這些數(shù)據(jù)并將處理結(jié)果輸出至一個(gè)空的輸出緩存(output buffer)中吼和。最終,你請(qǐng)求或接收到一個(gè)填充了結(jié)果數(shù)據(jù)的輸出緩存(output buffer)骑素,使用完其中的數(shù)據(jù)炫乓,并將其釋放給編解碼器再次使用。
具體工作如下:
- Client 從 input 緩沖區(qū)隊(duì)列申請(qǐng) empty buffer [dequeueInputBuffer]
- Client 把需要編解碼的數(shù)據(jù)拷貝到 empty buffer献丑,然后放入 input 緩沖區(qū)隊(duì)列 [queueInputBuffer]
- MediaCodec 模塊從 input 緩沖區(qū)隊(duì)列取一幀數(shù)據(jù)進(jìn)行編解碼處理
- 編解碼處理結(jié)束后末捣,MediaCodec 將原始數(shù)據(jù) buffer 置為 empty 后放回 input 緩沖區(qū)隊(duì)列,將編解碼后的數(shù)據(jù)放入到 output 緩沖區(qū)隊(duì)列
- Client 從 output 緩沖區(qū)隊(duì)列申請(qǐng)編解碼后的 buffer [dequeueOutputBuffer]
- Client 對(duì)編解碼后的 buffer 進(jìn)行渲染/播放
- 渲染/播放完成后创橄,Client 再將該 buffer 放回 output 緩沖區(qū)隊(duì)列 [releaseOutputBuffer]
MediaCodec的基本調(diào)用流程是:
createEncoderByType/createDecoderByType
configure
start
while(true) {
dequeueInputBuffer //從輸入流隊(duì)列中取數(shù)據(jù)進(jìn)行編碼操作
getInputBuffers //獲取需要編碼數(shù)據(jù)的輸入流隊(duì)列箩做,返回的是一個(gè)ByteBuffer數(shù)組
queueInputBuffer //輸入流入隊(duì)列
dequeueOutputBuffer //從輸出隊(duì)列中取出編碼操作之后的數(shù)據(jù)
getOutPutBuffers // 獲取編解碼之后的數(shù)據(jù)輸出流隊(duì)列,返回的是一個(gè)ByteBuffer數(shù)組
releaseOutputBuffer //處理完成妥畏,釋放ByteBuffer數(shù)據(jù)
}
stop
release
1.初始化MediaCodec邦邦,方法有兩種安吁,分別是通過名稱和類型來創(chuàng)建,對(duì)應(yīng)的方法為:
MediaCodec createByCodecName (String name);
MediaCodec createDecoderByType (String type);
- 選擇第一種創(chuàng)建方式
根據(jù) mineType 以及是否為編碼器圃酵,選擇出一個(gè) MediaCodecInfo柳畔,然后使用第一種方式初始化MediaCodec;
private MediaCodecInfo selectSupportCodec(String mimeType) {
int numCodecs = MediaCodecList.getCodecCount();
for (int i = 0; i < numCodecs; i++) {
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
// 判斷是否為編碼器郭赐,否則直接進(jìn)入下一次循環(huán)
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;
}
MediaCodecInfo codecInfo = selectSupportCodec(config.mMime);
if (codecInfo == null) return;
mMediaCodec = MediaCodec.createByCodecName(codecInfo.getName());
- 第二種方式比較簡(jiǎn)單
mMediaCodec = MediaCodec.createDecoderByType (MIME_TYPE);
2.配置編碼器,設(shè)置各種編碼器參數(shù)(MediaFormat)捌锭,這個(gè)類包含了比特率俘陷、幀率、關(guān)鍵幀間隔時(shí)間等观谦。然后再調(diào)用 mMediaCodec .configure拉盾,對(duì)于 API 19 以上的系統(tǒng),我們可以選擇 Surface 輸入:mMediaCodec .createInputSurface豁状,
format= MediaFormat.createVideoFormat(MIME_TYPE, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, framerate);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); //關(guān)鍵幀間隔時(shí)間 單位s
mMediaCodec .configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mMediaCodec.createInputSurface();
3.打開編碼器捉偏,獲取輸入輸出緩沖區(qū)
mMediaCodec .start();
mInputBuffers = mMediaCodec .getInputBuffers();
mOutputBuffers = mMediaCodec .getOutputBuffers();
獲取輸入輸出緩沖區(qū)在api19 上是以上方式獲取,api21以后 可以使用直接獲取ByteBuffer
ByteBuffer intputBuffer = mMediaCodec.getOutputBuffer(inputBufferIndex);
ByteBuffer outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
4.輸入數(shù)據(jù)泻红,有2種方式夭禽,一種是普通輸入,一種是Surface 輸入
普通輸入又可區(qū)分為兩種情況谊路,一種是配合MediaExtractor 讹躯,一種是取原數(shù)據(jù);
- 獲取可使用的緩沖區(qū)索引
int outputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMES_OUT);
返回一個(gè)填充了有效數(shù)據(jù)的input buffer的索引缠劝,如果沒有可用的buffer則返回-1潮梯,參數(shù)為超時(shí)時(shí)間(TIMES_OUT),單位是微秒惨恭,當(dāng)timeoutUs==0時(shí)秉馏,該方法立即返回;當(dāng)timeoutUs<0時(shí)脱羡,無限期地等待一個(gè)可用的input buffer沃饶,當(dāng)timeoutUs>0時(shí),
等待時(shí)間為傳入的微秒值轻黑。
- 普通輸入之獲取原數(shù)據(jù)方式
ByteBuffer inputBuffer = mInputBuffers[inputbufferindex];
inputBuffer.clear();//清除原來的內(nèi)容以接收新的內(nèi)容
inputBuffer.put(bytes, 0, len);//len是傳進(jìn)來的有效數(shù)據(jù)長(zhǎng)度
mMediaCodec .queueInputBuffer(inputbufferindex, 0, len, timestamp, 0);
上面輸入緩存的index,通過getInputBuffers()得到的是輸入緩存數(shù)組琴昆,通過index和輸入緩存數(shù)組可以得到當(dāng)前請(qǐng)求的輸入緩存氓鄙,在使用之前要clear一下,避免之前的緩存數(shù)據(jù)影響當(dāng)前數(shù)據(jù)业舍,接著就是把數(shù)據(jù)添加到輸入緩存中抖拦,并調(diào)用queueInputBuffer(...)把緩存數(shù)據(jù)入隊(duì)升酣;
- 普通輸入之配合MediaExtractor 解碼其他的音視頻數(shù)據(jù)
ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
int chunkSize = SDecoder.extractor.readSampleData(inputBuf, 0);
if (chunkSize < 0) {
SDecoder.decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
} else {
SDecoder.decoder.queueInputBuffer(inputBufIndex, 0, chunkSize, SDecoder.extractor.getSampleTime(), 0);
SDecoder.extractor.advance();
}
- 使用Surface輸入
Surface輸入是Android 4.3(api 18)引入。但用在某些 API 18 的機(jī)型上會(huì)導(dǎo)致編碼器輸出數(shù)據(jù)量特別小态罪,畫面是黑屏噩茄,所以 Surface 輸入模式從 API 19 啟用比較好。
//Requests a Surface to use as the input to an encoder, in place of input buffers. This may only be
//called after configure(MediaFormat, Surface, MediaCrypto, int) and before start().
//調(diào)用此方法复颈,官方有這么一段話绩聘,意思是必須在configure之后 start()之前調(diào)用。
mInputSurface = mMediaCodec.createInputSurface();
5.輸出數(shù)據(jù)
通常編碼傳輸時(shí)每個(gè)關(guān)鍵幀頭部都需要帶上編碼配置數(shù)據(jù)(PPS耗啦,SPS)凿菩,但 MediaCodec 會(huì)在首次輸出時(shí)專門輸出編碼配置數(shù)據(jù),后面的關(guān)鍵幀里是不攜帶這些數(shù)據(jù)的帜讲,所以需要我們手動(dòng)做一個(gè)拼接衅谷;
- 獲取可使用的緩沖區(qū)
獲取輸出緩存和獲取輸入緩存類似,首先通過dequeueOutputBuffer(BufferInfo info, long timeoutUs)來請(qǐng)求一個(gè)輸出緩存似将,這里需要傳入一個(gè)BufferInfo對(duì)象获黔,用于存儲(chǔ)ByteBuffer的信息,TIMES_OUT為超時(shí)時(shí)間在验。TIMES_OUT傳的是 0玷氏,表示不會(huì)等待,由于這里并沒有一個(gè)單獨(dú)的線程不停調(diào)用译红,所以這樣沒什么問題预茄,反倒可以防止阻塞,但如果我們單獨(dú)起了一個(gè)線程專門取輸出數(shù)據(jù)侦厚,那這就會(huì)導(dǎo)致 CPU 資源的浪費(fèi)了耻陕,可以加上一個(gè)合適的值,例如 3~10ms刨沦;
BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo,TIMES_OUT);
- 獲取數(shù)據(jù)
ByteBuffer outputBuffer = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
outputBuffer = outputBuffers[outputBufferIndex];
} else {
outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
MediaFormat format = mMediaCodec.getOutputFormat();
format.setByteBuffer("csd-0",outputBuffer);
mBufferInfo.size = 0;
}
// 如果API<=19诗宣,需要根據(jù)BufferInfo的offset偏移量調(diào)整ByteBuffer的位置
// 并且限定將要讀取緩存區(qū)數(shù)據(jù)的長(zhǎng)度,否則輸出數(shù)據(jù)會(huì)混亂
if (mBufferInfo.size != 0) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
}
// mMuxer.writeSampleData(mTrackIndex, encodedData, bufferInfo);
}
- 釋放緩沖區(qū)
mMediaCodec.releaseOutputBuffer(outputBufferIndex,false);
6.使用完MediaCodec后釋放資源
要告知編碼器我們要結(jié)束編碼想诅,Surface 輸入的話調(diào)用 mMediaCodec .signalEndOfInputStream召庞,普通輸入則可以為在 queueInputBuffer 時(shí)指定 MediaCodec.BUFFER_FLAG_END_OF_STREAM 這個(gè) flag;告知編碼器后我們就可以等到編碼器輸出的 buffer 帶著 MediaCodec.BUFFER_FLAG_END_OF_STREAM 這個(gè) flag 了来破,等到之后我們調(diào)用 mMediaCodec .release 銷毀編碼器
if (mMediaCodec != null) {
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
}
MediaCodec 流控
流控就是流量控制篮灼。為什么要控制,就是為了在一定的限制條件下徘禁,收益最大化诅诱!
涉及到了 TCP 和視頻編碼:
對(duì) TCP 來說就是控制單位時(shí)間內(nèi)發(fā)送數(shù)據(jù)包的數(shù)據(jù)量,對(duì)編碼來說就是控制單位時(shí)間內(nèi)輸出數(shù)據(jù)的數(shù)據(jù)量送朱。
TCP 的限制條件是網(wǎng)絡(luò)帶寬娘荡,流控就是在避免造成或者加劇網(wǎng)絡(luò)擁塞的前提下干旁,盡可能利用網(wǎng)絡(luò)帶寬。帶寬夠炮沐、網(wǎng)絡(luò)好争群,我們就加快速度發(fā)送數(shù)據(jù)包,出現(xiàn)了延遲增大大年、丟包之后换薄,就放慢發(fā)包的速度(因?yàn)槔^續(xù)高速發(fā)包,可能會(huì)加劇網(wǎng)絡(luò)擁塞鲜戒,反而發(fā)得更慢)专控。
視頻編碼的限制條件最初是解碼器的能力,碼率太高就會(huì)無法解碼遏餐,后來隨著 codec 的發(fā)展伦腐,解碼能力不再是瓶頸,限制條件變成了傳輸帶寬/文件大小失都,我們希望在控制數(shù)據(jù)量的前提下柏蘑,畫面質(zhì)量盡可能高。
一般編碼器都可以設(shè)置一個(gè)目標(biāo)碼率粹庞,但編碼器的實(shí)際輸出碼率不會(huì)完全符合設(shè)置咳焚,因?yàn)樵诰幋a過程中實(shí)際可以控制的并不是最終輸出的碼率,而是編碼過程中的一個(gè)量化參數(shù)(Quantization Parameter庞溜,QP)革半,它和碼率并沒有固定的關(guān)系,而是取決于圖像內(nèi)容流码。 這一點(diǎn)不在這里展開又官,感興趣的朋友可以閱讀視頻壓縮編碼和音頻壓縮編碼的基本原理。
無論是要發(fā)送的 TCP 數(shù)據(jù)包漫试,還是要編碼的圖像六敬,都可能出現(xiàn)“尖峰”,也就是短時(shí)間內(nèi)出現(xiàn)較大的數(shù)據(jù)量驾荣。TCP 面對(duì)尖峰外构,可以選擇不為所動(dòng)(尤其是網(wǎng)絡(luò)已經(jīng)擁塞的時(shí)候),這沒有太大的問題播掷,但如果視頻編碼也對(duì)尖峰不為所動(dòng)审编,那圖像質(zhì)量就會(huì)大打折扣了。如果有幾幀數(shù)據(jù)量特別大歧匈,但仍要把碼率控制在原來的水平割笙,那勢(shì)必要損失更多的信息,因此圖像失真就會(huì)更嚴(yán)重。這種情況通常的表現(xiàn)是畫面出現(xiàn)很多小方塊伤溉,看上去像是打了馬賽克一樣,導(dǎo)致畫面的局部或者整體看不清楚的情況
-
Android 硬編碼流控
MediaCodec 流控相關(guān)的接口并不多妻率,一是配置時(shí)設(shè)置目標(biāo)碼率和碼率控制模式乱顾,二是動(dòng)態(tài)調(diào)整目標(biāo)碼率(Android 19+)。
配置時(shí)指定目標(biāo)碼率和碼率控制模式:
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
碼率控制模式有三種:
碼率控制模式在 MediaCodecInfo.EncoderCapabilities
類中定義了三種宫静,在 framework 層有另一套名字和它們的值一一對(duì)應(yīng):
- CQ 對(duì)應(yīng)于
OMX_Video_ControlRateDisable
走净,它表示完全不控制碼率,盡最大可能保證圖像質(zhì)量孤里; - CBR 對(duì)應(yīng)于
OMX_Video_ControlRateConstant
囊榜,它表示編碼器會(huì)盡量把輸出碼率控制為設(shè)定值铃芦,即我們前面提到的“不為所動(dòng)”; - VBR 對(duì)應(yīng)于
OMX_Video_ControlRateVariable
,它表示編碼器會(huì)根據(jù)圖像內(nèi)容的復(fù)雜度(實(shí)際上是幀間變化量的大芯苍 )來動(dòng)態(tài)調(diào)整輸出碼率,圖像復(fù)雜則碼率高只怎,圖像簡(jiǎn)單則碼率低啡专;
動(dòng)態(tài)調(diào)整目標(biāo)碼率:
Bundle param = new Bundle();
param.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate);
mediaCodec.setParameters(param);
Android 流控策略選擇
- 質(zhì)量要求高、不在乎帶寬霍衫、解碼器支持碼率劇烈波動(dòng)的情況下候引,可以選擇 CQ 碼率控制策略。
- VBR 輸出碼率會(huì)在一定范圍內(nèi)波動(dòng)敦跌,對(duì)于小幅晃動(dòng)澄干,方塊效應(yīng)會(huì)有所改善,但對(duì)劇烈晃動(dòng)仍無能為力柠傍;連續(xù)調(diào)低碼率則會(huì)導(dǎo)致碼率急劇下降麸俘,如果無法接受這個(gè)問題,那 VBR 就不是好的選擇携兵。
編碼栗子
下面展示使用MediaExtractor獲取數(shù)據(jù)后疾掰,用MediaMuxer重新寫成一個(gè)MP4文件的簡(jiǎn)單栗子
private void doExtract() throws IOException {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean outputDone = false;
boolean inputDone = false;
while (!outputDone) {
if (!inputDone) {
int inputBufIndex = mMediaCodec.dequeueInputBuffer(10000);
if (inputBufIndex >= 0) {
ByteBuffer inputBuf = mMediaCodec.getInputBuffers()[inputBufIndex];
int chunkSize = mMediaExtractor.readSampleData(inputBuf, 0);
if (chunkSize < 0) {
mMediaCodec.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
} else {
mMediaCodec.queueInputBuffer(inputBufIndex, 0, chunkSize, mMediaExtractor.getSampleTime(), 0);
mMediaExtractor.advance();
}
}
}
if (!outputDone) {
int decoderStatus =mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10000);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
Log.d(TAG, "no output from decoder available");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
Log.d(TAG, "decoder output buffers changed");
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = SDecoder.decoder.getOutputFormat();
Log.d(TAG, "decoder output format changed: " + newFormat);
} else {
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
mMediaCodec.releaseOutputBuffer(outputBufferIndex,false);
outputDone = true;
break;
}
// 獲取一個(gè)只讀的輸出緩存區(qū)inputBuffer ,它包含被編碼好的數(shù)據(jù)
ByteBuffer outputBuffer = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
outputBuffer = mMediaCodec.getOutputBuffers()[outputBufferIndex];
} else {
outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
MediaFormat format = mMediaCodec.getOutputFormat();
format.setByteBuffer("csd-0",outputBuffer);
mBufferInfo.size = 0;
}
// 如果API<=19徐紧,需要根據(jù)BufferInfo的offset偏移量調(diào)整ByteBuffer的位置
// 并且限定將要讀取緩存區(qū)數(shù)據(jù)的長(zhǎng)度静檬,否則輸出數(shù)據(jù)會(huì)混亂
if (mBufferInfo.size != 0) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
}
// mMuxer.writeSampleData(mTrackIndex, encodedData, bufferInfo);
}
mMediaCodec.releaseOutputBuffer(decoderStatus, false);
}
}
}
}