Android音視頻學(xué)習(xí): MediaCodec 硬編解碼

官方文檔 https://developer.android.google.cn/reference/android/media/MediaCodec

MediaCodec 是做硬件(GPU,充分利用GPU 的并行處理能力)編解碼的。(通常結(jié)合 MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack 使用)

codec (即 encoder + decoder ) :編解碼器
MediaExtractor:解封裝
MediaMuxer: 混合器(封裝)(音視頻合成)
MediaSync: 音視頻同步
MediaCrypto: 加密

原始音頻數(shù)據(jù) (PCM )和 視頻幀壓縮編碼(aac + h.264 等 )后要封裝到一個(gè)容器(MP4 等)中進(jìn)行傳播。同理播放器播放前要先解封裝,取出里面的音頻部分和視頻部分,然后解碼成硬件可以直接播放和渲染的音頻流和視頻流。由于音頻流和視頻流是分別播放和渲染的,所以這里就有音視頻同步的問(wèn)題。

jiagou.png

codec 處理輸入數(shù)據(jù)產(chǎn)生輸出數(shù)據(jù)。它通過(guò)輸入緩沖集合和輸出緩沖集合異步的處理數(shù)據(jù)。先請(qǐng)求一個(gè)空的 input buffer,然后填充上要處理的數(shù)據(jù)發(fā)送給 codec 處理。 codec 處理數(shù)據(jù)后會(huì)把結(jié)果寫(xiě)到一個(gè)空的 output buffer 中。最后你請(qǐng)求 output buffer 從里面讀出處理后的數(shù)據(jù)就行了。output buffer 用完后釋放回 codec 重新使用。

數(shù)據(jù)類(lèi)型

codec 處理3種類(lèi)型的數(shù)據(jù), compressed data (待解碼的數(shù)據(jù) 或 編碼后的數(shù)據(jù))、raw audio data (待編碼或解碼后的數(shù)據(jù))和 raw video data (待編碼或解碼后的數(shù)據(jù))。3種數(shù)據(jù)類(lèi)型都可以用 ByteBuffers 處理。還可以用 Surface 來(lái)處理 raw video data 來(lái)提高性能。因?yàn)?Surface 可以直接使用 native video buffers (在 native 層分配的 buffer)而不需要映射或拷貝到 ByteBuffers (ByteBuffers 是分配在 JVM 堆中的緩沖區(qū)) 中。

狀態(tài)

state.png

概念上主要包含 Stopped、Executing、Released 3種狀態(tài)。Stopped 包含 Configured、Uninitialized、Error 3個(gè)子狀態(tài)。 Executing 包含 Flushed、Running、End of Stream 3個(gè)子狀態(tài)。

codec 實(shí)例化以后默認(rèn)是 Uninitialized 狀態(tài),之后需要調(diào)用 configure 進(jìn)入 Configured 狀態(tài),再調(diào)用 start 進(jìn)入 Executing 狀態(tài)。運(yùn)行狀態(tài)默認(rèn)是 Flushed, 這時(shí)可以調(diào)用 dequeueInputBuffer 拿到一個(gè) input buffer 開(kāi)始處理數(shù)據(jù),進(jìn)入 Running 狀態(tài)。當(dāng)沒(méi)有輸入后需要寫(xiě)一個(gè) end-of-stream marker 的標(biāo)志(可以放在最后一個(gè) Input buffer 中,也可以用一個(gè)單獨(dú)的空 buffer,空 buffer 的 timestamp 會(huì)被忽略)。當(dāng) End of stream 后 codec 就不再接受輸入了, 但仍然繼續(xù)產(chǎn)生輸出直到輸出 buffer 遇到 end-of-stream marker 標(biāo)志(這個(gè)標(biāo)志是 codec 寫(xiě)的,前提是當(dāng)沒(méi)有輸入時(shí)你必須給 input buffer 寫(xiě)一個(gè) end-of-stream 的標(biāo)志。這個(gè)標(biāo)志可以作為 codec 處理完畢的標(biāo)志)。在運(yùn)行狀態(tài)可以調(diào)用 flush() 方法回到 Flushed 狀態(tài)。

運(yùn)行時(shí)調(diào)用 stop 方法會(huì)重新回到 Uninitialized ,這時(shí)要重新 configue 、start 才能重新運(yùn)行。 運(yùn)行出錯(cuò)時(shí)會(huì)進(jìn)入 Error 狀態(tài),這時(shí)可以調(diào)用 reset 方法恢復(fù)到 Uninitialized。 codec 不再使用時(shí)調(diào)用 release 方法釋放資源進(jìn)入 Released 狀態(tài)。

創(chuàng)建

5.0 之后官方推薦用 MediaCodecList.findDecoderForFormat 傳入一個(gè) MediaFormat 來(lái)查找你要使用的 codec。 然后調(diào)用 MediaCodec.createByCodecName(String) 方法創(chuàng)建 codec。

MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 1);
mediaFormat.setString(MediaFormat.KEY_BIT_RATE, null);
MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
String name = mediaCodecList.findEncoderForFormat(mediaFormat);
Log.d(TAG, "name is " + name); // OMX.google.aac.encoder
try {
    mediaCodec = MediaCodec.createByCodecName(name);
} catch (IOException e) {
    e.printStackTrace();
}

