承接上一章节分析:【六】Android MediaPlayer整体架构源码分析 -【start请求播放处理流程】【Part 10】【02】
本系列文章分析的安卓源码版本:【Android 10.0 版本】
推荐涉及到的知识点:
Binder机制实现原理:Android C++底层Binder通信机制原理分析总结【通俗易懂】
ALooper机制实现原理:Android native层媒体通信架构AHandler/ALooper机制实现源码分析
Binder异常关闭监听:Android native层DeathRecipient对关联进程(如相关Service服务进程)异常关闭通知事件的监听实现源码分析
【此章节小节编号将重新排序】
handleAnOutputBuffer(index, offset, size, timeUs, flags)实现分析:
处理编解码器输出Buffer可使用事件,消耗输出Buffer
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDecoder.cpp]
bool NuPlayer::Decoder::handleAnOutputBuffer(
size_t index,
size_t offset,
size_t size,
int64_t timeUs,
int32_t flags) {
if (mCodec == NULL) {
ALOGE("[%s] handleAnOutputBuffer without a valid codec", mComponentName.c_str());
handleError(NO_INIT);
return false;
}
// CHECK_LT(bufferIx, mOutputBuffers.size());
sp<MediaCodecBuffer> buffer;
// 获取MediaCodec中指定索引的对应Buffer
// 第1小节分析
mCodec->getOutputBuffer(index, &buffer);
if (buffer == NULL) {
ALOGE("[%s] handleAnOutputBuffer, failed to get output buffer", mComponentName.c_str());
handleError(UNKNOWN_ERROR);
return false;
}
if (index >= mOutputBuffers.size()) {
// 输出Buffer索引大于当前Decoder输出端口队列时
// for循环添加item到指定index位置的新的空数据item对象到该队列中
for (size_t i = mOutputBuffers.size(); i <= index; ++i) {
// 添加创建新的空数据item对象到该队列中
mOutputBuffers.add();
}
}
// 添加输出Buffer到指定index的空数据item中,并提交它
mOutputBuffers.editItemAt(index) = buffer;
// 设置数据访问有效范围
buffer->setRange(offset, size);
// 清除原有旧Buffer格式配置信息
buffer->meta()->clear();
// 设置PTS
buffer->meta()->setInt64("timeUs", timeUs);
// 是否为eos状态Buffer
bool eos = flags & MediaCodec::BUFFER_FLAG_EOS;
// 注:解码器时不关注这两种Buffer数据【CODECCONFIG or SYNCFRAME】
// we do not expect CODECCONFIG or SYNCFRAME for decoder
// 创建【kWhatRenderBuffer】渲染Buffer事件应答消息
sp<AMessage> reply = new AMessage(kWhatRenderBuffer, this);
// 设置Buffer index
reply->setSize("buffer-ix", index);
// 设置当前Buffer代数值【代数值早前章节阐述过,不再阐述】
reply->setInt32("generation", mBufferGeneration);
// 设置该输出Buffer大小
reply->setSize("size", size);
if (eos) {
// eos状态buffer时
ALOGV("[%s] saw output EOS", mIsAudio ? "audio" : "video");
// 设置eos字段为true并设置给Buffer和该消息中
buffer->meta()->setInt32("eos", true);
reply->setInt32("eos", true);
}
// 视频帧总数目递增【只记录视频帧数】
// 备注:若为音频软解码器那么该值将不会增加,默认为0
mNumFramesTotal += !mIsAudio;
if (mSkipRenderingUntilMediaTimeUs >= 0) {
// 跳过渲染直到指定媒体时间戳的媒体时间点大于0时【mSkipRenderingUntilMediaTimeUs见前面相关分析】
// 也就是说它是支持seek到非关键帧即指定时间点才允许播放的处理流程判断条件,该值一般为seek的媒体时间戳
if (timeUs < mSkipRenderingUntilMediaTimeUs) {
// 若当前解码输出帧PTS小于seek允许播放媒体时间点时
// 丢弃该帧数据即不应该显示
ALOGV("[%s] dropping buffer at time %lld as requested.",
mComponentName.c_str(), (long long)timeUs);
// 立即发送【kWhatRenderBuffer】渲染Buffer事件应答消息
// 备注:关于该事件处理流程将会在后续涉及到时统一分析
reply->post();
if (eos) {
// 若是eos状态Buffer时
// 有必要时将通知恢复播放完成事件【注意:其实它最终将会通知上层APP该seek操作完成事件】
// 第2小节分析
notifyResumeCompleteIfNecessary();
// isDiscontinuityPending见前面已有分析,true表示输出格式发生变化事件
if (mRenderer != NULL && !isDiscontinuityPending()) {
// 渲染器不为空并且输出格式未发生变化时
// 通知渲染器该EOS事件
// 第3小节分析
mRenderer->queueEOS(mIsAudio, ERROR_END_OF_STREAM);
}
}
// 此处就直接返回,不进入下面的【mRenderer->queueBuffer】真正渲染处理,
// 因此此处理可能会造成若解码速度过慢导致该if条件一直成立的话就会导致较长时间的黑屏卡顿现象
return true;
}
// 当前解码输出帧PTS大于seek允许播放媒体时间点时
// 重置为-1,此时允许该帧进行渲染处理
mSkipRenderingUntilMediaTimeUs = -1;
} else if ((flags & MediaCodec::BUFFER_FLAG_DATACORRUPT) &&
AVNuUtils::get()->dropCorruptFrame()) {
// 该帧Buffer标记位为损坏数据并且允许drop丢弃损坏数据时
// 备注:AVNuUtils类的功能实现早前章节分析过,是平台私有实现库,默认空实现直接返回false
ALOGV("[%s] dropping corrupt buffer at time %lld as requested.",
mComponentName.c_str(), (long long)timeUs);
// 立即发送【kWhatRenderBuffer】渲染Buffer事件应答消息
// 备注:关于该事件处理流程将会在后续涉及到时统一分析
reply->post();
return true;
}
// 有必要时将通知恢复播放完成事件【注意:其实它最终将会通知上层APP该seek操作完成事件】
// 第2小节分析
// wait until 1st frame comes out to signal resume complete
notifyResumeCompleteIfNecessary();
if (mRenderer != NULL) {
// 渲染器不为空时
// send the buffer to renderer.
// 发送该渲染帧Buffer给渲染器渲染处理
// 第4小节分析
mRenderer->queueBuffer(mIsAudio, buffer, reply);
if (eos && !isDiscontinuityPending()) {
// 该帧为eos状态Buffer并且输出Buffer格式未改变时
// 通知渲染器该EOS事件
// 第3小节分析
mRenderer->queueEOS(mIsAudio, ERROR_END_OF_STREAM);
}
}
return true;
}
1、mCodec->getOutputBuffer(index, &buffer)实现分析:
根据指定输出Buffer索引获取MediaCodec输出端口队列中对应buffer,一个一个输出Buffer的获取
注意:此处传入的buffer为智能指针对象的地址(即指针类型),而不是实际Buffer对象指针,因此不为null的。
// [frameworks/av/media/libstagefright/MediaCodec.cpp]
status_t MediaCodec::getOutputBuffer(size_t index, sp<MediaCodecBuffer> *buffer) {
sp<AMessage> format;
// 获取指定index的输入Buffer及其格式配置信息
// 见前面已有分析
return getBufferAndFormat(kPortIndexOutput, index, buffer, &format);
}
2、notifyResumeCompleteIfNecessary()实现分析:
有必要时将通知恢复播放完成事件
【备注:其实它最终将会判断是否应该通知上层APP该seek操作完成事件】
关于seek流程此处不再分析,请查看此前已有章节分析:其实际它就是seek流程源码的分析。
Android MediaPlayer在seek视频时可能会黑屏卡顿好几秒且进度条不动但有声音播放的问题源码解析
3、mRenderer->queueEOS(mIsAudio, ERROR_END_OF_STREAM)实现分析:
通知渲染器该EOS事件
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::queueEOS(bool audio, status_t finalResult) {
CHECK_NE(finalResult, (status_t)OK);
// 创建【kWhatQueueEOS】事件消息
sp<AMessage> msg = new AMessage(kWhatQueueEOS, this);
// 设置音频或视频的入队列代数值,见下面实现
msg->setInt32("queueGeneration", getQueueGeneration(audio));
// 设置是否为音频和eos最终结果状态码
msg->setInt32("audio", static_cast<int32_t>(audio));
msg->setInt32("finalResult", finalResult);
msg->post();
}
getQueueGeneration(audio)实现:
实际就是返回全局计数的音频或视频的入队列代数值
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
int32_t NuPlayer::Renderer::getQueueGeneration(bool audio) {
Mutex::Autolock autoLock(mLock);
return (audio ? mAudioQueueGeneration : mVideoQueueGeneration);
}
【kWhatQueueEOS】事件消息NuPlayerRenderer接收处理:
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::onMessageReceived(const sp<AMessage> &msg) {
switch (msg->what()) {
case kWhatQueueEOS:
{
onQueueEOS(msg);
break;
}
}
}
onQueueEOS(msg)实现分析:
入队列EOS事件消息执行,即添加EOS消息事件处理流程
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::onQueueEOS(const sp<AMessage> &msg) {
int32_t audio;
CHECK(msg->findInt32("audio", &audio));
// 判断是否由于该Buffer位旧数据而应该drop丢弃该Buffer,
// 返回true则丢弃它,实际上啥事没干,直接return。
// 见下面分析
if (dropBufferIfStale(audio, msg)) {
return;
}
int32_t finalResult;
// 获取该输出Buffer错误码
CHECK(msg->findInt32("finalResult", &finalResult));
// 队列实体结构
// 该类是在Renderer中使用的记录渲染输出Buffer相关信息,见下面声明
QueueEntry entry;
// 当前渲染帧Buffer数据读取偏移量初始化为0
entry.mOffset = 0;
// 记录错误码
entry.mFinalResult = finalResult;
if (audio) {
// 音频渲染时
// 加锁访问【为什么音频此处需要加锁而视频不需要,原因是音频除了软解码器输出渲染流程
// 还有硬解码器offload模式的Callback回调方式获取音频渲染帧数据】
Mutex::Autolock autoLock(mLock);
// 备注:高版本中mSyncQueues始终为false
if (mAudioQueue.empty() && mSyncQueues) {
// 音频渲染帧队列为空并且同步入队列为true时,不关注同步执行流程。
syncQueuesDone_l();
}
// 添加当前音频队列实体到音频渲染帧队列中
mAudioQueue.push_back(entry);
// 执行消耗音频队列处理
// 该部分见将会在后续正常渲染流程中涉及分析
postDrainAudioQueue_l();
} else {
// 视频渲染时
if (mVideoQueue.empty() && getSyncQueues()) {
// 不关注同步执行流程。
Mutex::Autolock autoLock(mLock);
syncQueuesDone_l();
}
// 添加当前视频队列实体到视频渲染帧队列中
mVideoQueue.push_back(entry);
// 执行消耗视频队列处理
// 该部分见将会在后续正常渲染流程中涉及分析
postDrainVideoQueue();
}
}
dropBufferIfStale(audio, msg)实现分析:
判断是否由于该Buffer位旧数据而应该drop丢弃该Buffer,返回true则丢弃它,实际上啥事没干
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
bool NuPlayer::Renderer::dropBufferIfStale(
bool audio, const sp<AMessage> &msg) {
int32_t queueGeneration;
// 获取队列代数值
CHECK(msg->findInt32("queueGeneration", &queueGeneration));
// 判断对应音频或视频全局代数值是否在AMessage得到处理之间过程中被修改了,
// 未修改则返回false表示有效Buffer
if (queueGeneration == getQueueGeneration(audio)) {
return false;
}
// 被修改了即当前Buffer是旧数据了
sp<AMessage> notifyConsumed;
if (msg->findMessage("notifyConsumed", ¬ifyConsumed)) {
// 获取该消息中传递的已消耗该Buffer完成通知事件消息,并立即发送通知
// 根据前面分析可知,在EOS时该通知为空
notifyConsumed->post();
}
// 返回true,即无效buffer
return true;
}
QueueEntry队列实体结构声明:
该类是在Renderer中使用的记录渲染输出Buffer相关信息
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.h]
// 注:如果mBuffer不为空,它是一个包含实际负载数据Buffer,否则如果mNotifyConsumed为空,
// 它是EOS状态Buffer。否则最终它是一个标记需要重打开不同格式的音频AudioSink。
// if mBuffer != nullptr, it's a buffer containing real data.
// else if mNotifyConsumed == nullptr, it's EOS.
// else it's a tag for re-opening audio sink in different format.
struct QueueEntry {
// 实际负载数据Buffer
sp<MediaCodecBuffer> mBuffer;
// 该Buffer元数据信息
sp<AMessage> mMeta;
// 已消耗该Buffer完成通知事件消息
sp<AMessage> mNotifyConsumed;
// 当前渲染帧Buffer数据读取偏移量
size_t mOffset;
// 当前Buffer的错误码
status_t mFinalResult;
// Buffer序号
int32_t mBufferOrdinal;
};
4、mRenderer->queueBuffer(mIsAudio, buffer, reply)实现分析:
发送该渲染帧Buffer给渲染器渲染处理
备注:第三个参数reply为Decoder接收的【kWhatRenderBuffer】已渲染Buffer事件应答消息。
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::queueBuffer(
bool audio,
const sp<MediaCodecBuffer> &buffer,
const sp<AMessage> ¬ifyConsumed) {
// 创建【kWhatQueueBuffer】发送渲染帧数据事件消息
sp<AMessage> msg = new AMessage(kWhatQueueBuffer, this);
// 获取并设置音频或视频的入队列代数值
msg->setInt32("queueGeneration", getQueueGeneration(audio));
// 设置是否为音频、及其负载buffer
// 和已消耗该Buffer完成通知事件消息即【kWhatRenderBuffer】已渲染Buffer事件应答消息
msg->setInt32("audio", static_cast<int32_t>(audio));
msg->setObject("buffer", buffer);
msg->setMessage("notifyConsumed", notifyConsumed);
msg->post();
}
NuPlayerRenderer接收【kWhatQueueBuffer】渲染帧数据事件消息处理:
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::onMessageReceived(const sp<AMessage> &msg) {
switch (msg->what()) {
case kWhatQueueBuffer:
{
onQueueBuffer(msg);
break;
}
}
}
onQueueBuffer(msg)实现分析:
接收(入队列添加)渲染帧Buffer到对应音频或视频渲染队列中
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::onQueueBuffer(const sp<AMessage> &msg) {
int32_t audio;
// 获取是否音频标志
CHECK(msg->findInt32("audio", &audio));
// 见前面分析,主要就是判断参数中代数值和当前全局变量代数值是否一致,不一致表示应该丢弃该Buffer
if (dropBufferIfStale(audio, msg)) {
return;
}
if (audio) {
// 音频时设置有音频标志
mHasAudio = true;
} else {
// 否则设置有视频标志
mHasVideo = true;
}
if (mHasVideo) {
// 有视频时
if (mVideoScheduler == NULL) {
// 若为空则创建视频帧调度调整器,
// 早前章节中分析过它,不再阐述,并且由早前章节设置视频帧率时已创建了该对象
mVideoScheduler = new VideoFrameScheduler();
mVideoScheduler->init();
}
}
sp<RefBase> obj;
// 获取负载数据Buffer并强转
CHECK(msg->findObject("buffer", &obj));
sp<MediaCodecBuffer> buffer = static_cast<MediaCodecBuffer *>(obj.get());
sp<AMessage> notifyConsumed;
// 获取参数已消耗该Buffer完成通知事件消息即【kWhatRenderBuffer】已渲染Buffer事件应答消息
CHECK(msg->findMessage("notifyConsumed", ¬ifyConsumed));
// 创建队列实体结构对象,并进行相应初始化
QueueEntry entry;
entry.mBuffer = buffer;
entry.mNotifyConsumed = notifyConsumed;
// 访问数据偏移量
entry.mOffset = 0;
// 追踪结果状态码为OK
entry.mFinalResult = OK;
// 记录递增的当前帧Buffer已入队列序号
entry.mBufferOrdinal = ++mTotalBuffersQueued;
if (audio) {
// 音频时加锁访问音频渲染队列【注意视频队列不需要加锁】
Mutex::Autolock autoLock(mLock);
// 添加到音频渲染队列中
mAudioQueue.push_back(entry);
// 执行消耗音频队列处理
// 见4.1小节分析
postDrainAudioQueue_l();
} else {
// 视频时
// 添加到视频帧渲染队列中
mVideoQueue.push_back(entry);
// 执行消耗视频队列处理
// 见4.2小节分析
postDrainVideoQueue();
}
// 加锁访问
Mutex::Autolock autoLock(mLock);
// 注意由此前【mSyncQueues】该全局变量阐述可知,高版本中它始终为false,
// 也就是说当前版本实现为异步渲染,而非同步渲染处理流程了。
// 因此下面处理不关注
if (!mSyncQueues || mAudioQueue.empty() || mVideoQueue.empty()) {
return;
}
sp<MediaCodecBuffer> firstAudioBuffer = (*mAudioQueue.begin()).mBuffer;
sp<MediaCodecBuffer> firstVideoBuffer = (*mVideoQueue.begin()).mBuffer;
if (firstAudioBuffer == NULL || firstVideoBuffer == NULL) {
// EOS signalled on either queue.
syncQueuesDone_l();
return;
}
int64_t firstAudioTimeUs;
int64_t firstVideoTimeUs;
CHECK(firstAudioBuffer->meta()
->findInt64("timeUs", &firstAudioTimeUs));
CHECK(firstVideoBuffer->meta()
->findInt64("timeUs", &firstVideoTimeUs));
int64_t diff = firstVideoTimeUs - firstAudioTimeUs;
ALOGV("queueDiff = %.2f secs", diff / 1E6);
if (diff > 100000LL) {
// Audio data starts More than 0.1 secs before video.
// Drop some audio.
(*mAudioQueue.begin()).mNotifyConsumed->post();
mAudioQueue.erase(mAudioQueue.begin());
return;
}
syncQueuesDone_l();
}
4.1、postDrainAudioQueue_l()实现分析:
执行消耗音频队列处理
参数delayUs默认为0,但注意该值还是会在自动循环写入数据时不为0
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::postDrainAudioQueue_l(int64_t delayUs) {
// 注意此处的【mUseAudioCallback】使用Audio回调Callback模式参数
// 就是指此前未展开分析的offload音频播放模式为true时的标记位,
// 因此若软音频解码器时该值为false,也就是前面关于该offload模式流程的阐述中说到若是offload模式,
// 将不会执行该方法的音频数据流推送去播放,而是通过AudioSinK来主动拉取该音频渲染队列Buffer。
// 【mSyncQueues】该值始终为false。
// 【mDrainAudioQueuePending】标志位表示当前是否已有正在执行该流程的处理,
// 有的话将忽略此处处理,因为不需要多次发起音频buffer消费
if (mDrainAudioQueuePending || mSyncQueues || mUseAudioCallback) {
return;
}
if (mAudioQueue.empty()) {
// 音频队列为空时不处理
return;
}
// 若已暂停,等待AudioTrack stop()完成后再传递数据
// FIXME: if paused, wait until AudioTrack stop() is complete before delivering data.
if (mPaused) {
// 暂停状态时
// mPauseDrainAudioAllowedUs:可在暂停状态下允许输出/发送音频的时间戳,在当前实现中始终为0
// 因此此处计算得到系统已开机时长的负值
const int64_t diffUs = mPauseDrainAudioAllowedUs - ALooper::GetNowUs();
if (diffUs > delayUs) {
// 因此目前此处将不会执行
delayUs = diffUs;
}
}
// 标记待消耗音频队列标志位为true,也就是当前已有发起该处理流程,上面不会再次进入执行
mDrainAudioQueuePending = true;
// 创建【kWhatDrainAudioQueue】消耗音频队列事件,并设置当前音频消费代数值,延迟delayUs时长后发送该消息执行
sp<AMessage> msg = new AMessage(kWhatDrainAudioQueue, this);
msg->setInt32("drainGeneration", mAudioDrainGeneration);
msg->post(delayUs);
}
【kWhatDrainAudioQueue】消耗音频队列事件处理:
备注:关于AudioSink相关功能,不会源码分析,它属于另外的技术领域了,因此只会阐述它的大致工作原理
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::onMessageReceived(const sp<AMessage> &msg) {
switch (msg->what()) {
case kWhatDrainAudioQueue:
{
// 该事件已被接收处理,因此重置该标志位为false
mDrainAudioQueuePending = false;
int32_t generation;
// 获取消息发送时传入的音频消耗代数值
CHECK(msg->findInt32("drainGeneration", &generation));
// 判断和当前全局代数值是否一致,不一致的话则丢弃当前渲染请求,注意不是丢弃音频Buffer
if (generation != getDrainGeneration(true /* audio */)) {
break;
}
// 执行消耗音频队列处理流程
// 见下面分析
// 备注:该方法返回值通过分析可知,
// 表示是否需要重新安排(调度)另一次写入音频数据处理流程,true则需要
if (onDrainAudioQueue()) {
// true需要时
uint32_t numFramesPlayed;
// 获取已播放帧数
if (mAudioSink->getPosition(&numFramesPlayed) != OK) {
// 失败则不处理
ALOGE("Error in time stamp query, return from here.\
Fillbuffer is called as part of session recreation");
break;
}
// 获取成功时
// 注:在flush后立即调用start时处理AudioTrack播放速度问题。
// Handle AudioTrack race when start is immediately called after flush.
// 计算AudioSink中待播放写入帧总数
uint32_t numFramesPendingPlayout =
(mNumFramesWritten > numFramesPlayed ?
mNumFramesWritten - numFramesPlayed : 0);
// This is how long the audio sink will have data to
// play back.
// 延迟时长:表示AudioSink待播放数据的总时长
int64_t delayUs =
mAudioSink->msecsPerFrame()
* numFramesPendingPlayout * 1000LL;
if (mPlaybackRate > 1.0f) {
// 播放速率大于1时即快速播放时
// 除以播放速率得到最终延迟时长
delayUs /= mPlaybackRate;
}
// Let's give it more data after about half that time
// has elapsed.
// 注:在该时间大约流逝一半后才给它更多的音频数据
delayUs /= 2;
// check the buffer size to estimate maximum delay permitted.
// 注:检查buffer大小以估算允许的最大延迟时长
// 最大消耗(写入数据)延迟时长:从AudioSink中获取,否则默认为0.5秒
const int64_t maxDrainDelayUs = std::max(
mAudioSink->getBufferDurationInUs(), (int64_t)500000 /* half second */);
// 如果此时delayUs大于maxDrainDelayUs,则表示存在较长delay延迟。但目前没考虑处理它
ALOGD_IF(delayUs > maxDrainDelayUs, "postDrainAudioQueue long delay: %lld > %lld",
(long long)delayUs, (long long)maxDrainDelayUs);
// 加锁
Mutex::Autolock autoLock(mLock);
// 再次执行该方法,但此处传入了Buffer写入延迟时长
postDrainAudioQueue_l(delayUs);
}
break;
}
}
}
getDrainGeneration(true /* audio */)实现:
加锁返回当前对应全局代数值
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
int32_t NuPlayer::Renderer::getDrainGeneration(bool audio) {
Mutex::Autolock autoLock(mLock);
return (audio ? mAudioDrainGeneration : mVideoDrainGeneration);
}
onDrainAudioQueue()实现分析:
由于本章节接下来内容篇幅过长,因此必须放入另一章节分析,请查看:
【六】Android MediaPlayer整体架构源码分析 -【start请求播放处理流程】【Part 12】【02】