Android 音視訊 - MediaCodec 編解碼音視訊

聲網Agora發表於2021-11-03

我們知道 Camera 採集回傳的是 YUV 資料,AudioRecord 是 PCM,我們要對這些資料進行編碼(壓縮編碼),這裡我們來說在 Android 上音視訊編解碼逃不過的坑-MediaCodec

MediaCodec

PSMediaCodec 可以用來編/解碼 音/視訊

MediaCodec 簡單介紹

MediaCodec 類可用於訪問低階媒體編解碼器,即編碼器/解碼器元件。 它是 Android 低階多媒體支援基礎結構的一部分(通常與 MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface 和 AudioTrack 一起使用)。關於 MediaCodec 的描述可參看官方介紹MediaCodec

img

廣義而言,編解碼器處理輸入資料以生成輸出資料。 它非同步處理資料,並使用一組輸入和輸出緩衝區。 在簡單的情況下,您請求(或接收)一個空的輸入緩衝區,將其填充資料並將其傳送到編解碼器進行處理。 編解碼器用完了資料並將其轉換為空的輸出緩衝區之一。 最後,您請求(或接收)已填充的輸出緩衝區,使用其內容並將其釋放回編解碼器。

PS 讀者如果對生產者-消費者模型還有印象的話,那麼 MediaCodec 的執行模式其實也不難理解。

下面是 MediaCodec 的簡單類圖

img

MediaCodec 狀態機

在 MediaCodec 生命週期內,編解碼器從概念上講處於以下三種狀態之一:Stopped,Executing 或 Released。Stopped 的集體狀態實際上是三個狀態的集合:Uninitialized,Configured 和 Error,而 Executing 狀態從概念上講經過三個子狀態:Flushed,Running 和 Stream-of-Stream。

img

使用工廠方法之一建立編解碼器時,編解碼器處於未初始化狀態。首先,您需要通過 configure(…)對其進行配置,使它進入已配置狀態,然後呼叫 start()將其移至執行狀態。在這種狀態下,您可以通過上述緩衝區佇列操作來處理資料。

執行狀態具有三個子狀態:Flushed,Running 和 Stream-of-Stream。在 start()之後,編解碼器立即處於 Flushed 子狀態,其中包含所有緩衝區。一旦第一個輸入緩衝區出隊,編解碼器將移至“Running”子狀態,在此狀態下將花費大部分時間。當您將輸入緩衝區與流結束標記排隊時,編解碼器將轉換為 End-of-Stream 子狀態。在這種狀態下,編解碼器將不再接受其他輸入緩衝區,但仍會生成輸出緩衝區,直到在輸出端達到流結束為止。在執行狀態下,您可以使用 flush()隨時返回到“重新整理”子狀態。

呼叫 stop()使編解碼器返回 Uninitialized 狀態,隨後可以再次對其進行配置。使用編解碼器完成操作後,必須通過呼叫 release()釋放它。

在極少數情況下,編解碼器可能會遇到錯誤並進入“錯誤”狀態。使用來自排隊操作的無效返回值或有時通過異常來傳達此資訊。呼叫 reset()使編解碼器再次可用。您可以從任何狀態呼叫它,以將編解碼器移回“Uninitialized”狀態。否則,請呼叫 release()以移至終端的“Released”狀態。

PSMediaCodec 資料處理的模式可分為同步和非同步,下面我們會一一分析

MediaCodec 同步模式

img

上程式碼

    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

相關文章