也可以調(diào)用 MediaCodec.createDecoder/EncoderByType(String) 傳入要處理數(shù)據(jù)的 MIME type 來(lái)創(chuàng)建。

try {
    // 5.0 之前可以這樣寫(xiě),aac 編解碼一般都支持
    mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
} catch (IOException e) {
    e.printStackTrace();
}

初始化

調(diào)用 configue 方法,如果需要異步處理 buffer 可以先調(diào)用 setCallback 方法設(shè)置回調(diào)。

數(shù)據(jù)處理

輸入輸出 buffer 是用 buffer-ID 來(lái)標(biāo)識(shí)的。調(diào)用 start 后通過(guò) dequeueInput/OutputBuffer(…) 方法拿到一個(gè) buffer。異步模式需要在 MediaCodec.Callback.onInput/OutputBufferAvailable(…) 回調(diào)中拿到 buffer。
拿到輸出 buffer 的數(shù)據(jù)處理完畢后要調(diào)用 releaseOutputBuffer 將 buffer 釋放回 codec 中。

輸入輸出 buffer 用完后都要及時(shí)提交到/釋放回 codec 。畢竟 codec 的 buffer 數(shù)量是有限的,如果占滿(mǎn)了,肯定就沒(méi)法處理了。輸入 buffer 被占滿(mǎn)后 dequeueInputBuffer 會(huì)一直返回 -1, 輸出 buffer 占滿(mǎn)后 dequeueOutputBuffer 會(huì)一直返回 -1

5.0 之后官方推薦以異步方式處理 buffer

 MediaCodec codec = MediaCodec.createByCodecName(name);
 MediaFormat mOutputFormat; // member variable
 codec.setCallback(new MediaCodec.Callback() {
   @Override
   void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
     // 拿到一個(gè)輸入 buffer -> 填充數(shù)據(jù) ->入隊(duì)交給 codec 處理
     ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }

   @Override
   void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
     // 拿出一個(gè) codec 處理完的輸出 buffer -> 處理 -> 釋放
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is equivalent to mOutputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   }

   @Override
   void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     mOutputFormat = format; // option B
   }

   @Override
   void onError(…) {
     …
   }
 });
 codec.configure(format, …);
 mOutputFormat = codec.getOutputFormat(); // option B
 codec.start();
 // wait for processing to complete
 codec.stop();
 codec.release();

同步處理方式(不推薦)

MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 codec.stop();
 codec.release();

同步方式使用 ByteBuffer 數(shù)組獲取 buffer (已廢棄)

MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(…);
   if (inputBufferId >= 0) {
     // fill inputBuffers[inputBufferId] with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     // outputBuffers[outputBufferId] is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
     outputBuffers = codec.getOutputBuffers();
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     MediaFormat format = codec.getOutputFormat();
   }
 }
codec.stop();
codec.release();

如果需要兼容 4.X 版本,還得用上面的方法。

End-of-stream Handling

當(dāng)?shù)竭_(dá)輸入末尾時(shí),在調(diào)用 queueInputBuffer 時(shí)要在參數(shù)里面寫(xiě)一個(gè) BUFFER_FLAG_END_OF_STREAM 的標(biāo)志,表示輸入完畢了。標(biāo)志可以寫(xiě)在最后一個(gè) buffer 中,也可以最后再專(zhuān)門(mén)提交一個(gè)空的 buffer (沒(méi)有可用數(shù)據(jù))。如果用空 buffer ,buffer 的 timestamp 會(huì)被忽略。

輸入結(jié)束后 codec 就不再接受輸入了,但會(huì)繼續(xù)產(chǎn)生輸出。輸出處理完畢后也會(huì)在最后一個(gè)有用 buffer 或 空buffer 中含有 end-of-stream 的標(biāo)志??梢杂眠@個(gè)標(biāo)志來(lái)標(biāo)識(shí) codec 處理完畢。通過(guò) MediaCodec.BufferInfo 可以拿到 buffer 的 flag。

發(fā)出 end-of-stream 的 buffer 后就不要再提交 Input buffer 了。除非 codec 被 flushed, or stopped and restarted。

Using an Output Surface

codec 的輸出也可以直接關(guān)聯(lián)到一個(gè) Surface 上。如視頻解碼后可以直接渲染到 SurfaceView 上。但這時(shí) output buffers 就不可用了。getOutputBuffer/Image(int) 會(huì)返回 null。getOutputBuffers() 也會(huì)返回一個(gè)全是 null 的數(shù)組。

你可以選擇是否直接把輸出渲染到 Surface 上。

  • Do not render the buffer: Call releaseOutputBuffer(bufferId, false).
  • Render the buffer with the default timestamp: Call releaseOutputBuffer(bufferId, true).
  • Render the buffer with a specific timestamp: Call releaseOutputBuffer(bufferId, timestamp).

Using an Input Surface

也可以用 Surface 作為 codec 的輸入,同理這時(shí) input buffer 就不可用了,調(diào)用 dequeueInputBuffer 會(huì)拋異常。

調(diào)用 signalEndOfInputStream() 后 surface 會(huì)停止向 codec 發(fā)送數(shù)據(jù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容