前面的文章簡單介紹了 MediaCodec 的使用說明,這篇文章會(huì)說明如何使用 MediaCodec 進(jìn)行視頻轉(zhuǎn)碼。
首先關(guān)于轉(zhuǎn)碼的流程:
視頻文件 ——> 解封裝 ——> 解碼 ——> 編碼 ——> 封裝 ——> 轉(zhuǎn)碼后的視頻文件
那么轉(zhuǎn)換到 MediaCodec 中對應(yīng)的流程即:
視頻
MediaExtractor 解封裝 video 數(shù)據(jù),
MediaCodec 解碼器解碼壓縮視頻數(shù)據(jù),并輸入到 Surface
Surface 中的原始視頻數(shù)據(jù)輸入到 MediaCodec 編碼器進(jìn)行編碼
對編碼器輸出數(shù)據(jù)進(jìn)行封裝(不分塊的情況下:使用 MediaMuxer 進(jìn)行封裝。 分塊的情況下:使用 FFmpeg muxer 進(jìn)行封裝)
音頻
MediaExtractor 解封裝 audio 數(shù)據(jù),
MediaCodec 解碼器解碼壓縮視頻數(shù)據(jù)
解碼后的 ByteBuffer 數(shù)據(jù)輸入 MediaCodec 編碼器進(jìn)行編碼
對編碼器輸出數(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 選擇異步方式,前面的文章已經(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è)主要的步驟
視頻:
創(chuàng)建 MediaExtractor, 用于獲取輸入視頻的 MediaFormat 以及 讀取視頻壓縮數(shù)據(jù)
配置視頻輸出相關(guān)參數(shù)(碼率、寬&高、幀率等)MediaFormat, 創(chuàng)建 video 編碼器,并獲取 encoder 的輸入 Surface
通過 MediaExtractor 獲取輸入視頻的 MediaFormat, 創(chuàng)建 video 解碼器,并在 configure 時(shí)傳入 Surface 作為輸出目標(biāo)
當(dāng) decoder 存在可用輸入緩沖時(shí),通過 MediaExtractor 讀取 video 壓縮數(shù)據(jù),傳入 decoder 進(jìn)行處理(queueInputBuffer)
-
當(dāng) decoder 存在可用輸出緩沖時(shí),調(diào)用 releaseOutputBuffer(index, true) 將數(shù)據(jù)輸出到 Surface,
encoder 存在可用輸入緩沖時(shí),會(huì)直接從 Surface 獲取數(shù)據(jù)(這部分會(huì)自動(dòng)處理,不用做額外工作)
encoder 存在可用輸出緩沖時(shí),getOutputBuffer(index) 獲取 video 壓縮數(shù)據(jù),進(jìn)行封裝
音頻:
創(chuàng)建 MediaExtractor, 用于獲取輸入音頻的 MediaFormat 以及 讀取音頻壓縮數(shù)據(jù)
配置音頻輸出相關(guān)參數(shù)(采樣率、比特率、信道數(shù)量等)MediaFormat, 創(chuàng)建 audio 編碼器
通過 MediaExtractor 獲取輸入音頻的 MediaFormat, 創(chuàng)建 audio 解碼器
當(dāng) decoder 存在可用輸入緩沖時(shí),通過 MediaExtractor 讀取 audio 壓縮數(shù)據(jù),傳入 decoder 進(jìn)行處理(queueInputBuffer)
-
當(dāng) decoder 存在可用輸出緩沖時(shí),getOutputBuffer(index) 獲取音頻原始數(shù)據(jù),并存入本地緩存
encoder 存在可用輸入緩沖時(shí),將本地緩存中的音頻原始數(shù)據(jù) queInputBuffer 輸入編碼器
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ú)介紹。