作者:聲網(wǎng)Agora
我們知道 Camera 采集回傳的是 YUV 數(shù)據(jù)巢价,AudioRecord 是 PCM,我們要對(duì)這些數(shù)據(jù)進(jìn)行編碼(壓縮編碼)苦囱,這里我們來(lái)說(shuō)在 Android 上音視頻編解碼逃不過(guò)的坑-MediaCodec榜聂。
MediaCodec
PSMediaCodec 可以用來(lái)編/解碼 音/視頻唉俗。
MediaCodec 簡(jiǎn)單介紹
MediaCodec 類可用于訪問(wèn)低級(jí)媒體編解碼器闹司,即編碼器/解碼器組件娱仔。 它是 Android 低級(jí)多媒體支持基礎(chǔ)結(jié)構(gòu)的一部分(通常與 MediaExtractor,MediaSync游桩,MediaMuxer牲迫,MediaCrypto,MediaDrm借卧,Image恩溅,Surface 和 AudioTrack 一起使用)。關(guān)于 MediaCodec 的描述可參看官方介紹MediaCodec
廣義而言谓娃,編解碼器處理輸入數(shù)據(jù)以生成輸出數(shù)據(jù)。 它異步處理數(shù)據(jù)蜒滩,并使用一組輸入和輸出緩沖區(qū)滨达。 在簡(jiǎn)單的情況下奶稠,您請(qǐng)求(或接收)一個(gè)空的輸入緩沖區(qū),將其填充數(shù)據(jù)并將其發(fā)送到編解碼器進(jìn)行處理捡遍。 編解碼器用完了數(shù)據(jù)并將其轉(zhuǎn)換為空的輸出緩沖區(qū)之一锌订。 最后,您請(qǐng)求(或接收)已填充的輸出緩沖區(qū)画株,使用其內(nèi)容并將其釋放回編解碼器辆飘。
PS 讀者如果對(duì)生產(chǎn)者-消費(fèi)者模型還有印象的話,那么 MediaCodec 的運(yùn)行模式其實(shí)也不難理解谓传。
下面是 MediaCodec 的簡(jiǎn)單類圖
MediaCodec 狀態(tài)機(jī)
在 MediaCodec 生命周期內(nèi)蜈项,編解碼器從概念上講處于以下三種狀態(tài)之一:Stopped,Executing 或 Released续挟。Stopped 的集體狀態(tài)實(shí)際上是三個(gè)狀態(tài)的集合:Uninitialized紧卒,Configured 和 Error,而 Executing 狀態(tài)從概念上講經(jīng)過(guò)三個(gè)子狀態(tài):Flushed诗祸,Running 和 Stream-of-Stream跑芳。
使用工廠方法之一創(chuàng)建編解碼器時(shí),編解碼器處于未初始化狀態(tài)直颅。首先博个,您需要通過(guò) configure(…)對(duì)其進(jìn)行配置,使它進(jìn)入已配置狀態(tài)功偿,然后調(diào)用 start()將其移至執(zhí)行狀態(tài)盆佣。在這種狀態(tài)下,您可以通過(guò)上述緩沖區(qū)隊(duì)列操作來(lái)處理數(shù)據(jù)脖含。
執(zhí)行狀態(tài)具有三個(gè)子狀態(tài):Flushed罪塔,Running 和 Stream-of-Stream。在 start()之后养葵,編解碼器立即處于 Flushed 子狀態(tài)征堪,其中包含所有緩沖區(qū)。一旦第一個(gè)輸入緩沖區(qū)出隊(duì)关拒,編解碼器將移至“Running”子狀態(tài)佃蚜,在此狀態(tài)下將花費(fèi)大部分時(shí)間。當(dāng)您將輸入緩沖區(qū)與流結(jié)束標(biāo)記排隊(duì)時(shí)着绊,編解碼器將轉(zhuǎn)換為 End-of-Stream 子狀態(tài)谐算。在這種狀態(tài)下,編解碼器將不再接受其他輸入緩沖區(qū)归露,但仍會(huì)生成輸出緩沖區(qū)洲脂,直到在輸出端達(dá)到流結(jié)束為止。在執(zhí)行狀態(tài)下剧包,您可以使用 flush()隨時(shí)返回到“刷新”子狀態(tài)恐锦。
調(diào)用 stop()使編解碼器返回 Uninitialized 狀態(tài)往果,隨后可以再次對(duì)其進(jìn)行配置。使用編解碼器完成操作后一铅,必須通過(guò)調(diào)用 release()釋放它陕贮。
在極少數(shù)情況下,編解碼器可能會(huì)遇到錯(cuò)誤并進(jìn)入“錯(cuò)誤”狀態(tài)潘飘。使用來(lái)自排隊(duì)操作的無(wú)效返回值或有時(shí)通過(guò)異常來(lái)傳達(dá)此信息肮之。調(diào)用 reset()使編解碼器再次可用。您可以從任何狀態(tài)調(diào)用它卜录,以將編解碼器移回“Uninitialized”狀態(tài)戈擒。否則,請(qǐng)調(diào)用 release()以移至終端的“Released”狀態(tài)暴凑。
PSMediaCodec 數(shù)據(jù)處理的模式可分為同步和異步峦甩,下面我們會(huì)一一分析
MediaCodec 同步模式
上代碼
public H264MediaCodecEncoder(int width, int height) {
//設(shè)置MediaFormat的參數(shù)
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIMETYPE_VIDEO_AVC, width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);//FPS
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
try {
//通過(guò)MIMETYPE創(chuàng)建MediaCodec實(shí)例
mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC);
//調(diào)用configure,傳入的MediaCodec.CONFIGURE_FLAG_ENCODE表示編碼
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//調(diào)用start
mMediaCodec.start();
} catch (Exception e) {
e.printStackTrace();
}
}
調(diào)用 putData 向隊(duì)列中 add 原始 YUV 數(shù)據(jù)
public void putData(byte[] buffer) {
if (yuv420Queue.size() >= 10) {
yuv420Queue.poll();
}
yuv420Queue.add(buffer);
}
//開啟編碼
public void startEncoder() {
isRunning = true;
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(new Runnable() {
@Override
public void run() {
byte[] input = null;
while (isRunning) {
if (yuv420Queue.size() > 0) {
//從隊(duì)列中取數(shù)據(jù)
input = yuv420Queue.poll();
}
if (input != null) {
try {
//【1】dequeueInputBuffer
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_S);
if (inputBufferIndex >= 0) {
//【2】getInputBuffer
ByteBuffer inputBuffer = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex);
} else {
inputBuffer = mMediaCodec.getInputBuffers()[inputBufferIndex];
}
inputBuffer.clear();
inputBuffer.put(input);
//【3】queueInputBuffer
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, getPTSUs(), 0);
}
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
//【4】dequeueOutputBuffer
int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_S);
if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = mMediaCodec.getOutputFormat();
if (null != mEncoderCallback) {
mEncoderCallback.outputMediaFormatChanged(H264_ENCODER, newFormat);
}
if (mMuxer != null) {
if (mMuxerStarted) {
throw new RuntimeException("format changed twice");
}
// now that we have the Magic Goodies, start the muxer
mTrackIndex = mMuxer.addTrack(newFormat);
mMuxer.start();
mMuxerStarted = true;
}
}
while (outputBufferIndex >= 0) {
ByteBuffer outputBuffer = null;
//【5】getOutputBuffer
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex);
} else {
outputBuffer = mMediaCodec.getOutputBuffers()[outputBufferIndex];
}
if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
bufferInfo.size = 0;
}
if (bufferInfo.size > 0) {
// adjust the ByteBuffer values to match BufferInfo (not needed?)
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
// write encoded data to muxer(need to adjust presentationTimeUs.
bufferInfo.presentationTimeUs = getPTSUs();
if (mEncoderCallback != null) {
//回調(diào)
mEncoderCallback.onEncodeOutput(H264_ENCODER, outputBuffer, bufferInfo);
}
prevOutputPTSUs = bufferInfo.presentationTimeUs;
if (mMuxer != null) {
if (!mMuxerStarted) {
throw new RuntimeException("muxer hasn't started");
}
mMuxer.writeSampleData(mTrackIndex, outputBuffer, bufferInfo);
}
}
mMediaCodec.releaseOutputBuffer(outputBufferIndex, false);
bufferInfo = new MediaCodec.BufferInfo();
outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_S);
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} else {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
}
PS 編解碼這種耗時(shí)操作要在單獨(dú)的線程中完成,我們這里有個(gè)緩沖隊(duì)列
完整代碼請(qǐng)看H264MediaCodecEncoder
MediaCodec 異步模式
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public H264MediaCodecAsyncEncoder(int width, int height) {
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIMETYPE_VIDEO_AVC, width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);//FPS
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
try {
mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC);
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//設(shè)置回調(diào)
mMediaCodec.setCallback(new MediaCodec.Callback() {
@Override
/**
* Called when an input buffer becomes available.
*
* @param codec The MediaCodec object.
* @param index The index of the available input buffer.
*/
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
Log.i("MFB", "onInputBufferAvailable:" + index);
byte[] input = null;
if (isRunning) {
if (yuv420Queue.size() > 0) {
input = yuv420Queue.poll();
}
if (input != null) {
ByteBuffer inputBuffer = codec.getInputBuffer(index);
inputBuffer.clear();
inputBuffer.put(input);
codec.queueInputBuffer(index, 0, input.length, getPTSUs(), 0);
}
}
}
@Override
/**
* Called when an output buffer becomes available.
*
* @param codec The MediaCodec object.
* @param index The index of the available output buffer.
* @param info Info regarding the available output buffer {@link MediaCodec.BufferInfo}.
*/
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
Log.i("MFB", "onOutputBufferAvailable:" + index);
ByteBuffer outputBuffer = codec.getOutputBuffer(index);
if (info.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
info.size = 0;
}
if (info.size > 0) {
// adjust the ByteBuffer values to match BufferInfo (not needed?)
outputBuffer.position(info.offset);
outputBuffer.limit(info.offset + info.size);
// write encoded data to muxer(need to adjust presentationTimeUs.
info.presentationTimeUs = getPTSUs();
if (mEncoderCallback != null) {
//回調(diào)
mEncoderCallback.onEncodeOutput(H264_ENCODER, outputBuffer, info);
}
prevOutputPTSUs = info.presentationTimeUs;
if (mMuxer != null) {
if (!mMuxerStarted) {
throw new RuntimeException("muxer hasn't started");
}
mMuxer.writeSampleData(mTrackIndex, outputBuffer, info);
}
}
codec.releaseOutputBuffer(index, false);
}
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
}
@Override
/**
* Called when the output format has changed
*
* @param codec The MediaCodec object.
* @param format The new output format.
*/
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
if (null != mEncoderCallback) {
mEncoderCallback.outputMediaFormatChanged(H264_ENCODER, format);
}
if (mMuxer != null) {
if (mMuxerStarted) {
throw new RuntimeException("format changed twice");
}
// now that we have the Magic Goodies, start the muxer
mTrackIndex = mMuxer.addTrack(format);
mMuxer.start();
mMuxerStarted = true;
}
}
});
mMediaCodec.start();
} catch (Exception e) {
e.printStackTrace();
}
}
完整代碼請(qǐng)看H264MediaCodecAsyncEncoder
MediaCodec 小結(jié)
MediaCodec 用來(lái)音視頻的編解碼工作(這個(gè)過(guò)程有的文章也稱為硬解)现喳,通過(guò)MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC)函數(shù)中的參數(shù)來(lái)創(chuàng)建音頻或者視頻的編碼器凯傲,同理通過(guò)MediaCodec.createDecoderByType(MIMETYPE_VIDEO_AVC)創(chuàng)建音頻或者視頻的解碼器。對(duì)于音視頻編解碼中需要的不同參數(shù)用MediaFormat來(lái)指定嗦篱。
小結(jié)
本篇文章詳細(xì)的對(duì) MediaCodec 進(jìn)行了分析冰单,讀者可根據(jù)博客對(duì)應(yīng) Demo 來(lái)進(jìn)行實(shí)際操練。
放上 Demo 地址詳細(xì)Demo