利用 MediaCodec 進行轉碼

前面的文章簡單介紹了 MediaCodec 的使用說明,這篇文章會說明如何使用 MediaCodec 進行視頻轉碼。

首先關於轉碼的流程:

視頻文件 ——> 解封裝 ——> 解碼 ——> 編碼 ——> 封裝 ——> 轉碼後的視頻文件

那麼轉換到 MediaCodec 中對應的流程即:

視頻

  1. MediaExtractor 解封裝 video 數據,

  2. MediaCodec 解碼器解碼壓縮視頻數據,並輸入到 Surface

  3. Surface 中的原始視頻數據輸入到 MediaCodec 編碼器進行編碼

  4. 對編碼器輸出數據進行封裝(不分塊的情況下:使用 MediaMuxer 進行封裝。 分塊的情況下:使用 FFmpeg muxer 進行封裝)

音頻

  1. MediaExtractor 解封裝 audio 數據,

  2. MediaCodec 解碼器解碼壓縮視頻數據

  3. 解碼後的 ByteBuffer 數據輸入 MediaCodec 編碼器進行編碼

  4. 對編碼器輸出數據進行封裝(不分塊的情況下:使用 MediaMuxer 進行封裝。 分塊的情況下:使用 FFmpeg muxer 進行封裝)

先簡單介紹下前面流程中提到的 MediaExtractor & MediaMuxer

MediaExtractor

主要用於提取音視頻相關信息,分離音視頻。讀取音視頻文件,然後按照一定的格式輸出出來。

使用步驟(參考官方示例):

MediaExtractor extractor = new MediaExtractor();
// 設置數據源
extractor.setDataSource(...);
// 文件軌道總數
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; ++i) {
  MediaFormat format = extractor.getTrackFormat(i);
  String mime = format.getString(MediaFormat.KEY_MIME);
  if (weAreInterestedInThisTrack) {
    // 因爲 MediaExtractor 需要選定軌道之後,才能讀取數據。所以針對 video & audio 如果想要同步處理的話,則需要創建兩個MediaExtractor分別讀取
    extractor.selectTrack(i);
  }
}

// 讀取數據到 inputBuffer 
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
while (extractor.readSampleData(inputBuffer, ...) != 0) {
  // 數據對應索引
  int trackIndex = extractor.getSampleTrackIndex();
  // 數據時間戳
  long presentationTimeUs = extractor.getSampleTime();
  ...
  // 前進到下一幀(不存在下一幀,則返回 false)
  extractor.advance();
}
// 釋放
extractor.release();
extractor = null;

MediaMuxer

主要用於封裝編碼後的視頻流和音頻流到文件容器中(目前支持 MP4、Webm、3GP文件封裝格式)

使用步驟:

// 創建 MP4 封裝格式的封裝器
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
// More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
// or MediaExtractor.getTrackFormat().
MediaFormat audioFormat = new MediaFormat(...);
MediaFormat videoFormat = new MediaFormat(...);
int audioTrackIndex = muxer.addTrack(audioFormat);
int videoTrackIndex = muxer.addTrack(videoFormat);
ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
boolean finished = false;
BufferInfo bufferInfo = new BufferInfo();
muxer.start();
while(!finished) {
  // getInputBuffer() will fill the inputBuffer with one frame of encoded
  // sample from either MediaCodec or MediaExtractor, set isAudioSample to
  // true when the sample is audio data, set up all the fields of bufferInfo,
  // and return true if there are no more samples.
  finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
  if (!finished) {
    int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
    // 寫入文件
    muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
  }
};
muxer.stop();
muxer.release();

使用 Surface 作爲解碼的輸出以及編碼的輸入

MediaCodec 通過 Surface 可以實現編解碼的硬件加速。

編碼器通過調用 createInputSurface() 方法獲取一個 Surface 作爲 encoder的輸入。

解碼器在 調用 configure() 方法時傳入 Surface 參數,解碼後的數據直接輸出到 Surface。

前面簡單介紹了 MediaCodec 的大致流程,下面展開具體介紹:

MediaCodec 選擇異步方式,前面的文章已經介紹過異步方式下如何調用,主要是四個方法:

public void onInputBufferAvailable(); // codec 存在可用輸入緩衝區,將需要處理的數據輸入緩衝區
public void onOutputBufferAvailable();// codec 存在可用輸出緩衝,取出完成編解碼的數據進行下一步處理
public void onError(); // 編解碼出錯
public void onOutputFormatChanged(); // 輸出的 MediaFormat 發生了改變

參考着上面的流程圖,介紹下每個主要的步驟

