Android 音視訊 - MediaCodec 編解碼音視訊
MediaCodec
PSMediaCodec 可以用來
編/解碼
音/視訊
。
MediaCodec 簡單介紹
MediaCodec 類可用於訪問低階媒體編解碼器,即編碼器/解碼器元件。 它是 Android 低階多媒體支援基礎結構的一部分(通常與 MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface 和 AudioTrack 一起使用)。關於 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
格式
完整程式碼請看
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(); } }
完整程式碼請看
MediaCodec 小結
MediaCodec 用來音視訊的編解碼工作(這個過程有的文章也稱為硬解
),通過MediaCodec.createEncoderByType(MIMETYPE_VIDEO_AVC)
函式中的引數來建立音訊或者視訊的編碼器,同理通過MediaCodec.createDecoderByType(MIMETYPE_VIDEO_AVC)
建立音訊或者視訊的解碼器。對於音視訊編解碼中需要的不同引數用MediaFormat
來指定。
小結
本篇文章詳細的對 MediaCodec 進行了分析,讀者可根據部落格對應 Demo 來進行實際操練。
放上 Demo 地址