MediaPlayer的生命周期和缓冲策略 (源码篇)

概述

最近打算对公司的播放器进行优化.那么作为一个Android开发人员,Android自带的MediaPlayer本身具有很好的借鉴意义。MediaPlayer其实只是播放器在java层包的一层壳,具体的实现由评分机制决定,而在Android 7 之后Google官方移除了AwesomePlayer,故NuPlayer成为了大多数场景下播放器的底层实现。本文主要针对NuPlayer的生命周期、buffering策略以及拉取、消费多媒体数据进行讲解、学习。

必备的知识体系

在讲述原理之前,需要确保具有以下知识点的储备:

  1. MediaCodec、ACodec ,NuPlayer的编解码通过MediaCodec实现,而MediaCodec其实是Android在OpenMax基础上又封装了一层的ACodec实现的。ACodec是MediaCodec与OpenMax沟通的桥梁,MediaCodec通过ACodec发送命令给OpenMax,而ACodec负责接收事件回调并通知MediaCodec.
  2. MediaPlayer,相对于NuPlayer在c++层的实现,MediaPlayer在java层供用户使用。换句话说,我们可以通过MediaPlayer的状态转换去了解NuPlayer在对应阶段的行为和过程,因为java的native方法跟c++方法是一一映射的
  3. ALooper、AMessage, Google在c++层封装了一套类似Android Looper的架构,用于维持消息队列和派发消息。

MediaPlayer的api

  1. setDataSource ,设置数据源
  2. prepare,在调用MediaPlayer::start之前必须调用此函数,使得MediaPlayer进入prepared状态
  3. start,开始播放

MediaPlayer的setDataSource(path: String)流程

  1. 协议判断 : java层的MediaPlayer会判断path数据源是否属于file协议,如果不是则创建MediaHTTPService的binder,随后作为参数调用c++层的setDataSource函数。
  2. 播放器创建: 在MediaPlayer::setDataSource(c++层)的调用中,会先通过评分机制创建播放器,此处通常返回NU_PLAYER类型。即创建NuPlayerDriver.
  3. GenericSource: GenericSource作为在c++层对数据源的包装。当数据源为文件时,GenericSource会持有文件描述符fd,而当数据源通过http或者https协议拉取时,GenericSource会持有MediaHTTPService的BpBinder
    在这里插入图片描述

MediaPlayer::prepare的调用流程

在这里插入图片描述
在MediaPlayer::prepare的调用流程中,NuPlayer在背后给我们完成了以下几件事:

  1. 创建MediaHTTPConnection: 当设置数据源对应http协议时,会创建java层的实例。该类主要通过HttpURLConnection来获取InputStream,从而读取数据.
  2. 创建NuCachedSource2: 该类成员mLooper是ALooper类的实例,通过不断发送AMessage来驱使NuCachedSource2进行多媒体资源的数据读取.(如果是http协议,则是通过调用MediaHTTPConnection的函数完成读取数据操作),同时通过调整mFetching标志来限制读取行为等。
  3. 创建MediaExtractor: 举例来说,mp4文件其实是封装后的文件数据,本身具备一定的数据格式,所以协议通过MediaExtractor来进行demux。同时,GenericSource在调用initDataSource的过程中,会根据mime类型创建对应的MediaExtractor子类实例,比如mp4文件对应的就是MPEG4Extractor.随后再通过extractor获取媒体得时长、track数量等。

NuCachedSource2的成员变量:

  1. mCacheOffset: 用来标志当前媒体数据的缓冲起始位置。
  2. mCache->totalSize: 已缓冲的大小
  3. mFetching: 用于标志当前是否在进行缓冲
  4. mLastAccessPos: 最近访问的位置

NuCachedSource2的缓冲行为:
  1. 可以导致NuCachedSource2进行buffering的行为大致分为两种:a).解码器需要读取某个媒体位置的数据,但是缓冲无法cover.随后,NuCachedSource2设置mFetching = true,开始进行新的缓冲 b).NuCachedSource2的自主行为。在MediaPlayer::prepare阶段,GenericSource通过调用NuCachedSource2::Create创建该类实例,随后NuCachedSource发送kWhatFetchMore消息至自身的ALooper成员,驱使读取行为。
  2. buffering的上下限:GenericSource本身会根据媒体数据的比特率以及缓冲数据大小算出目前的缓冲时长。随后,跟kLowWaterMarkUs(2s)和kHighWaterMarkUs(5s)比较,当缓冲时长少于2s时,会继续进行buffering行为,当缓冲时长高于5s后会自动停止。

