Android中MediaMuxer和MediaCodec用例
阿新 • • 發佈:2019-02-17
在Android的多媒體類中,MediaMuxer和MediaCodec算是比較年輕的,它們是JB 4.1和JB 4.3才引入的。前者用於將音訊和視訊進行混合生成多媒體檔案。缺點是目前只能支援一個audio track和一個video track,而且僅支援mp4輸出。不過既然是新生事物,相信之後的版本應該會有大的改進。MediaCodec用於將音視訊進行壓縮編碼,它有個比較牛X的地方是可以對Surface內容進行編碼,如KK 4.4中螢幕錄影功能就是用它實現的。
注意它們和其它一些多媒體相關類的關係和區別:MediaExtractor用於音視訊分路,和MediaMuxer正好是反過程。MediaFormat用於描述多媒體資料的格式。MediaRecorder用於錄影+壓縮編碼,生成編碼好的檔案如mp4, 3gpp,視訊主要是用於錄製Camera preview。MediaPlayer用於播放壓縮編碼後的音視訊檔案。AudioRecord用於錄製PCM資料。AudioTrack用於播放PCM資料。PCM即原始音訊取樣資料,可以用如vlc播放器播放。當然了,通道取樣率之類的要自己設,因為原始取樣資料是沒有檔案頭的,如:
vlc --demux=rawaud --rawaud-channels 2 --rawaud-samplerate 44100 audio.pcm
回到MediaMuxer和MediaCodec這兩個類,它們的參考文件見http://developer.android.com/reference/android/media/MediaMuxer.html和http://developer.android.com/reference/android/media/MediaCodec.html,裡邊有使用的框架。這個組合可以實現很多功能,比如音視訊檔案的編輯(結合MediaExtractor),用OpenGL繪製Surface並生成mp4檔案,螢幕錄影以及類似Camera app裡的錄影功能(雖然這個用MediaRecorder更合適)等。
注意這裡Muxer要等把audio track和video track都加入了再開始。MediaCodec在一開始呼叫dequeueOutputBuffer()時會返回一次INFO_OUTPUT_FORMAT_CHANGED訊息。我們只需在這裡獲取該MediaCodec的format,並註冊到MediaMuxer裡。接著判斷當前audio track和video track是否都已就緒,如果是的話就啟動Muxer。
總結來說,drainVideoEncoder()的主邏輯大致如下,drainAudioEncoder也是類似的,只是把video的MediaCodec換成audio的MediaCodec即可。
最後幾點注意:
1. 在AndroidManifest.xml里加上錄音許可權,否則建立AudioRecord物件時鐵定失敗:
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
2. 音視訊通過PTS同步,兩個的單位要一致。
3. MediaMuxer的使用要按照Constructor -> addTrack -> start -> writeSampleData -> stop 的順序。如果既有音訊又有視訊,在stop前兩個都要writeSampleData()過。
Code references:
Grafika: https://github.com/google/grafika
Bigflake: http://bigflake.com/mediacodec/
HWEncoderExperiments:https://github.com/OnlyInAmerica/HWEncoderExperiments/tree/audioonly/HWEncoderExperiments/src/main/java/net/openwatch/hwencoderexperiments
Android test:http://androidxref.com/4.4.2_r2/xref/cts/tests/tests/media/src/android/media/cts/
http://androidxref.com/4.4.2_r2/xref/pdk/apps/TestingCamera2/src/com/android/testingcamera2/CameraRecordingStream.java
注意它們和其它一些多媒體相關類的關係和區別:MediaExtractor用於音視訊分路,和MediaMuxer正好是反過程。MediaFormat用於描述多媒體資料的格式。MediaRecorder用於錄影+壓縮編碼,生成編碼好的檔案如mp4, 3gpp,視訊主要是用於錄製Camera preview。MediaPlayer用於播放壓縮編碼後的音視訊檔案。AudioRecord用於錄製PCM資料。AudioTrack用於播放PCM資料。PCM即原始音訊取樣資料,可以用如vlc播放器播放。當然了,通道取樣率之類的要自己設,因為原始取樣資料是沒有檔案頭的,如:
vlc --demux=rawaud --rawaud-channels 2 --rawaud-samplerate 44100 audio.pcm
回到MediaMuxer和MediaCodec這兩個類,它們的參考文件見http://developer.android.com/reference/android/media/MediaMuxer.html和http://developer.android.com/reference/android/media/MediaCodec.html,裡邊有使用的框架。這個組合可以實現很多功能,比如音視訊檔案的編輯(結合MediaExtractor),用OpenGL繪製Surface並生成mp4檔案,螢幕錄影以及類似Camera app裡的錄影功能(雖然這個用MediaRecorder更合適)等。
這裡以一個很無聊的功能為例,就是在一個Surface上畫圖編碼生成視訊,同時用MIC錄音編碼生成音訊,然後將音視訊混合生成mp4檔案。程式本身沒什麼用,但是示例了MediaMuxer和MediaCodec的基本用法。本程式主要是基於兩個測試程式:一個是Grafika中的SoftInputSurfaceActivity和HWEncoderExperiments。它們一個是生成視訊,一個生成音訊,這裡把它們結合一下,同時生成音訊和視訊。基本框架和流程如下:
首先是錄音執行緒,主要參考HWEncoderExperiments。通過AudioRecord類接收來自麥克風的取樣資料,然後丟給Encoder準備編碼:
這裡也可以設定AudioRecord的回撥(通過setRecordPositionUpdateListener())來觸發音訊資料的讀取。offerAudioEncoder()裡主要是把audio取樣資料送入音訊MediaCodec的InputBuffer進行編碼:AudioRecord audio_recorder; audio_recorder = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, buffer_size); // ... audio_recorder.startRecording(); while (is_recording) { byte[] this_buffer = new byte[frame_buffer_size]; read_result = audio_recorder.read(this_buffer, 0, frame_buffer_size); // read audio raw data // … presentationTimeStamp = System.nanoTime() / 1000; audioEncoder.offerAudioEncoder(this_buffer.clone(), presentationTimeStamp); // feed to audio encoder }
下面,參考Grafika-SoftInputSurfaceActivity,並加入音訊處理。主迴圈大體分四部分:ByteBuffer[] inputBuffers = mAudioEncoder.getInputBuffers(); int inputBufferIndex = mAudioEncoder.dequeueInputBuffer(-1); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; inputBuffer.clear(); inputBuffer.put(this_buffer); ... mAudioEncoder.queueInputBuffer(inputBufferIndex, 0, this_buffer.length, presentationTimeStamp, 0); }
try {
// Part 1
prepareEncoder(outputFile);
...
// Part 2
for (int i = 0; i < NUM_FRAMES; i++) {
generateFrame(i);
drainVideoEncoder(false);
drainAudioEncoder(false);
}
// Part 3
...
drainVideoEncoder(true);
drainAudioEncoder(true);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
} finally {
// Part 4
releaseEncoder();
}
第1部分是準備工作,除了video的MediaCodec,這裡還初始化了audio的MediaCodec:
MediaFormat audioFormat = new MediaFormat();
audioFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
...
mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME_TYPE);
mAudioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mAudioEncoder.start();
第2部分進入主迴圈,app在Surface上直接繪圖,由於這個Surface是從MediaCodec中用createInputSurface()申請來的,所以畫完後不用顯式用queueInputBuffer()交給Encoder。drainVideoEncoder()和drainAudioEncoder()分別將編碼好的音視訊從buffer中拿出來(通過dequeueOutputBuffer()),然後交由MediaMuxer進行混合(通過writeSampleData())。注意音視訊通過PTS(Presentation
time stamp,決定了某一幀的音視訊資料何時顯示或播放)來同步,音訊的time stamp需在AudioRecord從MIC採集到資料時獲取並放到相應的bufferInfo中,視訊由於是在Surface上畫,因此直接用dequeueOutputBuffer()出來的bufferInfo中的就行,最後將編碼好的資料送去MediaMuxer進行多路混合。注意這裡Muxer要等把audio track和video track都加入了再開始。MediaCodec在一開始呼叫dequeueOutputBuffer()時會返回一次INFO_OUTPUT_FORMAT_CHANGED訊息。我們只需在這裡獲取該MediaCodec的format,並註冊到MediaMuxer裡。接著判斷當前audio track和video track是否都已就緒,如果是的話就啟動Muxer。
總結來說,drainVideoEncoder()的主邏輯大致如下,drainAudioEncoder也是類似的,只是把video的MediaCodec換成audio的MediaCodec即可。
while(true) {
int encoderStatus = mVideoEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
...
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
encoderOutputBuffers = mVideoEncoder.getOutputBuffers();
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = mAudioEncoder.getOutputFormat();
mAudioTrackIndex = mMuxer.addTrack(newFormat);
mNumTracksAdded++;
if (mNumTracksAdded == TOTAL_NUM_TRACKS) {
mMuxer.start();
}
} else if (encoderStatus < 0) {
...
} else {
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
...
if (mBufferInfo.size != 0) {
mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);
}
mVideoEncoder.releaseOutputBuffer(encoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
break;
}
}
}
第3部分是結束錄製,傳送EOS資訊,這樣在drainVideoEncoder()和drainAudioEncoder中就可以根據EOS退出內迴圈。第4部分為清理工作。把audio和video的MediaCodec,MediaCodec用的Surface及MediaMuxer物件釋放。最後幾點注意:
1. 在AndroidManifest.xml里加上錄音許可權,否則建立AudioRecord物件時鐵定失敗:
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
2. 音視訊通過PTS同步,兩個的單位要一致。
3. MediaMuxer的使用要按照Constructor -> addTrack -> start -> writeSampleData -> stop 的順序。如果既有音訊又有視訊,在stop前兩個都要writeSampleData()過。
Code references:
Grafika: https://github.com/google/grafika
Bigflake: http://bigflake.com/mediacodec/
HWEncoderExperiments:https://github.com/OnlyInAmerica/HWEncoderExperiments/tree/audioonly/HWEncoderExperiments/src/main/java/net/openwatch/hwencoderexperiments
Android test:http://androidxref.com/4.4.2_r2/xref/cts/tests/tests/media/src/android/media/cts/
http://androidxref.com/4.4.2_r2/xref/pdk/apps/TestingCamera2/src/com/android/testingcamera2/CameraRecordingStream.java