Android MediaCodec 使用說明

最近公司要求提供一個支持 Android 硬件轉碼的底層庫,所以自己從頭去看了 MediaCodec 相關的知識,費了老大的勁終於完成了。目前的硬件轉碼使用 MediaCodec 進行解碼和編碼,然後使用 FFmpeg 進行文件封裝(爲了支持文件分塊)。這篇文章主要介紹一些 MediaCodec 的基礎知識和使用方式,後面會寫如何利用 FFmpeg 封裝 MediaCodec 編碼後的數據以及 FFmpeg 分塊封裝的文章。

MediaCodec 可以用來獲得安卓底層的多媒體編碼,可以用來編碼和解碼,它是安卓 low-level 多媒體基礎框架的重要組成部分。

MediaCodec 的作用是處理輸入的數據生成輸出數據。首先生成一個輸入數據緩衝區,將數據填入緩衝區提供給 codec,codec 會採用異步的方式處理這些輸入的數據,然後將填滿輸出緩衝區提供給消費者,消費者消費完後將緩衝區返還給 codec。

接收的數據

MediaCodec 接受三種數據格式:壓縮數據,原始音頻數據和原始視頻數據。

這三種數據都可以使用 ByteBuffer 作爲載體傳輸給 MediaCodec 來處理。但是當使用原始視頻數據時,最好採用 Surface 作爲輸入源來替代 ByteBuffer,這樣效率更高,因爲 Surface 使用的更底層的視頻數據,不會映射或複製到 ByteBuffer 緩衝區。

壓縮數據

壓縮數據可以作爲解碼器的輸入數據或者編碼器的輸出數據,需要指定數據格式,這樣 codec 才能知道如何處理這些壓縮數據。

對於視頻數據而言,通常是一幀數據;音頻數據,一般是單個處理單元。

原始音頻數據

原始音頻數據即編碼器的輸入數據,解碼器的輸出數據。包含整個 PCM 音頻數據幀,這是通道順序中每個通道的一個樣本。每個採樣都是以本地字節順序的 16 位有符號整數。

原始視頻數據

原始視頻數據也是編碼器的輸入數據,解碼器的輸出數據。即yuv數據,MediaCodec主要支持的格式爲:

  • native raw video format : COLOR_FormatSurface,用來處理 Surface 模式的數據輸入輸出
  • flexible YUV buffers : 例如 COLOR_FormatYUV420Flexible
  • specific formats: 支持ByteBuffer模式,有一些廠家會定製

使用流程

編解碼器處理輸入數據併產生輸出數據,MediaCodec 使用輸入輸出緩存,異步處理數據。

  1. 請求一個空的輸入 input buffer
  2. 填入數據、並將其交給 MediaCodec
  3. MediaCodec 處理數據後,將處理後的數據放在一個空的 output buffer
  4. 獲取填充數據了的 output buffer,得到其中的數據,然後將其返還給 MediaCodec

首先了解下 MediaCodec 中的生命週期


MediaCodec 大體上分爲三種狀態:Stopped、Executing 和 Released。

創建 MediaCodec

首先是如何創建 MediaCodec,在知道 MimeType 的情況下,可以通過 createDecoderByType, createEncoderByType, createByCodecName 方法來獲取實例。

如果不知道 MimeType,可以使用 MediaCodecList.findDecoderForFormat、 MediaCodecList.findEncoderForFormat 來獲取。

創建成功之後,MediaCodec 進入 Uninitialized 狀態。

Configuration

在創建好 MediaCodec 之後,需要對其進行設置,這樣 MediaCodec 的狀態就可以由 uninitialized 變成 configured

public void configure(
            @Nullable MediaFormat format,
            @Nullable Surface surface, @Nullable MediaCrypto crypto,
            @ConfigureFlag int flags) {
        configure(format, surface, crypto, null, flags);
}
public void configure(
            @Nullable MediaFormat format, @Nullable Surface surface,
            @ConfigureFlag int flags, @Nullable MediaDescrambler descrambler) {
        configure(format, surface, null,
                descrambler != null ? descrambler.getBinder() : null, flags);
}

這裏最重要的參數是 MediaFormat, 如果某些參數沒有設置的話,會導致 MediaCodec 拋出 IllegalStateException.

Video 所必須的 Format Setting

Encoder Decoder
KEY_MIME ✔️ ✔️
KEY_BIT_RATE ✔️
KEY_WIDTH ✔️ ✔️
KEY_HEIGHT ✔️ ✔️
KEY_COLOR_FORMAT ✔️
KYE_FRAME_RATE ✔️
KEY_I_FRAME_INTERVAL ✔️

Audio 所必須的 Format Setting

Encoder Decoder
KEY_MIME ✔️ ✔️
KEY_BIT_RATE ✔️
KEY_CHANNEL_COUNT ✔️ ✔️
KEY_SAMPLE_RATE ✔️ ✔️

輸入數據與獲取編解碼後的數據

從 5.0 開始,首選方法是在調用 configure 方法之前通過設置回調來異步處理數據。所以這裏就直接介紹異步模式下如何輸入需要編解碼的數據,以及如何獲取編解碼後的數據

異步模式

官方示例代碼:

MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
// 設置回調方法
codec.setCallback(new MediaCodec.Callback() {
   /**
    * mediacodec 存在可用輸入緩衝
    */
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // 可通過 MediaExtractor 讀取 video 或 audio 數據,然後填充數據到緩衝區
     …
     codec.queueInputBuffer(inputBufferId, …);
   }

   /**
    * 輸出緩衝填充完數據後
    */
   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     // 獲取輸出緩衝(其中包含編解碼後數據)
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); 
     // 處理編解碼後的數據
     …
     // 返還輸出緩衝給 codec
     codec.releaseOutputBuffer(outputBufferId, …);
   }

    /**
     * 輸出格式發生變化
     */
   @Override
   void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     mOutputFormat = format; 
   }

    /**
     * 發生錯誤
     */
   @Override
   void onError(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); 
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

看一個幾個重要的方法

ByteBuffer getInputBuffer(int index)

該方法返回一個已清空、可寫入的 input 緩衝區,通過調用 ByteBuffer.put(data) 方法將 data 中的數據放到緩衝區,然後調用

/**
 * @param index              - 緩衝區索引
 * @param offset             - 緩衝區提交數據的起始位置
 * @param size               - 提交的數據長度
 * @param presentationTimeUs - 時間戳
 * @param flags              - BUFFER_FLAG_CODEC_CONFIG:配置信息;               
 *                             BUFFER_FLAG_END_OF_STREAM:結束標誌; 
 *                             BUFFER_FLAG_KEY_FRAME:關鍵幀
 */
void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags)

就可以將緩衝區返回給 codec。

ByteBuffer getOutputBuffer(int index)

該方法返回一個 output 緩衝區,包含解碼或編碼後的數據。

void releaseOutputBuffer(int index, boolean render)
void releaseOutputBuffer(int index, long renderTimeStampNs)

這兩個方法都會釋放 index 所指向的緩衝區。

處理完需要編/解碼的數據之後,調用 stop & release 方法釋放 MediaCodec。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章