博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android视频开发进阶(part3-Android的Media API)
阅读量:6371 次
发布时间:2019-06-23

本文共 9545 字,大约阅读时间需要 31 分钟。

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

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


这一章我们会如此安排

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

2.Android 新 Media API的使用

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

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

在很久很久以前。。。。

咳咳咳,言重了。。。。

在2012年以前,安卓平台的视频播放,一直都是非常简单的事情(对于大部分开发者来说,因为大部分开发者都不需要深入底层MediaPlayer Service),

非常简单,创建播放器对象,注入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等等。

以上的图都是从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播放视频的例子

那么我们是时候看看源代码了!我们这次使用的是谷歌一个非官方维护的开源项目,叫。这个项目其实是一个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
()); /** * 这里我们需要选择我们要解析的轨道,我们在这个例子里面只解析视频轨道 */ 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!

转载地址:http://fcyqa.baihongyu.com/

你可能感兴趣的文章
3D打印开源切片软件Cura配置步骤
查看>>
c++读取TXT文件内容
查看>>
EF Core使用CodeFirst在MySql中创建新数据库以及已有的Mysql数据库如何使用DB First生成域模型...
查看>>
[android] ndk环境的搭建
查看>>
Kafka集群搭建
查看>>
js表达式
查看>>
oracle的日期相减
查看>>
半正定矩阵
查看>>
C语言面试基本问题
查看>>
这不是一篇随笔
查看>>
vc写csv文件
查看>>
LaTeX 加粗
查看>>
Microsoft Dynamics CRM 2011 SDK 5.07版本已经发布
查看>>
Go使用Gob存储数据
查看>>
What Are You Talking About(字典树)
查看>>
sivlerlight系统类 关系大观
查看>>
VBA快速入门技巧
查看>>
<中国人聪明之道>读书笔记
查看>>
如何手工释放linux内存
查看>>
Sliverlight好教程
查看>>