根本原因:个别视频格式播放器【特别是video decoder软解码器】解码速度较慢导致的。
从源码分析原因就是:
一、当前视频支持seek到非关键帧,导致其seek位置前最近的一个关键帧开始到seek位置的视频帧数据将会被drop掉,并且延迟通知notifySeekCompleted事件给APP,直到解码数据到seek位置时才通知,但此时seek位置的视频帧播放时间已经错过了音频播放时间,延迟太长而被丢弃,一直解码出来的视频数据直到丢弃到当前音频播放时间点的4毫秒范围内,才认为是音视频同步播放数据,才可进行渲染,因此seek时遇到的黑屏卡顿是有两个阶段的卡顿原因:
1.1、seek位置前已解码的视频帧drop掉,直到seek位置才通知seek完成;
1.2、seek位置准备开始渲染但由于音视频渲染机制是以音频播放时间为基准,因此还是会丢弃延迟太长的视频帧,直到解码的视频帧时间符合音视频同步机制才可进行渲染播放。
二、而进度条不动的原因是:通知notifySeekCompleted事件的延迟导致上层每隔1秒调用getCurrentPosition方法时在NuPlayerDriver中会判断是否正在seeking,如果正在seeking就会直接返回seek的位置给到上层,直到该通知事件收到之后才会返回真正正在播放的时间点给到上层APP更新,也就导致了进度条不更新但声音的现象。
源码分析流程:【android 10.0版本】
1、seek调用流程为:
MediaPlayer.java的seekTo() ===> android_media_MediaPlayer.cpp的xxx_seekTo() ===> mediaplayer.cpp的seekTo() ===> NuPlayerDriver.cpp的seekTo() ===> NuPlayer.cpp的seekToAsync(),然后发送kWhatSeek事件给自己的工作线程的onMessageReceived去接收和处理,此事件调用的时候分两种情况处理:1、当前还未播放;2、当前正在播放。
此问题只分析当前正在播放时的流程,因为黑屏但有声音播放就是发生在正在播放流程中,并且其seek mode方式为设置了支持seek到非关键帧功能才会出现黑屏比较久。
2、源码分析:
2.1、第1小节中调用流程在NuPlayerDriver之前的处理均为透传调用,因此不分析,此处开始分析:
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDriver.cpp]
status_t NuPlayerDriver::seekTo(int msec, MediaPlayerSeekMode mode) {
ALOGV("seekTo(%p) (%d ms, %d) at state %d", this, msec, mode, mState);
Mutex::Autolock autoLock(mLock);
int64_t seekTimeUs = msec * 1000LL;
switch (mState) {
case STATE_PREPARED:
case STATE_STOPPED_AND_PREPARED:
case STATE_PAUSED:
case STATE_RUNNING:
{
// 设置此处的两个标记值
mAtEOS = false;
mSeekInProgress = true;
// seeks can take a while, so we essentially paused
// 注意这个通知从源码调用链分析可知对于上层APP是接收不到的,
// 只是给到了MediaPlayer内部类TimeProvider去处理了
notifyListener_l(MEDIA_PAUSED);
// 调用NuPlayer.seekToAsync方法
// 见第2.2小节分析
mPlayer->seekToAsync(seekTimeUs, mode, true /* needNotify */);
break;
}
default:
return INVALID_OPERATION;
}
// 若seek执行了,则记录当前seek时间点
mPositionUs = seekTimeUs;
return OK;
}
// 此处先分析下seeking状态时上层APP获取播放进度不变化的处理:
status_t NuPlayerDriver::getCurrentPosition(int *msec) {
int64_t tempUs = 0;
{
Mutex::Autolock autoLock(mLock);
if (mSeekInProgress || (mState == STATE_PAUSED && !mAtEOS)) {
// 由上面的seekTo方法处理可知,mSeekInProgress为true,
// 进入此处处理即直接将上次seek的位置时间返回给了上层APP,
// 而该标识是在seek完成通知事件处理中设为false的,
// 因此造成了若seek完成通知事件延迟很久,那么上层APP获取到的进度值将不会改变。
tempUs = (mPositionUs <= 0) ? 0 : mPositionUs;
*msec = (int)divRound(tempUs, (int64_t)(1000));
return OK;
}
}
// 下面就是正常播放流程中获取当前播放位置的处理,此处不展开分析,可见后续MediaPlayer框架实现分析章节
status_t ret = mPlayer->getCurrentPosition(&tempUs);
Mutex::Autolock autoLock(mLock);
// We need to check mSeekInProgress here because mPlayer->seekToAsync is an async call, which
// means getCurrentPosition can be called before seek is completed. Iow, renderer may return a
// position value that's different the seek to position.
if (ret != OK) {
tempUs = (mPositionUs <= 0) ? 0 : mPositionUs;
} else {
mPositionUs = tempUs;
}
*msec = (int)divRound(tempUs, (int64_t)(1000));
return OK;
}
2.2、seekToAsync源码分析:
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayer.cpp]
void NuPlayer::seekToAsync(int64_t seekTimeUs, MediaPlayerSeekMode mode, bool needNotify) {
// 此处只是发送了kWhatSeek事件给自己Looper工作线程接收,见下面分析
sp<AMessage> msg = new AMessage(kWhatSeek, this);
msg->setInt64("seekTimeUs", seekTimeUs);
msg->setInt32("mode", mode);
msg->setInt32("needNotify", needNotify);
msg->post();
}
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayer.cpp]
void NuPlayer::onMessageReceived(const sp<AMessage> &msg) {
switch (msg->what()) {
// 省略其它代码
case kWhatSeek:
{
int64_t seekTimeUs;
int32_t mode;
int32_t needNotify;
CHECK(msg->findInt64("seekTimeUs", &seekTimeUs));
CHECK(msg->findInt32("mode", &mode));
CHECK(msg->findInt32("needNotify", &needNotify));
ALOGV("kWhatSeek seekTimeUs=%lld us, mode=%d, needNotify=%d",
(long long)seekTimeUs, mode, needNotify);
// 分两种情况处理:1、当前还未播放;2、当前正在播放
// 但我们此处只分析当前正在播放时的流程,因为黑屏但有声音播放就是发生在正在播放流程中,
// 因为未播放流程中seek之后会立即暂停即基本不会出现声音播放现象
if (!mStarted) {
// 当前还未播放时
// Seek before the player is started. In order to preview video,
// need to start the player and pause it. This branch is called
// only once if needed. After the player is started, any seek
// operation will go through normal path.
// Audio-only cases are handled separately.
onStart(seekTimeUs, (MediaPlayerSeekMode)mode);
if (mStarted) {
onPause();
mPausedByClient = true;
}
if (needNotify) {
notifyDriverSeekComplete();
}
break;
}
// 当前正在播放时,此处走了三个流程:
// 1、先处理音视频解码器的flush刷新清空缓冲数据;【注意不会shutdown解码器,此时解码器只是暂停状态】
// 2、decoder flush完成后执行seek定位操作;
// 3、然后恢复解码器进行解码新seek的音视频数据进行播放。
// 此处是将三个动作封装为三个待执行的Action操作类中,然后进行逐一执行
// 见2.3小节分析
mDeferredActions.push_back(
new FlushDecoderAction(FLUSH_CMD_FLUSH /* audio */,
FLUSH_CMD_FLUSH /* video */));
// 见2.4小节分析
mDeferredActions.push_back(
new SeekAction(seekTimeUs, (MediaPlayerSeekMode)mode));
// 见2.5小节分析
// After a flush without shutdown, decoder is paused.
// Don't resume it until source seek is done, otherwise it could
// start pulling stale data too soon.
mDeferredActions.push_back(
new ResumeDecoderAction(needNotify));
// 此处很简单就是按顺序执行上面三个流程Action
processDeferredActions();
break;
}
}
}
2.3、FlushDecoderAction实现为:
【处理音视频解码器的flush刷新清空缓冲数据】
// TODO 该流程对于该问题可以不用分析,只需知道其的作用即可。在后续的MediaPlayer架构实现中会详细分析
2.4、SeekAction实现流程分析:
【decoder flush完成后执行seek定位操作】
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayer.cpp]
struct NuPlayer::SeekAction : public Action {
explicit SeekAction(int64_t seekTimeUs, MediaPlayerSeekMode mode)
: mSeekTimeUs(seekTimeUs),
mMode(mode) {
}
virtual void execute(NuPlayer *player) {
// 最终会执行NuPlayer的该方法进行seek数据
// 见下面的分析
player->performSeek(mSeekTimeUs, mMode);
}
private:
int64_t mSeekTimeUs;
MediaPlayerSeekMode mMode;
DISALLOW_EVIL_CONSTRUCTORS(SeekAction);
};
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayer.cpp]
void NuPlayer::performSeek(int64_t seekTimeUs, MediaPlayerSeekMode mode) {
ALOGV("performSeek seekTimeUs=%lld us (%.2f secs), mode=%d",
(long long)seekTimeUs, seekTimeUs / 1E6, mode)