初始MediaCodec

來自專欄音視頻技術

MediaCodec是一個Codec,通過硬體加速解碼和編碼。它為晶元廠商和應用開發者搭建了一個統一介面。MediaCodec幾乎是所有安卓播放器硬解的標配,要深入分析開源播放器的源碼,如ijkplayer的源碼 ,有必要先了解其基礎的使用方法。

MediaCodec API : developer.android.google.cn

供參考的中文翻譯:jianshu.com/p/2721548cb

下面我們從硬解播放一個視頻講解如何使用MediaCodec。(以下講解中用到的完整工程代碼在:github.com/compilelife/)

MediaExtractor

在播放視頻前,需先解封裝,MediaExtractor可以負責解封裝。

extractor = new MediaExtractor();extractor.setDataSource("/sdcard/test.mp4");dumpFormat(extractor);private void dumpFormat(MediaExtractor extractor) { int count = extractor.getTrackCount(); Log.i(TAG, "playVideo: track count: " + count); for (int i = 0; i < count; i++) { MediaFormat format = extractor.getTrackFormat(i); Log.i(TAG, "playVideo: track " + i + ":" + getTrackInfo(format)); } }

這裡我們打開sdcard下的一個測試視頻,然後列印其軌道信息。軌道信息的遍歷可以通過MediaExtractor的getTrackCountgetTrackFormat配合完成。

getTrackFormat的返回值是一個MediaFormat類型,這裡列印部分信息:

private String getTrackInfo(MediaFormat format) { String info = format.getString(MediaFormat.KEY_MIME); if (info.startsWith("audio/")) { info += " samplerate: "+ format.getInteger(MediaFormat.KEY_SAMPLE_RATE) + ", channel count:" + format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); } else if (info.startsWith("video/")){ info += " size:" + format.getInteger(MediaFormat.KEY_WIDTH) + "x" + format.getInteger(MediaFormat.KEY_HEIGHT); } return info; }

樣例輸出如下(測試視頻不同,輸出不同):

I/MainActivity: playVideo: track count: 2I/MainActivity: playVideo: track 0:video/avc size:1920x1080I/MainActivity: playVideo: track 1:audio/mp4a-latm samplerate: 48000, channel count:2

可以看出,這是一個H264(avc)的1080P視頻,包含一條48K採樣的雙通道音頻。

MediaCodecList

MediaCodec是一個統一API,支持不同編碼格式,在創建MediaCodec的時候需要根據視頻編碼選擇合適的解碼器。這是通過MediaCodec.createByCodecName完成的。

然而不同廠商提供的解碼器名稱有所不同,編寫通用播放器的時候,無法預見。

所以Android API中提供了一個MediaCodecList用於枚舉設備支持的編解碼器的名字、能力,以查找合適的編解碼器。

我們先枚舉下設備支持的解碼器:

private void displayDecoders() { MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);//REGULAR_CODECS參考api說明 MediaCodecInfo[] codecs = list.getCodecInfos(); for (MediaCodecInfo codec : codecs) { if (codec.isEncoder()) continue; Log.i(TAG, "displayDecoders: "+codec.getName()); } }

不同設備支持的類型不同,這裡以紅米6為例(節選):

displayDecoders: OMX.google.aac.decoder displayDecoders: OMX.MTK.AUDIO.DECODER.ALAC displayDecoders: OMX.MTK.AUDIO.DECODER.RAW displayDecoders: OMX.MTK.VIDEO.DECODER.AVC displayDecoders: OMX.google.h264.decoder displayDecoders: OMX.MTK.VIDEO.DECODER.HEVC displayDecoders: OMX.google.hevc.decoder displayDecoders: OMX.MTK.VIDEO.DECODER.VP9 displayDecoders: OMX.google.vp9.decoder

紅米6是MTK晶元,MTK提供的解碼器以OMX.MTK打頭。還有OMX.google打頭的一些軟解器。這裡為瞭解碼H264的視頻,我們應該以「OMX.MTK.VIDEO.DECODER.AVC」來創建MediaCodec。

相比直接寫死晶元相關的名稱,更合理的方法是這樣的:

