最近打算对公司的播放器进行优化.那么作为一个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 onPollingBuffer_l instantiateDecoder MediaCodec NuPlayerRender
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会在回调中进行状态切换