Android視訊開發進階(part3-Android的Media API)

qing的世界發表於2018-03-12

上兩期我們已經學習了關於視訊播放的基礎知識,還有容器格式檔案的結構。那麼今天終於可以開始學習安卓平臺的視訊播放知識了!相信大家早就已經等不及了。

Android視訊開發進階(part3-Android的Media API)

但是千萬不要小看之前兩章的基礎知識,理解他們對我們接下來學習安卓平臺的Codec API大有益處。如果沒有理解之前的章節最好還是仔細再複習一遍。。。。:smile:


這一章我們會如此安排

1.Android 平臺視訊播放API的變遷歷史

2.Android 新 Media API的使用

3.一個使用新Media API播放視訊的例子

1.Android 平臺視訊播放API的變遷歷史

在很久很久以前。。。。

咳咳咳,言重了。。。。

在2012年以前,安卓平臺的視訊播放,一直都是非常簡單的事情(對於大部分開發者來說,因為大部分開發者都不需要深入底層MediaPlayer Service),

Android視訊開發進階(part3-Android的Media API)

非常簡單,建立播放器物件,注入URL,播放,播放完畢之後release。。。。。

這給安卓開發者帶來了非常大的便利,應用程式碼也非常少。可以說,在2011年之前(尤其是直播業務還沒爆火之前),這款Native Player還是很好用的。

但是這款播放器的缺點也非常顯而易見。

1.很多格式的容器檔案不支援,也不支援自適應視訊播放(Adaptive Streaming)

2.應用開發者很難debug播放器,MediaPlayer的程式碼很多都是Native Method。並不在Java層。

3.很難做自定義的擴充和設定,比如緩衝的大小,下載進度等等。

正是因為MediaPlayer本身的實現對開發者是完全透明的,所以它也越來越神祕,也逐漸跟不上現在的業務對播放器的需求了。

所以谷歌也意識到了這一點,在2012年的Google IO大會上,谷歌宣佈了Android Jelly Bean,也就是4.3之後,安卓平臺release新的Media Codec API組。這些API不再像之前傻瓜式的MediaPlayer一樣,而是把API元件設計的面向視訊播放的更底層概念。比如,編解碼API,容器檔案讀取器Extractor API等等。

Android視訊開發進階(part3-Android的Media API)

以上的圖都是從Google IO大會的視訊進行截圖而來的。我們可以從結構圖裡看出,原來的MediaPlayer把Extractor,和Codec API全部封鎖在了Framework層,應用層完全接觸不到。在新的API設計裡面,這些都挪到了應用層(其實雖然MediaCodec API,就是編解碼API還在Framework,但是應用層可以呼叫他們)


2.Android Codec API的使用

在全新的Media API裡面,最最最重要的就是MediaExtractor和MediaCodec這兩個類,第一個可以對容器檔案進行讀取控制,第二個就是對資料進行編解碼的API。

MediaExtractor

Android視訊開發進階(part3-Android的Media API)

MediaExtractor可以同一個URL,獲取容器檔案的軌道數量,軌道資訊(Track)。在確定了軌道資訊之後,可以選擇想要解碼的軌道(只能選擇一個,所以音軌和視訊軌道需要兩個不同MediaExtractor給兩個不同MediaCodec解碼),再從該軌道不停的讀取資料放入MediaCodec API進行解碼。

MediaCodec

Android視訊開發進階(part3-Android的Media API)

MediaCodec API則是建立的時候就需要選擇Codec的型別。然後編碼的時候需要安卓平臺顯示視訊的Surface,MediaCrypto物件(如果視訊被加密的話,這個細節我會在DRM章節介紹)。

一個MediaCodec在建立之後會在內部維護兩個對列(Queue),一個是InputQueue,一個是OutputQueue。類似生產者消費者的模式,MediaCodec會不停的從InputQueue獲取資料(InputQueue的資料又是又MediaExtractor提供),解碼,再把解碼之後的資料放入OutputQueue,再提供給Surface讓其視訊內容。

這兩個類協作的方式如下圖

Android視訊開發進階(part3-Android的Media API)


3.一個使用新Media API播放視訊的例子

那麼我們是時候看看原始碼了!我們這次使用的是谷歌一個非官方維護的開源專案,叫grafika。這個專案其實是一個Demo app,裡面使用新的Media API做了很多有意思的小例項。其中就包括我們這次要看的,使用MediaAPI播放視訊的例子。這裡只有三個方法,呼叫順序也是依次進行。

public void playWithUrl() throws IOException {
        MediaExtractor extractor = null;
        MediaCodec decoder = null;
        try {
            /**
             * 建立一個MediaExtractor物件
             */
            extractor = new MediaExtractor();
            /**
             * 設定Extractor的source,這裡可以把mp4的url傳進來,
             */
            extractor.setDataSource(context, Uri.parse(url),new HashMap<String, String>());
            /**
             * 這裡我們需要選擇我們要解析的軌道,我們在這個例子裡面只解析視訊軌道
             */
            int trackIndex = selectTrack(extractor);
            if (trackIndex < 0) {
                throw new RuntimeException("No video track found in " + url);
            }


            /**
             * 選擇視訊軌道的索引
             */
            extractor.selectTrack(trackIndex);

            /**
             * 獲取軌道的音視訊格式,這個格式和Codec有關,可以點選MediaFormat類看看有哪些
             */
            MediaFormat format = extractor.getTrackFormat(trackIndex);
            String mime = format.getString(MediaFormat.KEY_MIME);

            /**
             * 建立一個MediaCodec物件
             */
            decoder = MediaCodec.createDecoderByType(mime);
            /**
             * 設定格式,和視訊輸出的Surface,開始解碼
             */
            decoder.configure(format, mOutputSurface, null, 0);
            decoder.start();

            doExtract(extractor, trackIndex, decoder, mFrameCallback);
        }
        catch ( Exception e ){
            e.printStackTrace();
        }

        finally {
            // release everything we grabbed
            if (decoder != null) {
                decoder.stop();
                decoder.release();
                decoder = null;
            }
            if (extractor != null) {
                extractor.release();
                extractor = null;
            }
        }
    }
