Android音視訊處理之MediaCodec

DroidMind發表於2018-11-06
MediaCodec是Android中媒體編解碼器,可以對媒體進行編/解碼

MediaCodec採用同步/非同步方式處理資料,並且使用了一組輸入輸出快取(ByteBuffer)。通過請求一個空的輸入快取(ByteBuffer),向其中填充滿資料並將它傳遞給編解碼器處理。編解碼器處理完這些資料並將處理結果輸出至一個空的輸出快取(ByteBuffer)中。使用完輸出快取的資料之後,將其釋放回編解碼器

Android音視訊處理之MediaCodec

MediaCodec的生命週期有三種狀態:停止態-Stopped、執行態-Executing、釋放態-Released

停止狀態(Stopped)包括了三種子狀態:未初始化(Uninitialized)、配置(Configured)、錯誤(Error)。

執行狀態(Executing)會經歷三種子狀態:重新整理(Flushed)、執行(Running)、流結束(End-of-Stream)

Android音視訊處理之MediaCodec

(1)當建立了一個MediaCodec物件,此時MediaCodec處於Uninitialized狀態。

MediaCodec主要提供了createEncoderByType(String type)、createDecoderByType(String type)兩個方法來建立編解碼器,它們均需要傳入一個MIME型別多媒體格式。常見的MIME型別多媒體格式如下:

video/x-vnd.on2.vp8 - VP8 video (i.e. video in .webm) 
video/x-vnd.on2.vp9 - VP9 video (i.e. video in .webm) 
video/avc - H.264/AVC video 
video/mp4v-es - MPEG4 video 
video/3gpp - H.263 video 
audio/3gpp - AMR narrowband audio 
audio/amr-wb - AMR wideband audio 
audio/mpeg - MPEG1/2 audio layer III 
audio/mp4a-latm - AAC audio (note, this is raw AAC packets, not packaged in LATM!) 
audio/vorbis - vorbis audio 
audio/g711-alaw - G.711 alaw audio 
audio/g711-mlaw - G.711 ulaw audio 
複製程式碼

String AUDIO_MIME = "audio/mp4a-latm";
MediaCodec mAudioEncoder = MediaCodec.createEncoderByType(AUDIO_MIME);複製程式碼

MediaCodec還提供了一個createByCodecName (String name)方法,支援使用元件的具體名稱來建立編解碼器。但是該方法使用起來有些麻煩,且官方是建議最好是配合MediaCodecList使用,因為MediaCodecList記錄了所有可用的編解碼器。另外,我們也可以使用該類對傳入的minmeType引數進行判斷,以匹配出MediaCodec對該mineType型別的編解碼器是否支援。以指定MIME型別為"video/avc"為例,程式碼如下:

 private static MediaCodecInfo selectCodec(String mimeType) {
     // 獲取所有支援編解碼器數量
     int numCodecs = MediaCodecList.getCodecCount();
     for (int i = 0; i < numCodecs; i++) {
        // 編解碼器相關性資訊儲存在MediaCodecInfo中
         MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
         // 判斷是否為編碼器
         if (!codecInfo.isEncoder()) {
             continue;
         }
        // 獲取編碼器支援的MIME型別,並進行匹配
         String[] types = codecInfo.getSupportedTypes();
         for (int j = 0; j < types.length; j++) {
             if (types[j].equalsIgnoreCase(mimeType)) {
                 return codecInfo;
             }
         }
     }
     return null;
 }
複製程式碼

(2)建立完MediaCodec之後,需要使用configure(…)方法對MediaCodec進行配置,這時MediaCodec轉為Configured狀態。 

編解碼器配置使用的是MediaCodec的configure方法,在配置時,configure方法需要傳入format、surface、crypto、flags引數,其中format為MediaFormat的例項,它使用”key-value”鍵值對的形式儲存多媒體資料格式資訊;surface用於指明解碼器的資料來源來自於該surface;crypto用於指定一個MediaCrypto物件,以便對媒體資料進行安全解密;flags指明配置的是編碼器(CONFIGURE_FLAG_ENCODE)。

