Android在MediaMuxer和MediaCodec用例 - audio+video

在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更合適)等。


 

這裏以一個非常無聊的功能爲例,就是在一個Surface上繪圖編碼生成視頻。同一時候用MIC錄音編碼生成音頻,然後將音視頻混合生成mp4文件。

程序本身沒什麼用。可是演示樣例了MediaMuxer和MediaCodec的基本使用方法。本程序主要是基於兩個測試程序:一個是Grafika中的SoftInputSurfaceActivity和HWEncoderExperiments。它們一個是生成視頻,一個生成音頻,這裏把它們結合一下,同一時候生成音頻和視頻。基本框架和流程例如以下:

首先是錄音線程,主要參考HWEncoderExperiments。

通過AudioRecord類接收來自麥克風的採樣數據,然後丟給Encoder準備編碼:

 

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

}

這裏也能夠設置AudioRecord的回調(通過setRecordPositionUpdateListener())來觸發音頻數據的讀取。offerAudioEncoder()裏主要是把audio採樣數據送入音頻MediaCodec的InputBuffer進行編碼:

 

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);
}

以下,參考Grafika-SoftInputSurfaceActivity,並增加音頻處理。

 

主循環大體分四部分:

 

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

發佈了5 篇原創文章 · 獲贊 43 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章