Android视频开发进阶(part3-Android的Media API)

上两期咱们已经学习了关于视频播放的基础知识,还有容器格式文件的结构。那么今天终于能够开始学习安卓平台的视频播放知识了!相信你们早就已经等不及了。 java

可是千万不要小看以前两章的基础知识,理解他们对咱们接下来学习安卓平台的Codec API大有益处。若是没有理解以前的章节最好仍是仔细再复习一遍。。。。:smile:git


这一章咱们会如此安排github

1.Android 平台视频播放API的变迁历史数组

2.Android 新 Media API的使用app

3.一个使用新Media API播放视频的例子ide

1.Android 平台视频播放API的变迁历史

在好久好久之前。。。。oop

咳咳咳,言重了。。。。post

在2012年之前,安卓平台的视频播放,一直都是很是简单的事情(对于大部分开发者来讲,由于大部分开发者都不须要深刻底层MediaPlayer Service),学习

很是简单,建立播放器对象,注入URL,播放,播放完毕以后release。。。。。google

这给安卓开发者带来了很是大的便利,应用代码也很是少。能够说,在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等等。

以上的图都是从Google IO大会的视频进行截图而来的。咱们能够从结构图里看出,原来的MediaPlayer把Extractor,和Codec API所有封锁在了Framework层,应用层彻底接触不到。在新的API设计里面,这些都挪到了应用层(其实虽然MediaCodec API,就是编解码API还在Framework,可是应用层能够调用他们)


2.Android Codec API的使用

在全新的Media API里面,最最最重要的就是MediaExtractor和MediaCodec这两个类,第一个能够对容器文件进行读取控制,第二个就是对数据进行编解码的API。

MediaExtractor

MediaExtractor能够同一个URL,获取容器文件的轨道数量,轨道信息(Track)。在肯定了轨道信息以后,能够选择想要解码的轨道(只能选择一个,因此音轨和视频轨道须要两个不一样MediaExtractor给两个不一样MediaCodec解码),再从该轨道不停的读取数据放入MediaCodec API进行解码。

MediaCodec

MediaCodec API则是建立的时候就须要选择Codec的类型。而后编码的时候须要安卓平台显示视频的Surface,MediaCrypto对象(若是视频被加密的话,这个细节我会在DRM章节介绍)。

一个MediaCodec在建立以后会在内部维护两个对列(Queue),一个是InputQueue,一个是OutputQueue。相似生产者消费者的模式,MediaCodec会不停的从InputQueue获取数据(InputQueue的数据又是又MediaExtractor提供),解码,再把解码以后的数据放入OutputQueue,再提供给Surface让其视频内容。

这两个类协做的方式以下图


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!

part4-自适应视频播放

相关文章
相关标签/搜索