MediaFormat mFormat = MediaFormat.createVideoFormat("video/avc", 640 ,480);     // 建立MediaFormat
mFormat.setInteger(MediaFormat.KEY_BIT_RATE,600);       // 指定位元率
mFormat.setInteger(MediaFormat.KEY_FRAME_RATE,30);  // 指定幀率
mFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,mColorFormat);  // 指定編碼器顏色格式  
mFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,10); // 指定關鍵幀時間間隔
mVideoEncodec.configure(mFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
複製程式碼

(3)配置完MediaCodec之後,呼叫MediaCodec的start()方法使其轉入Executing狀態。

在呼叫start()方法後MediaCodec立即進入Flushed子狀態,此時MediaCodec會擁有所有的快取。一旦第一個輸入快取(input buffer)被移出佇列,MediaCodec就轉入Running子狀態,這種狀態佔據了MediaCodec的大部分生命週期。當你將一個帶有end-of-stream marker標記的輸入快取入佇列時,MediaCodec將轉入End-of-Stream子狀態。在這種狀態下,MediaCodec不再接收之後的輸入快取,但它仍然產生輸出快取直到end-of- stream標記輸出。你可以在Executing狀態的任何時候通過呼叫flush()方法返回到Flushed子狀態。

下面看看整個過程:

呼叫MediaCodec的start()方法,此時MediaCodec處於Executing狀態,可以通過getInputBuffers()方法和getOutputBuffers()方法獲取快取佇列:

mAudioEncoder.start();
mAudioInputBuffers = mAudioEncoder.getInputBuffers();
mAudioOutputBuffers = mAudioEncoder.getOutputBuffers();
複製程式碼

當MediaCodec處於Executing狀態之後就可以對資料進行處理了。

  • 首先通過dequeueInputBuffer(long timeoutUs)請求一個輸入快取,timeoutUs代表等待時間,設定為-1代表無限等待:

int inputBufIndex = mAudioEncoder.dequeueInputBuffer(1000);
複製程式碼

返回的整型變數為請求到的輸入快取的index,通過getInputBuffers()得到的是輸入快取陣列,通過index和輸入快取陣列可以得到當前請求的輸入快取,在使用之前要clear一下,避免之前的快取資料影響當前資料

mInputBuffer = mAudioInputBuffers[inputBufIndex];
mInputBuffer.clear();
複製程式碼

也可以直接通過mAudioEncoder.getInputBuffer(inputBufIndex)得到輸入快取

  • 接著就是把資料新增到輸入快取中,並呼叫queueInputBuffer(...)把快取資料入隊

mInputBuffer.put(bytes, 0, BUFFER_SIZE_IN_BYTES);
mAudioEncoder.queueInputBuffer(inputBufIndex, 0, BUFFER_SIZE_IN_BYTES, (1000000 * mEncodedSize / AUDIO_BYTE_PER_SAMPLE), 0);
複製程式碼

獲取輸出快取和獲取輸入快取類似

  • 首先通過dequeueOutputBuffer(BufferInfo info, long timeoutUs)來請求一個輸出快取,這裡需要傳入一個BufferInfo物件,用於儲存ByteBuffer的資訊

int outputBufIndex = mAudioEncoder.dequeueOutputBuffer(mOutBufferInfo, 1000);
複製程式碼

public final static class BufferInfo {
    public void set(
            int newOffset, int newSize, long newTimeUs, @BufferFlag int newFlags) {
        offset = newOffset;
        size = newSize;
        presentationTimeUs = newTimeUs;
        flags = newFlags;
    }
    public int offset // 偏移量
    public int size;    // 快取區有效資料大小
    public long presentationTimeUs; // 顯示時間戳
    public int flags;                   // 快取區標誌

    @NonNull
    public BufferInfo dup() {
        BufferInfo copy = new BufferInfo();
        copy.set(offset, size, presentationTimeUs, flags);
        return copy;
    }
};
複製程式碼

  • 然後通過返回的index得到輸出快取,並通過BufferInfo獲取ByteBuffer的資訊,我們可以得到當前資料是否Codec-specific Data:

mOutBuffer = mAudioOutputBuffers[outputBufIndex];
if ((mOutBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
    // Codec-specific Data, 這裡可以從ByteBuffer中獲取csd引數
    // audioFormat.setByteBuffer("csd-0", mOutBuffer);
} else {
    // 處理資料
}
mAudioEncoder.releaseOutputBuffer(outputBufIndex, false);
複製程式碼

也可以直接通過mAudioEncoder.getOutputBuffer(outputBufIndex)得到輸出快取

注意一定要呼叫releaseOutputBuffer方法。

通過呼叫stop()方法使MediaCodec返回到Uninitialized狀態,因此這個MediaCodec可以再次重新配置 。

當使用完MediaCodec後,必須呼叫release()方法釋放其資源。

在極少情況下MediaCodec會遇到錯誤並進入Error狀態。這個錯誤可能是在佇列操作時返回一個錯誤的值或者有時候產生了一個異常導致的。通過呼叫 reset()方法使MediaCodec再次可用。你可以在任何狀態呼叫reset()方法使MediaCodec返回到Uninitialized狀態。否則,呼叫 release()方法進入最終的Released狀態。

參考文章: 

https://www.jianshu.com/p/30e596112015 
https://blog.csdn.net/AndrExpert/article/details/79578149複製程式碼


相關文章