Android音視訊-視訊分解與合成(MP4檔案)
上一篇們通過Camera的API結合MediaRecorder實現了視訊的錄製,具體的程式碼也大致的瞭解了。使用起來不是很難,這次得加大對視訊的理解。在視訊的基礎知識裡面我們瞭解了一些視訊的相關的概念和名詞,這篇文章我們搞清楚視訊的組成,視訊分離,視訊的合成等概念和實現方法,這裡操作的是MP4檔案,其他的檔案格式我覺得按照這個思路去了解應該也問題不大。
簡介
查閱網上各種資料以後解析和分離視訊大致有如下三種方式
Android系統自帶API MediaExtractor和MediaMuxer類
MediaExtractor是API16之後出來的,MediaMuxer是API18之後出來的Mp4Parser
一個開源的專案,好想功能很強大FFMepg
一個終極武器,我們應該都聽過。
我們現在瞭解的是MediaExtractor和MediaMuxer類相關的來實現。
完整示例程式碼檢視
MP4檔案簡介
維基百科定義
MP4,全稱MPEG-4 Part 14,是一種使用MPEG-4的多媒體電腦檔案格式,副檔名為.mp4,以儲存數字音訊及數字視訊為主。
這個看了並沒啥用,我們現在要關心的是它的一個總體的結構,裡面的構成細節實在是太多了。推薦細節介紹文章現在也沒去看裡面的東西,要細節瞭解的時候看。
MP4檔案結構
有幾個基本的概念大概的瞭解一下
- box
MP4檔案中的所有資料都封裝在box中,MP4檔案由若干個box組成,每個box有型別和長度。並且有不同的box型別
MP4檔案主要由不同的box組成,ftyp,mdat和moov。fypt指示一些頭部資訊(知道的MetaData),可以判斷檔案的型別。mdat存放媒體資料,我們在使用MediaRecorder的時候設定的視訊和音訊的一些資料就放在它裡面。moov包含一個mvhd和若干個track box,它的功能是解析mdat裡面的資料,並且拿到每一幀的資料,怎麼個原理弄到資料,參考下面的參考文章。
裡面的細節現在我們也扣不完,先有一個大致的概念,等要深入瞭解的時候再入手,上面的幾個概念參考自
這裡
不錯的MP4資料結構細節介紹
分離視訊檔案
對於一個視訊檔案它包含視訊和音訊資料,我們現在把它分離出來得到聲音和影象資料。我們使用MediaExtractor可以實現這個功能
MediaExtractor
MediaExtractor便於從資料來源中提取解析多媒體資料。我們可以使用這個它強大的API來實現我們的功能。它使用的大致模版參考官網給出的
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) {
extractor.selectTrack(i);
}
}
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
while (extractor.readSampleData(inputBuffer, ...) >= 0) {
int trackIndex = extractor.getSampleTrackIndex();
long presentationTimeUs = extractor.getSampleTime();
...
extractor.advance();
}
extractor.release();
extractor = null;
MediaMuxer
這個類可以合成音訊和視訊,同樣它還可以把我們從一個MP4中分離出來的音訊和視訊的軌道資訊生成一個單獨的視訊和音訊檔案,非常的強大。我們下面的視訊合成的例子就是它和MediaExtractor來實現的。
MediaMuxer最多僅支援一個視訊track和一個音訊track,所以如果有多個音訊track可以先把它們混合成為一個音訊track然後再使用MediaMuxer封裝到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();
分離視訊主要程式碼
主要過程為
- 構建原始檔和MediaExtractor關聯
- 獲取原始檔Mp4中的視訊和音訊通道並且和MediaMuxer關聯
- 開始解析音訊或者視訊檔案
- 解析檔案兩幀資料的時間
- 解析寫入資料
- 釋放資源
通常視訊編碼使用H.264(AVC)編碼,音訊編碼使用AAC編碼,我們的MP4檔案也是這兩種編碼方式。
public void divideMedia(AssetFileDescriptor inputFile, File outAudioFile, File outVideoFile) {
MediaExtractor mediaExtractor = new MediaExtractor();
try {
//設定要分離的原始MP4檔案
mediaExtractor.setDataSource(inputFile);
//獲取檔案通道數目
int trackCount = mediaExtractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
//獲取當前軌道的媒體格式
MediaFormat mediaFormat = mediaExtractor.getTrackFormat(i);
//獲取媒體格式的mime type(我們的為video/avc 和audio/)
String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
//獲取軌道檔案最大值
int maxInputSize = mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
Log.d(TAG, "maxInputSize:" + maxInputSize);
ByteBuffer byteBuffer = ByteBuffer.allocate(maxInputSize);
if (mime.startsWith(AUDIO_MIME)) {
Log.d(TAG, "divide audio media to file +");
//構建音訊檔案合成物件MediaMuxer
MediaMuxer mediaMuxer = new MediaMuxer(outAudioFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
//給MediaMuxer新增通道的MediaFormat
int audioTrack = mediaMuxer.addTrack(mediaFormat);
mediaMuxer.start();
divideToOutputAudio(mediaExtractor, mediaMuxer, byteBuffer, mediaFormat, audioTrack, i);
//停止MediaMuxer釋放資源
mediaMuxer.stop();
mediaMuxer.release();
mediaMuxer = null;
Log.d(TAG, "divide audio media to file -");
} else if (mime.startsWith(VIDEO_MIME)) {
Log.d(TAG, "divide video media to file +");
MediaMuxer mediaMuxer = new MediaMuxer(outVideoFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
int videoTrack = mediaMuxer.addTrack(mediaFormat);
mediaMuxer.start();
divideToOutputVideo(mediaExtractor, mediaMuxer, byteBuffer, mediaFormat, videoTrack, i);
mediaMuxer.stop();
mediaMuxer.release();
mediaMuxer = null;
Log.d(TAG, "divide video media to file -");
}
}
mediaExtractor.release();
mediaExtractor = null;
} catch (IOException e) {
e.printStackTrace();
}
}
主要通過一個MediaExtractor來引入Mp4原始檔,然後要分離的音訊和視訊檔案連線到各自的MediaMuxer物件,操作過程較為固定。
合成視訊檔案
合成視訊的過程和分離視訊的過程很類似
- 要輸出的MP4檔案和MediaMuxer關聯
- 根據要解析的音訊和視訊構建個字的MediaExtractor物件關聯
- 獲取兩個檔案各自的通道資訊
- MediaMuxer新增通道
- 開始解析幀資料
- 解析檔案兩幀資料的時間
- 解析寫入資料
- 釋放資源
主要程式碼如下:
public void combineVideo(File inputVideoFile, File inputAudioFile, File outputVideoFile) {
MediaExtractor videoExtractor = new MediaExtractor();
MediaExtractor audioExtractor = new MediaExtractor();
MediaMuxer mediaMuxer = null;
try {
mediaMuxer = new MediaMuxer(outputVideoFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// set data source
videoExtractor.setDataSource(inputVideoFile.getAbsolutePath());
audioExtractor.setDataSource(inputAudioFile.getAbsolutePath());
// get video or audio 取出視訊或音訊的訊號
int videoTrack = getTrack(videoExtractor, true);
int audioTrack = getTrack(audioExtractor, false);
// change to video oraudio track 切換道視訊或音訊訊號的通道
videoExtractor.selectTrack(videoTrack);
MediaFormat videoFormat = videoExtractor.getTrackFormat(videoTrack);
audioExtractor.selectTrack(audioTrack);
MediaFormat audioFormat = audioExtractor.getTrackFormat(audioTrack);
//追蹤此通道
int writeVideoIndex = mediaMuxer.addTrack(videoFormat);
int writeAudioIndex = mediaMuxer.addTrack(audioFormat);
mediaMuxer.start();
// 讀取寫入幀資料
writeSampleData(videoExtractor, mediaMuxer, writeVideoIndex, videoTrack);
writeSampleData(audioExtractor, mediaMuxer, writeAudioIndex, audioTrack);
} catch (IOException e) {
Log.w(TAG, "combineMedia ex", e);
} finally {
try {
if (mediaMuxer != null) {
mediaMuxer.stop();
mediaMuxer.release();
}
if (videoExtractor != null) {
videoExtractor.release();
}
if (audioExtractor != null) {
audioExtractor.release();
}
} catch (Exception e) {
Log.w(TAG, "combineMedia release ex", e);
}
}
}