转载

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的生命周期和缓冲策略(源码篇)

MediaPlayer::prepare的调用流程

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

在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的成员变量:

mCacheOffset
mCache->totalSize
mFetching
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的调用流程

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

NuPlayer所做的主要工作:

readBuffer
onPollingBuffer_l
instantiateDecoder
MediaCodec
NuPlayerRender

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架构的创建、配置。
MediaPlayer的生命周期和缓冲策略(源码篇)

在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的事件进行驱动。
原文  https://juejin.im/post/5da192545188257d0a6cf1bf
正文到此结束
Loading...