赞
踩
最近打算对公司的播放器进行优化.那么作为一个Android开发人员,Android自带的MediaPlayer本身具有很好的借鉴意义。MediaPlayer其实只是播放器在java层包的一层壳,具体的实现由评分机制决定,而在Android 7 之后Google官方移除了AwesomePlayer,故NuPlayer成为了大多数场景下播放器的底层实现。本文主要针对NuPlayer的生命周期、buffering策略以及拉取、消费多媒体数据进行讲解、学习。
在讲述原理之前,需要确保具有以下知识点的储备:
path
数据源是否属于file协议,如果不是则创建MediaHTTPService
的binder,随后作为参数调用c++层的setDataSource函数。MediaHTTPService
的BpBinder
在MediaPlayer::prepare的调用流程中,NuPlayer在背后给我们完成了以下几件事:
MediaHTTPConnection
: 当设置数据源对应http协议时,会创建java层的实例。该类主要通过HttpURLConnection来获取InputStream,从而读取数据.NuCachedSource2
: 该类成员mLooper是ALooper类的实例,通过不断发送AMessage来驱使NuCachedSource2进行多媒体资源的数据读取.(如果是http协议,则是通过调用MediaHTTPConnection的函数完成读取数据操作),同时通过调整mFetching标志来限制读取行为等。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(); }
NuPlayer所做的主要工作:
readBuffer
: 通过GenericSource读取媒体资源的数据,读取后的数据通过调用MediaExtractor解析生成MediaBuffer。同时如果缓冲字节低于lowLimit的话开启NuCachedSource2的缓冲行为。由此可见,数据拉取由NuCachedSource2进行,之后这些在特定封装格式下的数据由MediaExtractor进行解析(此处并不是解码,比如mp4文件本身由moov、mdat等结构,这些必须通过extractor抽离出数据)。onPollingBuffer_l
: NuCachedSource2缓冲行为的主体,在该函数中会判断是否继续buffering,同时会通过schedulePollBuffering_l发送消息使得ALooper经过延迟再反复循环调用onPollingBuffer_l。instantiateDecoder
: 初始化NuPlayerDecoder以及创建、配置、启动MediaCodecMediaCodec
: 解码器,比如H264视频数据可以通过它解出yuv等原始数据。当NuPlayer创建并配置好MediaCodec实例之后,会由MediaCodec通过ACodec创建OpenMax,并把实际的解码工作交由OpenMax实现。NuPlayerRender
: 用于控制解码后的媒体数据的渲染过程、音视频同步(NuPlayer以MediaClock时间为基准),具体的渲染工作比如视频可分为软件绘制和硬件绘制,交由ACodec负责。NuPlayerRender的音视频同步:
MediaClock
: 外部时钟,持有mAnchorTimeMediaUs(此刻音频轨正在播放的帧的pts)、mAnchorTimeRealUs(mAnchorTimeMediaUs换算成系统时间的值)、nowUs(当前系统时间)三个成员变量。delayUs
: 时延,每次render获取到新的解码数据后会进行音频轨和视频轨的渲染。拿音频来说,解码后的音频数据会通过AudioOutput写到音频设备进行输出。如果一次写入行为无法cover所有的解码音频轨数据,则需要进行长度为delayUs
的时延。时延过后继续解码完成的音频数据的写入。
delayUs = (nextMediaRealTime - nowUs) / 2
, nextMediaRealTime -> 下一个解码数据pts换算成系统时间的值,nowUs -> 当前系统时间
drainVideoQueue
: 视频的同步则相对简单些,在drainVideoQueue的函数调用中,会检查当前渲染的视频帧的pts,会将pts和nowUs(当前系统时间)进行比较。如果pts晚于nowUs,则进行(nowUs - pts)
的延时在MediaCodec的生命周期中,主要做了以下三件事:
MediaCodec如何与GenericSource发生联系:
NuPlayer::start
的函数调用中,会完成MediaCodec实例的初始化、配置等生命周期GenericSource
通过MediaExtractor解析出不含文件格式的多媒体数据后,存放在Track::source::mPackets的数据结构中,供MediaCodec解码MediaCodec
的职责是解码,当其完成初始化之后,会等待ACodec发送kWhatFillThisBuffer的消息,随后将Track::source::mPackets填充至buffer中,再通过ACodec发送命令给OpenMax进行解码ACodec的事件驱动:
ExecutingToIdleState
、IdleToLoadedState
、LoadedState
等状态,而这些状态都会监听OMX的事件回调,当一次OMX底层执行命令成功后,往往ACodec会在回调中进行状态切换Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。