以下为GenericSource缓冲行为的源码实现(基于Android7.0.0):

 void NuPlayer::GenericSource::BufferingMonitor::onPollBuffering_l() {
    status_t finalStatus = UNKNOWN_ERROR;
    int64_t cachedDurationUs = -1ll;
    ssize_t cachedDataRemaining = -1;
    
    //仅适合WVME格式
    if (mWVMExtractor != NULL) {
      ...
    } else if (mCachedSource != NULL) { //mCachedSource -> NuCachedSource2
        //cachedDataRemaining -> mCacheOffset + mCache->totalSize() - lastBytePosCached
        cachedDataRemaining =
                mCachedSource->approxDataRemaining(&finalStatus);

        if (finalStatus == OK) {
            off64_t size;
            int64_t bitrate = 0ll;
            if (mDurationUs > 0 && mCachedSource->getSize(&size) == OK) {
                // |bitrate| uses bits/second unit, while size is number of bytes.
                bitrate = size * 8000000ll / mDurationUs;
            } else if (mBitrate > 0) {
                bitrate = mBitrate;
            }
            //如果比特率有效,计算缓冲时长
            if (bitrate > 0) {
                cachedDurationUs = cachedDataRemaining * 8000000ll / bitrate;
            }
        }
    }
	//获取缓存字节状态异常
    if (finalStatus != OK) {
        if (finalStatus == ERROR_END_OF_STREAM) {//EOS
            notifyBufferingUpdate_l(100);
        }
        stopBufferingIfNecessary_l();
        return;
    } else if (cachedDurationUs >= 0ll) {
        if (mDurationUs > 0ll) {
            int64_t cachedPosUs = getLastReadPosition_l() + cachedDurationUs;
            int percentage = 100.0 * cachedPosUs / mDurationUs;
            if (percentage > 100) {
                percentage = 100;
            }
			//通知缓冲进度
            notifyBufferingUpdate_l(percentage);
        }

        //kLowWaterMarkUs-> 2s,当cachedDurationUs少于2s时会继续拉取媒体资源数据
        if (cachedDurationUs < kLowWaterMarkUs) {
            // Take into account the data cached in downstream components to try to avoid
            // unnecessary pause.
            if (mOffloadAudio && mFirstDequeuedBufferRealUs >= 0) {
                int64_t downStreamCacheUs = mlastDequeuedBufferMediaUs - mFirstDequeuedBufferMediaUs
                        - (ALooper::GetNowUs() - mFirstDequeuedBufferRealUs);
                if (downStreamCacheUs > 0) {
                    cachedDurationUs += downStreamCacheUs;
                }
            }

            if (cachedDurationUs < kLowWaterMarkUs) {
                startBufferingIfNecessary_l();
            }
        } else {
            //如果当前在buffering, mPrepareBuffering == true
            //kHighWaterMarkUs -> 5s, kHighWaterMarkRebufferUs -> 15s
            int64_t highWaterMark = mPrepareBuffering ? kHighWaterMarkUs : kHighWaterMarkRebufferUs;
            if (cachedDurationUs > highWaterMark) {
                //如果缓冲时长够了,则停止buffering行为(为流量考虑吧?)
                stopBufferingIfNecessary_l();
            }
        }
    } else if (cachedDataRemaining >= 0) {
        if (cachedDataRemaining < kLowWaterMarkBytes) {
            startBufferingIfNecessary_l();
        } else if (cachedDataRemaining > kHighWaterMarkBytes) {
            stopBufferingIfNecessary_l();
        }
    }

    //该消息主要用于重入该函数
    schedulePollBuffering_l();
}

MediaPlayer::start的调用流程

在这里插入图片描述

NuPlayer所做的主要工作:

  1. readBuffer: 通过GenericSource读取媒体资源的数据,读取后的数据通过调用MediaExtractor解析生成MediaBuffer。同时如果缓冲字节低于lowLimit的话开启NuCachedSource2的缓冲行为。由此可见,数据拉取由NuCachedSource2进行,之后这些在特定封装格式下的数据由MediaExtractor进行解析(此处并不是解码,比如mp4文件本身由moov、mdat等结构,这些必须通过extractor抽离出数据)。
  2. onPollingBuffer_l: NuCachedSource2缓冲行为的主体,在该函数中会判断是否继续buffering,同时会通过schedulePollBuffering_l发送消息使得ALooper经过延迟再反复循环调用onPollingBuffer_l。
  3. instantiateDecoder: 初始化NuPlayerDecoder以及创建、配置、启动MediaCodec
  4. MediaCodec: 解码器,比如H264视频数据可以通过它解出yuv等原始数据。当NuPlayer创建并配置好MediaCodec实例之后,会由MediaCodec通过ACodec创建OpenMax,并把实际的解码工作交由OpenMax实现。
  5. NuPlayerRender: 用于控制解码后的媒体数据的渲染过程、音视频同步(NuPlayer以MediaClock时间为基准),具体的渲染工作比如视频可分为软件绘制和硬件绘制,交由ACodec负责。

