利用 MediaCodec 進行轉碼
前面的文章簡單介紹了 MediaCodec 的使用說明,這篇文章會說明如何使用 MediaCodec 進行視訊轉碼。
首先關於轉碼的流程:
視訊檔案 ——> 解封裝 ——> 解碼 ——> 編碼 ——> 封裝 ——> 轉碼後的視訊檔案
那麼轉換到 MediaCodec 中對應的流程即:
視訊
-
MediaExtractor 解封裝 video 資料,
-
MediaCodec 解碼器解碼壓縮視訊資料,並輸入到 Surface
-
Surface 中的原始視訊資料輸入到 MediaCodec 編碼器進行編碼
-
對編碼器輸出資料進行封裝(不分塊的情況下:使用 MediaMuxer 進行封裝。 分塊的情況下:使用 FFmpeg muxer 進行封裝)
音訊
-
MediaExtractor 解封裝 audio 資料,
-
MediaCodec 解碼器解碼壓縮視訊資料
-
解碼後的 ByteBuffer 資料輸入 MediaCodec 編碼器進行編碼
-
對編碼器輸出資料進行封裝(不分塊的情況下:使用 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 轉碼流程.png
MediaCodec 選擇非同步方式,前面的文章已經介紹過非同步方式下如何呼叫,主要是四個方法:
public void onInputBufferAvailable(); // codec 存在可用輸入緩衝區,將需要處理的資料輸入緩衝區 public void onOutputBufferAvailable();// codec 存在可用輸出緩衝,取出完成編解碼的資料進行下一步處理 public void onError(); // 編解碼出錯 public void onOutputFormatChanged(); // 輸出的 MediaFormat 發生了改變
參考著上面的流程圖,介紹下每個主要的步驟
視訊:
-
建立 MediaExtractor, 用於獲取輸入視訊的 MediaFormat 以及 讀取視訊壓縮資料
-
配置視訊輸出相關引數(位元速率、寬&高、幀率等)MediaFormat, 建立 video 編碼器,並獲取 encoder 的輸入 Surface
-
通過 MediaExtractor 獲取輸入視訊的 MediaFormat, 建立 video 解碼器,並在 configure 時傳入 Surface 作為輸出目標
-
當 decoder 存在可用輸入緩衝時,通過 MediaExtractor 讀取 video 壓縮資料,傳入 decoder 進行處理(queueInputBuffer)
-
當 decoder 存在可用輸出緩衝時,呼叫 releaseOutputBuffer(index, true) 將資料輸出到 Surface,
encoder 存在可用輸入緩衝時,會直接從 Surface 獲取資料(這部分會自動處理,不用做額外工作)
-
encoder 存在可用輸出緩衝時,getOutputBuffer(index) 獲取 video 壓縮資料,進行封裝
音訊:
-
建立 MediaExtractor, 用於獲取輸入音訊的 MediaFormat 以及 讀取音訊壓縮資料
-
配置音訊輸出相關引數(取樣率、位元率、通道數量等)MediaFormat, 建立 audio 編碼器
-
通過 MediaExtractor 獲取輸入音訊的 MediaFormat, 建立 audio 解碼器
-
當 decoder 存在可用輸入緩衝時,通過 MediaExtractor 讀取 audio 壓縮資料,傳入 decoder 進行處理(queueInputBuffer)
-
當 decoder 存在可用輸出緩衝時,getOutputBuffer(index) 獲取音訊原始資料,並存入本地快取
encoder 存在可用輸入緩衝時,將本地快取中的音訊原始資料 queInputBuffer 輸入編碼器
-
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 壓縮資料在另外一篇文章中單獨介紹。