利用 MediaCodec 進(jìn)行轉(zhuǎn)碼

前面的文章簡單介紹了 MediaCodec 的使用說明,這篇文章會(huì)說明如何使用 MediaCodec 進(jìn)行視頻轉(zhuǎn)碼。

首先關(guān)于轉(zhuǎn)碼的流程:

視頻文件 ——> 解封裝 ——> 解碼 ——> 編碼 ——> 封裝 ——> 轉(zhuǎn)碼后的視頻文件

那么轉(zhuǎn)換到 MediaCodec 中對應(yīng)的流程即:

視頻

  1. MediaExtractor 解封裝 video 數(shù)據(jù),

  2. MediaCodec 解碼器解碼壓縮視頻數(shù)據(jù),并輸入到 Surface

  3. Surface 中的原始視頻數(shù)據(jù)輸入到 MediaCodec 編碼器進(jìn)行編碼

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

音頻

  1. MediaExtractor 解封裝 audio 數(shù)據(jù),

  2. MediaCodec 解碼器解碼壓縮視頻數(shù)據(jù)

  3. 解碼后的 ByteBuffer 數(shù)據(jù)輸入 MediaCodec 編碼器進(jìn)行編碼

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

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

MediaExtractor

主要用于提取音視頻相關(guān)信息,分離音視頻。讀取音視頻文件,然后按照一定的格式輸出出來。

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

MediaExtractor extractor = new MediaExtractor();
// 設(shè)置數(shù)據(jù)源
extractor.setDataSource(...);
// 文件軌道總數(shù)
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) {
    // 因?yàn)?MediaExtractor 需要選定軌道之后,才能讀取數(shù)據(jù)。所以針對 video & audio 如果想要同步處理的話,則需要?jiǎng)?chuàng)建兩個(gè)MediaExtractor分別讀取
    extractor.selectTrack(i);
  }
}

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

MediaMuxer

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

使用步驟:

// 創(chuàng)建 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 可以實(shí)現(xiàn)編解碼的硬件加速。

編碼器通過調(diào)用 createInputSurface() 方法獲取一個(gè) Surface 作為 encoder的輸入。

解碼器在 調(diào)用 configure() 方法時(shí)傳入 Surface 參數(shù),解碼后的數(shù)據(jù)直接輸出到 Surface。

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

MediaCodec 轉(zhuǎn)碼流程.png

MediaCodec 選擇異步方式,前面的文章已經(jīng)介紹過異步方式下如何調(diào)用,主要是四個(gè)方法:

public void onInputBufferAvailable(); // codec 存在可用輸入緩沖區(qū),將需要處理的數(shù)據(jù)輸入緩沖區(qū)
public void onOutputBufferAvailable();// codec 存在可用輸出緩沖,取出完成編解碼的數(shù)據(jù)進(jìn)行下一步處理
public void onError(); // 編解碼出錯(cuò)
public void onOutputFormatChanged(); // 輸出的 MediaFormat 發(fā)生了改變

參考著上面的流程圖,介紹下每個(gè)主要的步驟

視頻:

  1. 創(chuàng)建 MediaExtractor, 用于獲取輸入視頻的 MediaFormat 以及 讀取視頻壓縮數(shù)據(jù)

  2. 配置視頻輸出相關(guān)參數(shù)(碼率、寬&高、幀率等)MediaFormat, 創(chuàng)建 video 編碼器,并獲取 encoder 的輸入 Surface

  3. 通過 MediaExtractor 獲取輸入視頻的 MediaFormat, 創(chuàng)建 video 解碼器,并在 configure 時(shí)傳入 Surface 作為輸出目標(biāo)

  4. 當(dāng) decoder 存在可用輸入緩沖時(shí),通過 MediaExtractor 讀取 video 壓縮數(shù)據(jù),傳入 decoder 進(jìn)行處理(queueInputBuffer)

  5. 當(dāng) decoder 存在可用輸出緩沖時(shí),調(diào)用 releaseOutputBuffer(index, true) 將數(shù)據(jù)輸出到 Surface,

    encoder 存在可用輸入緩沖時(shí),會(huì)直接從 Surface 獲取數(shù)據(jù)(這部分會(huì)自動(dòng)處理,不用做額外工作)

  6. encoder 存在可用輸出緩沖時(shí),getOutputBuffer(index) 獲取 video 壓縮數(shù)據(jù),進(jìn)行封裝