NuPlayerRender的音视频同步:

  1. MediaClock: 外部时钟,持有mAnchorTimeMediaUs(此刻音频轨正在播放的帧的pts)、mAnchorTimeRealUs(mAnchorTimeMediaUs换算成系统时间的值)、nowUs(当前系统时间)三个成员变量。
  2. delayUs: 时延,每次render获取到新的解码数据后会进行音频轨和视频轨的渲染。拿音频来说,解码后的音频数据会通过AudioOutput写到音频设备进行输出。如果一次写入行为无法cover所有的解码音频轨数据,则需要进行长度为delayUs的时延。时延过后继续解码完成的音频数据的写入。

delayUs = (nextMediaRealTime - nowUs) / 2, nextMediaRealTime -> 下一个解码数据pts换算成系统时间的值,nowUs -> 当前系统时间

  1. drainVideoQueue: 视频的同步则相对简单些,在drainVideoQueue的函数调用中,会检查当前渲染的视频帧的pts,会将pts和nowUs(当前系统时间)进行比较。如果pts晚于nowUs,则进行(nowUs - pts)的延时

MediaCodec的生命周期

  • MediaCodec的生命周期由三个init、configure、start三个阶段组成,分别对应INITIALIZED、CONFIGURED、STARTED三个状态。
  • ACodec: MediaCodec并不会直接与OpenMax进行交流,主要的原因是因为CodecBase的实现类同时有ACodec和MediaFilter,从而将与OpenMax交接的职责划分给了CodecBase接口去实现,而MediaCodec与ACodec则通过AMessage进行沟通。
  • OMX: ACodec并不直接持有OMX实例,而是通过OMXClient与MediaPlayerService端持有的omx实例发送参数,属于cs架构(总之,跟binder传参是一致的~)。随后,再由omx实例完成OpenMax架构的创建、配置。

在这里插入图片描述

在MediaCodec的生命周期中,主要做了以下三件事:

  • 通过ACodec创建OMXNodeInstance实例,并由ACodec的CodecObserver监听OMX的事件回调
  • 配置ACodec
  • 通过ACodec分配解码的buffer以及端口,并且在ACodec完成OMX的启动之后,会先向MediaCodec提交一次OMX的解码数据,之后循坏调用postFillThisBuffer来通知MediaCodec不断填充待解码的媒体数据

MediaCodec如何与GenericSource发生联系:

  1. 实例引用: 在NuPlayer::start的函数调用中,会完成MediaCodec实例的初始化、配置等生命周期
  2. demux数据的生产者: GenericSource通过MediaExtractor解析出不含文件格式的多媒体数据后,存放在Track::source::mPackets的数据结构中,供MediaCodec解码
  3. demux数据的消费者: MediaCodec的职责是解码,当其完成初始化之后,会等待ACodec发送kWhatFillThisBuffer的消息,随后将Track::source::mPackets填充至buffer中,再通过ACodec发送命令给OpenMax进行解码

ACodec的事件驱动:

  • allocateNode: ACodec通过调用IOMX::allocateNode完成OMXNodeInstance的创建,同时在传参时传入了实现CodecObserver的监听函数,从而可以根据OMX的解码状态实时的驱动自身行为,并通知MediaCodec响应。
  • 状态:ACodec具有ExecutingToIdleStateIdleToLoadedStateLoadedState等状态,而这些状态都会监听OMX的事件回调,当一次OMX底层执行命令成功后,往往ACodec会在回调中进行状态切换
  • emptyBuffer: ACodec要求OMX对buffer进行一次消费行为。当OMX解码完成后,会通过消息回调通知ACodec,随后ACodec得以再次调用postFillThisBuffer请求MediaCodec完成待解码数据的填充。当MediaCodec启动之后,这个待解码数据的填充、消费也是最主要的循环过程,而这个过程由OMX的事件进行驱动。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章