複製程式碼
 /**
     * 我們用Extractor獲取軌道數量,然後遍歷他們,只要找到第一個軌道是Video的就返回
     */
    private static int selectTrack(MediaExtractor extractor) {
        // Select the first video track we find, ignore the rest.
        int numTracks = extractor.getTrackCount();
        for (int i = 0; i < numTracks; i++) {
            MediaFormat format = extractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith("video/")) {
                if (VERBOSE) {
                    Log.d(TAG, "Extractor selected track " + i + " (" + mime + "): " + format);
                }
                return i;
            }
        }

        return -1;
    }
複製程式碼
 private void doExtract(MediaExtractor extractor, int trackIndex, MediaCodec decoder,
                           FrameCallback frameCallback) {
        final int TIMEOUT_USEC = 10000;
        /**
         * 獲取MediaCodec的輸入佇列,是一個陣列
         */
        ByteBuffer[] decoderInputBuffers = decoder.getInputBuffers();
        int inputChunk = 0;
        long firstInputTimeNsec = -1;

        boolean outputDone = false;
        boolean inputDone = false;
        /**
         * 用while做迴圈
         */
        while (!outputDone) {
            if (VERBOSE) Log.d(TAG, "loop");
            if (mIsStopRequested) {
                Log.d(TAG, "Stop requested");
                return;
            }

            // Feed more data to the decoder.
            /**
             * 不停的輸入資料知道輸入佇列滿為止
             */
            if (!inputDone) {
                /**
                 * 這個方法返回輸入佇列陣列可以放資料的位置,即一個索引
                 */
                int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
                /**
                 * 如果輸入佇列還有位置
                 */
                if (inputBufIndex >= 0) {
                    if (firstInputTimeNsec == -1) {
                        firstInputTimeNsec = System.nanoTime();
                    }
                    ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
                    // Read the sample data into the ByteBuffer.  This neither respects nor
                    // updates inputBuf's position, limit, etc.
                    /**
                     * 用Extractor讀取一個sample的資料,並且放入輸入佇列
                     */
                    int chunkSize = extractor.readSampleData(inputBuf, 0);
                    /**
                     * 如果chunk size是小於0,證明我們已經讀取完畢這個軌道的資料了。
                     */
                    if (chunkSize < 0) {
                        // End of stream -- send empty frame with EOS flag set.
                        decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
                                MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                        inputDone = true;
                        if (VERBOSE) Log.d(TAG, "sent input EOS");
                    }
                    else {
                        if (extractor.getSampleTrackIndex() != trackIndex) {
                            Log.w(TAG, "WEIRD: got sample from track " +
                                    extractor.getSampleTrackIndex() + ", expected " + trackIndex);
                        }
                        long presentationTimeUs = extractor.getSampleTime();
                        decoder.queueInputBuffer(inputBufIndex, 0, chunkSize,
                                presentationTimeUs, 0 /*flags*/);
                        if (VERBOSE) {
                            Log.d(TAG, "submitted frame " + inputChunk + " to dec, size=" +
                                    chunkSize);
                        }
                        inputChunk++;
                        /**
                         * Extractor移動一個sample的位置,下一次再呼叫extractor.readSampleData()就會讀取下一個sample
                         */
                        extractor.advance();
                    }
                } else {
                    if (VERBOSE) Log.d(TAG, "input buffer not available");
                }
            }

            if (!outputDone) {
                /**
                 * 開始把輸出佇列的資料拿出來,decodeStatus只要不是大於零的整數都是異常的現象,需要處理
                 */
                int decoderStatus = decoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
                if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                    // no output available yet
                    if (VERBOSE) Log.d(TAG, "no output from decoder available");
                } else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                    // not important for us, since we're using Surface
                    if (VERBOSE) Log.d(TAG, "decoder output buffers changed");
                } else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                    MediaFormat newFormat = decoder.getOutputFormat();
                    if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
                } else if (decoderStatus < 0) {
                    throw new RuntimeException(
                            "unexpected result from decoder.dequeueOutputBuffer: " +
                                    decoderStatus);
                } else { // decoderStatus >= 0
                    if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                        if (VERBOSE) Log.d(TAG, "output EOS");
                            outputDone = true;
                    }
                    boolean doRender = (mBufferInfo.size != 0);
                    if (doRender && frameCallback != null) {
                        frameCallback.preRender(mBufferInfo.presentationTimeUs);
                    }
                    /**
                     * 只要我們呼叫了decoder.releaseOutputBuffer(),
                     * 就會把輸出佇列的資料全部輸出到Surface上顯示,並且釋放輸出佇列的資料
                     */
                    decoder.releaseOutputBuffer(decoderStatus, doRender);
                }
            }
        }
    }
複製程式碼

當然,大家可能會有很多問題,比如,你說了可擴充性呢?Extractor不還是隻能讀取指定的格式?等等等等的問題。我會再接下來的幾章慢慢的講解,通過谷歌的開源播放器ExoPlayer,我們可以深入到如何使用,擴充這些API。下一章我會先講解自適應視訊的概念,然後會通過Exoplayer的例子來闡述如何使用Media API播放自適應視訊。

good night!

Android視訊開發進階(part3-Android的Media API)

part4-自適應視訊播放

相關文章