音頻:

  1. 創(chuàng)建 MediaExtractor, 用于獲取輸入音頻的 MediaFormat 以及 讀取音頻壓縮數(shù)據(jù)

  2. 配置音頻輸出相關(guān)參數(shù)(采樣率、比特率、信道數(shù)量等)MediaFormat, 創(chuàng)建 audio 編碼器

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

  4. 當(dāng) decoder 存在可用輸入緩沖時(shí),通過 MediaExtractor 讀取 audio 壓縮數(shù)據(jù),傳入 decoder 進(jìn)行處理(queueInputBuffer)

  5. 當(dāng) decoder 存在可用輸出緩沖時(shí),getOutputBuffer(index) 獲取音頻原始數(shù)據(jù),并存入本地緩存

    encoder 存在可用輸入緩沖時(shí),將本地緩存中的音頻原始數(shù)據(jù) queInputBuffer 輸入編碼器

  6. encoder 存在可用輸出緩沖時(shí),getOutputBuffer(index) 獲取 audio 壓縮數(shù)據(jù),進(jìn)行封裝

Tips:

轉(zhuǎn)碼中存在視頻截取的場景,MediaCodec 中沒有類似 FFmpeg 中 "-ss、-t" 可以控制截取起點(diǎn)和時(shí)長的參數(shù),所以需要在向解碼器輸入?yún)?shù)時(shí)人為進(jìn)行截?。?/p>

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

首先: 調(diào)用 MediaExtractor.seekTo 方法 seek 到視頻截取開始時(shí)間

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

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

            // 視頻數(shù)據(jù)是否已讀取完
            mVideoReadDone = !mVideoExtractor.advance();
        }
        if (mVideoReadDone) {
            // 視頻數(shù)據(jù)讀完 或 到達(dá)剪輯時(shí)間
            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 進(jìn)行音視頻封裝時(shí)需要注意:需要先添加 video & audio track,然后才能向 muxer 寫入壓縮數(shù)據(jù)。

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

在編碼器輸出數(shù)據(jù)之前,會(huì)先輸出壓縮數(shù)據(jù)的 MediaFormat,因此要在 video & audio 編碼器都輸出 OutputFormat 之后,并添加到 MeidaMuxer 之后,再調(diào)用 start 方法啟動(dòng) Muxer:

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

當(dāng)編碼器輸出壓縮數(shù)據(jù)后:

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

就可以將 video & audio 壓縮數(shù)據(jù)寫入 MediaMuxer 進(jìn)行封裝:

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

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

FFmpeg: 關(guān)于使用 FFmpeg muxer 封裝 MediaCodec 壓縮數(shù)據(jù)在另外一篇文章中單獨(dú)介紹。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、文章說明 最近工作實(shí)在太忙,很久沒有更新文章了,收到很多小伙伴催更的消息,心中實(shí)在慚愧,趁著今天有空趕緊更新。...
    風(fēng)從影閱讀 19,293評論 33 118
  • 原文:https://developer.android.com/reference/android/media/...
    thebestofrocky閱讀 6,361評論 0 6
  • 3、使用 MediaCodec創(chuàng)建之后,需要通過start()方法進(jìn)行開啟。MediaCodec有輸入緩沖區(qū)隊(duì)列和...
    韓瞅瞅閱讀 1,366評論 0 1
  • 打包 視音頻在傳輸過程中需要定義相應(yīng)的格式,這樣傳輸?shù)綄Χ说臅r(shí)候才能正確地被解析出來。 1、HTTP-FLV We...
    韓瞅瞅閱讀 1,820評論 2 5
  • flink的核心操作有3部分: source transformation sink window 有2類窗口...
    3bd3c1497272閱讀 531評論 0 0

友情鏈接更多精彩內(nèi)容