我們知道 Camera 採集回傳的是 YUV 資料,AudioRecord 是 PCM,我們要對這些資料進行編碼(壓縮編碼),這裡我們來說在 Android 上音視訊編解碼逃不過的坑-MediaCodec
MediaCodec
PSMediaCodec 可以用來編/解碼
音/視訊
。
MediaCodec 簡單介紹
MediaCodec 類可用於訪問低階媒體編解碼器,即編碼器/解碼器元件。 它是 Android 低階多媒體支援基礎結構的一部分(通常與 MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface 和 AudioTrack 一起使用)。關於 MediaCodec 的描述可參看官方介紹MediaCodec
廣義而言,編解碼器處理輸入資料以生成輸出資料。 它非同步處理資料,並使用一組輸入和輸出緩衝區。 在簡單的情況下,您請求(或接收)一個空的輸入緩衝區,將其填充資料並將其傳送到編解碼器進行處理。 編解碼器用完了資料並將其轉換為空的輸出緩衝區之一。 最後,您請求(或接收)已填充的輸出緩衝區,使用其內容並將其釋放回編解碼器。
PS 讀者如果對生產者-消費者模型還有印象的話,那麼 MediaCodec 的執行模式其實也不難理解。
下面是 MediaCodec 的簡單類圖
MediaCodec 狀態機
在 MediaCodec 生命週期內,編解碼器從概念上講處於以下三種狀態之一:Stopped,Executing 或 Released。Stopped 的集體狀態實際上是三個狀態的集合:Uninitialized,Configured 和 Error,而 Executing 狀態從概念上講經過三個子狀態:Flushed,Running 和 Stream-of-Stream。
使用工廠方法之一建立編解碼器時,編解碼器處於未初始化狀態。首先,您需要通過 configure(…)對其進行配置,使它進入已配置狀態,然後呼叫 start()將其移至執行狀態。在這種狀態下,您可以通過上述緩衝區佇列操作來處理資料。
執行狀態具有三個子狀態:Flushed,Running 和 Stream-of-Stream。在 start()之後,編解碼器立即處於 Flushed 子狀態,其中包含所有緩衝區。一旦第一個輸入緩衝區出隊,編解碼器將移至“Running”子狀態,在此狀態下將花費大部分時間。當您將輸入緩衝區與流結束標記排隊時,編解碼器將轉換為 End-of-Stream 子狀態。在這種狀態下,編解碼器將不再接受其他輸入緩衝區,但仍會生成輸出緩衝區,直到在輸出端達到流結束為止。在執行狀態下,您可以使用 flush()隨時返回到“重新整理”子狀態。
呼叫 stop()使編解碼器返回 Uninitialized 狀態,隨後可以再次對其進行配置。使用編解碼器完成操作後,必須通過呼叫 release()釋放它。
在極少數情況下,編解碼器可能會遇到錯誤並進入“錯誤”狀態。使用來自排隊操作的無效返回值或有時通過異常來傳達此資訊。呼叫 reset()使編解碼器再次可用。您可以從任何狀態呼叫它,以將編解碼器移回“Uninitialized”狀態。否則,請呼叫 release()以移至終端的“Released”狀態。
PSMediaCodec 資料處理的模式可分為同步和非同步,下面我們會一一分析
MediaCodec 同步模式
上程式碼
public H264MediaCodecEncoder(int width, int height) {
//設定MediaFormat的引數
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 {
//通過MIMETYPE建立MediaCodec例項
mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC);
//呼叫configure,傳入的MediaCodec.CONFIGURE_FLAG_ENCODE表示編碼
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
//呼叫start
mMediaCodec.start();
} catch (Exception e) {
e.printStackTrace();
}
}
呼叫 putData 向佇列中 add 原始 YUV 資料
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) {
//從佇列中取資料
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) {
//回撥
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 編解碼這種耗時操作要在單獨的執行緒中完成,我們這裡有個緩衝佇列ArrayBlockingQueue<byte[]> yuv420Queue = new ArrayBlockingQueue<>(10);
,用來接收從 Camera 回撥中傳入的 byte[] YUV 資料,我們又新建立了一個現成來從緩衝佇列yuv420Queue
中迴圈讀取資料交給 MediaCodec 進行編碼處理,編碼完成的格式是由mMediaCodec = MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC);
指定的,這裡輸出的是目前最為廣泛使用的H264
格式
完整程式碼請看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);
//設定回撥
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) {
//回撥
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();
}
}
完整程式碼請看H264MediaCodecAsyncEncoder
MediaCodec 小結
MediaCodec 用來音視訊的編解碼工作(這個過程有的文章也稱為硬解
),通過MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC)
函式中的引數來建立音訊或者視訊的編碼器,同理通過MediaCodec.createDecoderByType(MIMETYPE_VIDEO_AVC)
建立音訊或者視訊的解碼器。對於音視訊編解碼中需要的不同引數用MediaFormat
來指定
小結
本篇文章詳細的對 MediaCodec 進行了分析,讀者可根據部落格對應 Demo 來進行實際操練
放上 Demo 地址詳細Demo