前面的文章簡單介紹了 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 選擇異步方式,前面的文章已經介紹過異步方式下如何調用,主要是四個方法:
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 壓縮數據在另外一篇文章中單獨介紹。