視頻:

  1. 創建 MediaExtractor, 用於獲取輸入視頻的 MediaFormat 以及 讀取視頻壓縮數據

  2. 配置視頻輸出相關參數(碼率、寬&高、幀率等)MediaFormat, 創建 video 編碼器,並獲取 encoder 的輸入 Surface

  3. 通過 MediaExtractor 獲取輸入視頻的 MediaFormat, 創建 video 解碼器,並在 configure 時傳入 Surface 作爲輸出目標

  4. 當 decoder 存在可用輸入緩衝時,通過 MediaExtractor 讀取 video 壓縮數據,傳入 decoder 進行處理(queueInputBuffer)

  5. 當 decoder 存在可用輸出緩衝時,調用 releaseOutputBuffer(index, true) 將數據輸出到 Surface,

    encoder 存在可用輸入緩衝時,會直接從 Surface 獲取數據(這部分會自動處理,不用做額外工作)

  6. encoder 存在可用輸出緩衝時,getOutputBuffer(index) 獲取 video 壓縮數據,進行封裝

音頻:

  1. 創建 MediaExtractor, 用於獲取輸入音頻的 MediaFormat 以及 讀取音頻壓縮數據

  2. 配置音頻輸出相關參數(採樣率、比特率、信道數量等)MediaFormat, 創建 audio 編碼器

  3. 通過 MediaExtractor 獲取輸入音頻的 MediaFormat, 創建 audio 解碼器

  4. 當 decoder 存在可用輸入緩衝時,通過 MediaExtractor 讀取 audio 壓縮數據,傳入 decoder 進行處理(queueInputBuffer)

  5. 當 decoder 存在可用輸出緩衝時,getOutputBuffer(index) 獲取音頻原始數據,並存入本地緩存

    encoder 存在可用輸入緩衝時,將本地緩存中的音頻原始數據 queInputBuffer 輸入編碼器

  6. encoder 存在可用輸出緩衝時,getOutputBuffer(index) 獲取 audio 壓縮數據,進行封裝

Tips:

轉碼中存在視頻截取的場景,MediaCodec 中沒有類似 FFmpeg 中 "-ss、-t" 可以控制截取起點和時長的參數,所以需要在向解碼器輸入參數時人爲進行截取:

// seek 到指定時間(mode - 指定時間的前一幀、後一幀、最靠近的一幀)
public native void seekTo(long timeUs, @SeekMode int mode);

首先: 調用 MediaExtractor.seekTo 方法 seek 到視頻截取開始時間

然後: 在向解碼器中傳輸壓縮數據時,判斷是否處理了足夠時長的數據,下面直接通過代碼來看:

while (!mVideoReadDone) {
    // 讀取視頻數據到解碼器輸入緩衝
    int size = mVideoExtractor.readSampleData(decoderInputBuffer, 0);
    long pst = mVideoExtractor.getSampleTime();
    // 判斷當前幀的時間戳是否已經超過要截取的時長
    if (length != 0 && pst > start + length) {
        // 到達剪輯時間
        mVideoReadDone = true;
        } else {
            if (start > 0) {
                // 如果需要截取視頻,需要重新計算時間戳(因爲當前幀記錄的還是截取之前的時間戳)
                videoPst += videoSampleTime;
                pst = videoPst;
            }
            if (size >= 0) {
                // 將解碼器緩衝送入解碼器
                codec.queueInputBuffer(index, 0, size, pst,
                                mVideoExtractor.getSampleFlags());
            }

            // 視頻數據是否已讀取完
            mVideoReadDone = !mVideoExtractor.advance();
        }
        if (mVideoReadDone) {
            // 視頻數據讀完 或 到達剪輯時間
            logdw(LOG_LEVEL_DEBUG, "Video extractor: EOS");

            // send EOS to decoder
            codec.queueInputBuffer(index, 0, 0, 0,
                    MediaCodec.BUFFER_FLAG_END_OF_STREAM);
        }
        if (size >= 0) {
            break;
        }
}

視頻封裝:

MediaMuxer:

在使用 MediaMuxer 進行音視頻封裝時需要注意:需要先添加 video & audio track,然後才能向 muxer 寫入壓縮數據。

public abstract void onOutputFormatChanged(
                @NonNull MediaCodec codec, @NonNull MediaFormat format);

在編碼器輸出數據之前,會先輸出壓縮數據的 MediaFormat,因此要在 video & audio 編碼器都輸出 OutputFormat 之後,並添加到 MeidaMuxer 之後,再調用 start 方法啓動 Muxer:

// 記錄下 video & audio 的track,後面寫入數據時需要用到
mOutputVideoTrack = mMuxer.addTrack(mEncoderVideoFormat);
mOutputAudioTrack = mMuxer.addTrack(mEncoderAudioFormat);
    
mMuxer.start();

當編碼器輸出壓縮數據後:

public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info)

就可以將 video & audio 壓縮數據寫入 MediaMuxer 進行封裝:

// video 
ByteBuffer videoOutputBuffer = mVideoEncoder.getOutputBuffer(index);
mMuxer.writeSampleData(mOutputVideoTrack, videoOutputBuffer, info);

// audio
ByteBuffer audioOutputBuffer = mAudioEncoder.getOutputBuffer(index);
mMuxer.writeSampleData(mOutputAudioTrack, audioOutputBuffer, info);

FFmpeg: 關於使用 FFmpeg muxer 封裝 MediaCodec 壓縮數據在另外一篇文章中單獨介紹。

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