MediaFormat selTrackFmt = chooseVideoTrack(extractor);codec = createCodec(selTrackFmt, surface);private MediaFormat chooseVideoTrack(MediaExtractor extractor) { int count = extractor.getTrackCount(); for (int i = 0; i < count; i++) { MediaFormat format = extractor.getTrackFormat(i); if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")){ extractor.selectTrack(i);//選擇軌道 return format; } } return null; } private MediaCodec createCodec(MediaFormat format, Surface surface) throws IOException{ MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); MediaCodec codec = MediaCodec.createByCodecName(codecList.findDecoderForFormat(format)); codec.configure(format, surface, null, 0); return codec; }

目標軌道的選擇需要根據具體的節目和業務決定,這裡我們簡單選擇第一條視頻軌道。軌道選擇後,從MediaExtractor那裡取到了目標軌道的MediaFormat,就可以通過codecList.findDecoderForFormat(format)獲取到最合適的解碼器了。

Synchronous Mode

根據文檔的介紹,有兩種使用方法——同步和非同步。

先看同步的用法。同步的主流程是在一個循環內不斷調用dequeueInputBuffer -> queueInputBuffer填充數據 -> dequeueOutputBuffer -> releaseOutputBuffer顯示畫面

與MediaExtractor配合,給MediaCodec喂數據的一般流程是這樣的:

if (inIndex >= 0) { ByteBuffer buffer = codec.getInputBuffer(inIndex); int sampleSize = extractor.readSampleData(buffer, 0); if (sampleSize < 0) { codec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); isEOS = true; } else { long sampleTime = extractor.getSampleTime(); codec.queueInputBuffer(inIndex, 0, sampleSize, sampleTime, 0); extractor.advance(); } }

通過MediaExtractor的readSampleDataadvance我們可以從視頻文件中不斷取到所選軌道的未解碼數據。

當sampleSize大於等於0,說明取到了數據,通過queueInputBuffer通知MediaCodec inIndex的input buffer已經準備好了。如果返回值小於0,說明已經到的文件末尾,此時可以通過BUFFER_FLAG_END_OF_STREAM標記通知MediaCodec已經達到文件末尾。

從MediaCodec取解碼後的數據(實際拿到的是一個Index),一般流程是這樣的:

int outIndex = codec.dequeueOutputBuffer(info, 10000); Log.i(TAG, "run: outIndex="+outIndex); switch (outIndex) { case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED: Log.i(TAG, "run: new format: "+codec.getOutputFormat()); break; case MediaCodec.INFO_TRY_AGAIN_LATER: Log.i(TAG, "run: try later"); break; case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED: Log.i(TAG, "run: output buffer changed"); break; default: codec.releaseOutputBuffer(outIndex, true); break; } if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { Log.i(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM"); break; }

dequeueOutputBuffer從MediaCodec中獲取已經解碼好的一幀的索引,再通過releaseOutputBuffer(outIndex, true)就可以讓MediaCodec將這一幀輸出到Surface上。(如果想控制播放速率,或丟幀,可以在這裡控制)

dequeueOutputBuffer還會在特殊情況返回一些輔助值,如視頻格式變化、或解碼數據未準備好等。

運行代碼可以看到類似如下輸出:

outIndex=2outIndex=6outIndex=-1try lateroutIndex=-1try lateroutIndex=3outIndex=4outIndex=9outIndex=7outIndex=-1try later

可以看到try later會頻繁列印,這是因為dequeueOutputBuffer調用過於頻繁,解碼數據來不及準備。

Asynchronous Mode

非同步的代碼如下:

codec.setCallback(new MediaCodec.Callback() { @Override public void onInputBufferAvailable(MediaCodec codec, int index) { ByteBuffer buffer = codec.getInputBuffer(index); int sampleSize = extractor.readSampleData(buffer, 0); if (sampleSize < 0) { codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); } else { long sampleTime = extractor.getSampleTime(); codec.queueInputBuffer(index, 0, sampleSize, sampleTime, 0); extractor.advance(); } } @Override public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) { codec.releaseOutputBuffer(index, true); } @Override public void onError(MediaCodec codec, MediaCodec.CodecException e) { Log.e(TAG, "onError: "+e.getMessage()); } @Override public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { Log.i(TAG, "onOutputFormatChanged: "+format); } }); codec.start();

非同步播放的方式是給MediaCodec註冊了一個回調,由MediaCodec通知何時input buffer可用,何時output buffer可用,何時format changed等。

我們需要做的就是,當inuput buffer可用時,就把MediaExtractor提取的數據填充給指定buffer;當output buffer可用時,決定是否顯示該幀(實際應用中,不會立即顯示它,而是要根據fps控制顯示速度)

其他

那麼是否可以開始寫自己的播放器了呢?不然。

還有很多的播放問題需要關註:視頻諸多封裝的兼容性、音視頻同步、播放速率控制、視頻詳細信息的獲取、csd配置、seek處理等。這些問題的處理往往依靠MediaExtractor和MediaCodec的組合是不足以處理的。

對於MediaCodec的工程級使用,可以參考VLC、IjkMediaPlayer、Kodi等優秀開源項目。


推薦閱讀:
相關文章