Android MediaPlayer在seek视频时可能会黑屏卡顿好几秒且进度条不动但有声音播放的问题源码解析

根本原因:个别视频格式播放器【特别是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);

    if (mSource == NULL) {
        // This happens when reset occurs right before the loop mode
        // asynchronously seeks to the start of the stream.
        LOG_ALWAYS_FATAL_IF(mAudioDecoder != NULL || mVideoDecoder != NULL,
                "mSource is NULL and decoders not NULL audio(%p) video(%p)",
                mAudioDecoder.get(), mVideoDecoder.get());
        return;
    }
    mPreviousSeekTimeUs = seekTimeUs;
    // 请求GenericSource对象进行文件数据的seek读取解析音视频数据,
    // 但要注意该方法是有返回状态的跨线程wait同步执行的,可能会耗时,一般情况下耗时很少
    // 此处也不再进行往下分析了,其功能已经说明了。在后续的MediaPlayer架构实现中会详细分析
    mSource->seekTo(seekTimeUs, mode);
    ++mTimedTextGeneration;

    // everything's flushed, continue playback.
}

2.5、ResumeDecoderAction实现流程分析:
【ResumeDecoderAction】

// 由2.4小节分析可知,音视频数据已经重新seek到新的seek时间点附近位置的数据
// 因此此时进行重启音视频解码器,使其工作拉取音视频已demuxed解复用的数据进行解码播放

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayer.cpp]
struct NuPlayer::ResumeDecoderAction : public Action {
    explicit ResumeDecoderAction(bool needNotify)
        : mNeedNotify(needNotify) {
    }

    virtual void execute(NuPlayer *player) {
        // 最终会执行NuPlayer的该方法
        // 见下面的分析
        player->performResumeDecoders(mNeedNotify);
    }

private:
    bool mNeedNotify;

    DISALLOW_EVIL_CONSTRUCTORS(ResumeDecoderAction);
};

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayer.cpp]
void NuPlayer::performResumeDecoders(bool needNotify) {
    if (needNotify) {
        // 由前面的分析中可知会进入此处
        
        // 该标识表示待恢复通知seek完成事件,即需要等待解码器判断可以渲染视频帧时进行通知上层APP
        mResumePending = true;
        if (mVideoDecoder == NULL) {
            // 此处是当只有音频播放时就可以立即通知seek完成事件给到上层APP,
            // 因为音频seek就是播放的当前seek位置的数据。
            // 见2.6小节分析
            // if audio-only, we can notify seek complete now,
            // as the resume operation will be relatively fast.
            finishResume();
        }
    }

    if (mVideoDecoder != NULL) {
        // When there is continuous seek, MediaPlayer will cache the seek
        // position, and send down new seek request when previous seek is
        // complete. Let's wait for at least one video output frame before
        // notifying seek complete, so that the video thumbnail gets updated
        // when seekbar is dragged.
        // 视频解码器根据参数【needNotify】需要回调通知,并通知恢复解码
        // 见2.7小节分析
        mVideoDecoder->signalResume(needNotify);
    }

    if (mAudioDecoder != NULL) {
        // 音频解码器此处不需要回调通知,只需要通知恢复解码播放即可
        // 此处不展开分析,因为音频seek时直接进行播放即可
        mAudioDecoder->signalResume(false /* needNotify */);
    }
}

2.6、finishResume实现分析:

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayer.cpp]
void NuPlayer::finishResume() {
    if (mResumePending) {
        mResumePending = false;
        notifyDriverSeekComplete();
    }
}
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayer.cpp]
void NuPlayer::notifyDriverSeekComplete() {
    if (mDriver != NULL) {
        sp<NuPlayerDriver> driver = mDriver.promote();
        if (driver != NULL) {
            // 见下面的分析
            driver->notifySeekComplete();
        }
    }
}

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDriver.cpp]
void NuPlayerDriver::notifySeekComplete() {
    ALOGV("notifySeekComplete(%p)", this);
    Mutex::Autolock autoLock(mLock);
    // 此处将seek标志位设置为false,使其上层APP可以正确获取其音频播放进度
    // 【因为安卓高版本音视频同步时钟是以音频播放时间为基准同步的】
    mSeekInProgress = false;
    notifySeekComplete_l();
}
// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDriver.cpp]
void NuPlayerDriver::notifySeekComplete_l() {
    bool wasSeeking = true;
    if (mState == STATE_STOPPED_AND_PREPARING) {
        // 如果当前播放状态为暂停状态且正在preparing中那么将唤醒prepare wait锁等处理
        wasSeeking = false;
        mState = STATE_STOPPED_AND_PREPARED;
        mCondition.broadcast();
        if (!mIsAsyncPrepare) {
            // if we are preparing synchronously, no need to notify listener
            return;
        }
    } else if (mState == STATE_STOPPED) {
        // no need to notify listener
        return;
    }
    // 正常播放状态下则通知事件给上层APP
    notifyListener_l(wasSeeking ? MEDIA_SEEK_COMPLETE : MEDIA_PREPARED);
}

2.7、mVideoDecoder->signalResume(needNotify)实现分析:

// mVideoDecoder对象为NuPlayer::Decoder,该方法为该类的父类【DecoderBase】方法实现,
// 父类实现文件为NuPlayerDecoderBase.cpp中。其实最终调用了该对象的onResume对象。

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDecoder.cpp]
void NuPlayer::Decoder::onResume(bool notifyComplete) {
    mPaused = false;

    if (notifyComplete) {
        // 该标识表示待恢复通知seek完成事件,即需要等待解码器判断可以渲染视频帧时进行通知上层APP
        mResumePending = true;
    }

    if (mCodec == NULL) {
        ALOGE("[%s] onResume without a valid codec", mComponentName.c_str());
        handleError(NO_INIT);
        return;
    }
    // 启动视频解码器开始获取数据进行解码工作,它将会拉取已解复用的视频数据进行解码,
    // 然后会通过消息事件方式通知[NuPlayer::Decoder]类中的onMessageReceived方法来接收事件:
    // 获取已解析的视频数据进行解码和通知解码完成获取已解码数据进行播放,
    // 当前只需要分析这两个处理流程即可分析出原因,这两个处理流程见2.8和2.9小节分析
    mCodec->start();
}
2.8[NuPlayer::Decoder]获取已解析的视频数据进行解码处理分析:
// TODO 该流程只简单分析其关键点,在后续的MediaPlayer架构实现中会详细分析

// 此事件为【kWhatCodecNotify】和【MediaCodec::CB_INPUT_AVAILABLE】
// 是在【NuPlayer::Decoder::onMessageReceived】方法中接收处理的。

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDecoder.cpp]

void NuPlayer::Decoder::onMessageReceived(const sp<AMessage> &msg) {
    ALOGV("[%s] onMessage: %s", mComponentName.c_str(), msg->debugString().c_str());

    switch (msg->what()) {
        case kWhatCodecNotify:
        {
            int32_t cbID;
            CHECK(msg->findInt32("callbackID", &cbID));

            ALOGV("[%s] kWhatCodecNotify: cbID = %d, paused = %d",
                    mIsAudio ? "audio" : "video", cbID, mPaused);

            if (mPaused) {
                break;
            }

            switch (cbID) {
                // 此处便是该事件的处理
                case MediaCodec::CB_INPUT_AVAILABLE:
                {
                    int32_t index;
                    CHECK(msg->findInt32("index", &index));

                    // 处理获取一个待解码的已解复用的视频数据buffer去解码
                    // 此方法此处不分析,只分析方法内调用的问题关键方法【onInputBufferFetched(msg)】见下面的分析
                    handleAnInputBuffer(index);
                    break;
                }
            }

            break;
        }
    }
}

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDecoder.cpp]
bool NuPlayer::Decoder::onInputBufferFetched(const sp<AMessage> &msg) {
    if (mCodec == NULL) {
        ALOGE("[%s] onInputBufferFetched without a valid codec", mComponentName.c_str());
        handleError(NO_INIT);
        return false;
    }
    
    // 该方法只分析引起问题的关键处理,见下面关键处的分析

    size_t bufferIx;
    CHECK(msg->findSize("buffer-ix", &bufferIx));
    CHECK_LT(bufferIx, mInputBuffers.size());
    sp<MediaCodecBuffer> codecBuffer = mInputBuffers[bufferIx];

    sp<ABuffer> buffer;
    bool hasBuffer = msg->findBuffer("buffer", &buffer);
    bool needsCopy = true;

    if (buffer == NULL /* includes !hasBuffer */) {
       // 省略多余代码
    } else {
        // 会进入此处
        sp<AMessage> extra;
        if (buffer->meta()->findMessage("extra", &extra) && extra != NULL) {
            int64_t resumeAtMediaTimeUs;
            if (extra->findInt64(
                        "resume-at-mediaTimeUs", &resumeAtMediaTimeUs)) {
                ALOGV("[%s] suppressing rendering until %lld us",
                        mComponentName.c_str(), (long long)resumeAtMediaTimeUs);
                // 在可seek非关键帧方式的seek到的第一个buffer数据中会有该字段的值,
                // 标识其应该恢复的播放指定时间点为seek时间点即跳过该时间点之前的视频帧,
                // 该设置的值就是造成第一阶段黑屏卡顿的原因,该值在2.9小节获取已解码视频帧处理流程分析中用到的
                mSkipRenderingUntilMediaTimeUs = resumeAtMediaTimeUs;
            }
        }
        
        // 省略多余代码

    }   // buffer != NULL
    return true;
}

2.9、通知[NuPlayer::Decoder]解码完成事件获取已解码数据处理分析:

// 此事件为【kWhatCodecNotify】和【MediaCodec::CB_OUTPUT_AVAILABLE】
// 是在【NuPlayer::Decoder::onMessageReceived】方法中接收处理的。

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDecoder.cpp]
void NuPlayer::Decoder::onMessageReceived(const sp<AMessage> &msg) {
    ALOGV("[%s] onMessage: %s", mComponentName.c_str(), msg->debugString().c_str());

    switch (msg->what()) {
        case kWhatCodecNotify:
        {
            int32_t cbID;
            CHECK(msg->findInt32("callbackID", &cbID));

            ALOGV("[%s] kWhatCodecNotify: cbID = %d, paused = %d",
                    mIsAudio ? "audio" : "video", cbID, mPaused);

            if (mPaused) {
                break;
            }

            // 省略多余代码
            
            switch (cbID) {
                // 此处便是该事件的处理
                case MediaCodec::CB_OUTPUT_AVAILABLE:
                {
                    int32_t index;
                    size_t offset;
                    size_t size;
                    int64_t timeUs;
                    int32_t flags;

                    CHECK(msg->findInt32("index", &index));
                    CHECK(msg->findSize("offset", &offset));
                    CHECK(msg->findSize("size", &size));
                    CHECK(msg->findInt64("timeUs", &timeUs));
                    CHECK(msg->findInt32("flags", &flags));

                    // 处理获取一个已解码的视频数据buffer
                    // 见下面的分析
                    handleAnOutputBuffer(index, offset, size, timeUs, flags);
                    break;
                }
            }

            break;
        }
    }
}

// [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;
    // 根据已解码的buffer index索引来向解码器获取该已解码buffer数据
    mCodec->getOutputBuffer(index, &buffer);

    if (buffer == NULL) {
        ALOGE("[%s] handleAnOutputBuffer, failed to get output buffer", mComponentName.c_str());
        handleError(UNKNOWN_ERROR);
        return false;
    }

    // 此处处理为若当前buffer缓冲区大小不足,则增加到指定大小【index + 1】
    if (index >= mOutputBuffers.size()) {
        for (size_t i = mOutputBuffers.size(); i <= index; ++i) {
            mOutputBuffers.add();
        }
    }

    // 更新缓冲区中指定索引index对应的buffer数据
    mOutputBuffers.editItemAt(index) = buffer;

    buffer->setRange(offset, size);
    buffer->meta()->clear();
    buffer->meta()->setInt64("timeUs", timeUs);

    // 是否解码器缓冲区buffer数据流结束EOS
    bool eos = flags & MediaCodec::BUFFER_FLAG_EOS;
    // we do not expect CODECCONFIG or SYNCFRAME for decoder

    // 已解码视频buffer渲染事件
    sp<AMessage> reply = new AMessage(kWhatRenderBuffer, this);
    reply->setSize("buffer-ix", index);
    reply->setInt32("generation", mBufferGeneration);
    reply->setSize("size", size);

    if (eos) {
        ALOGV("[%s] saw output EOS", mIsAudio ? "audio" : "video");

        buffer->meta()->setInt32("eos", true);
        reply->setInt32("eos", true);
    }

    mNumFramesTotal += !mIsAudio;
    
    if (mSkipRenderingUntilMediaTimeUs >= 0) {
        // 引起问题的关键处理:由2.8小节可知在seek时【mSkipRenderingUntilMediaTimeUs】该值为seek位置值
        // 并且刚开始播放时【timeUs < mSkipRenderingUntilMediaTimeUs】该条件成立即视频帧时间小于seek时间
        if (timeUs < mSkipRenderingUntilMediaTimeUs) {
            // 因此进入此处处理
            ALOGV("[%s] dropping buffer at time %lld as requested.",
                     mComponentName.c_str(), (long long)timeUs);
            
            // 此处就是通知【kWhatRenderBuffer】已渲染buffer事件给自己接收处理
            // 见2.10小节分析     
            reply->post();
            if (eos) {
                // 若是解码器缓冲区buffer数据流已经EOS状态了,则直接通知恢复【此时为seek事件】播放完成事件
                notifyResumeCompleteIfNecessary();
                if (mRenderer != NULL && !isDiscontinuityPending()) {
                    mRenderer->queueEOS(mIsAudio, ERROR_END_OF_STREAM);
                }
            }
            
            // 此处就直接返回,不进入下面的【mRenderer->queueBuffer】真正渲染处理,
            // 因此就造成若解码速度过慢导致该if条件一直成立的话就会导致较长时间的黑屏卡顿现象
            return true;
        }
        
        // 当视频帧时间大于等于seek时间时则将该标记值设置为无效,即允许开始尝试执行下面的真正的播放处理了
        // 注意从此时开始也还只是尝试直接【mRenderer->queueBuffer】尝试渲染处理,
        // 因为还需要音视频同步时钟的处理,也就导致了第二阶段的黑屏原因
        mSkipRenderingUntilMediaTimeUs = -1;
    } else if ((flags & MediaCodec::BUFFER_FLAG_DATACORRUPT) &&
            AVNuUtils::get()->dropCorruptFrame()) {
        ALOGV("[%s] dropping corrupt buffer at time %lld as requested.",
                     mComponentName.c_str(), (long long)timeUs);
        reply->post();
        return true;
    }
    
    // 通知恢复【此时为seek事件】播放成功事件给到上层APP
    // 此方法的延迟执行导致了第一阶段的黑屏卡顿原因,并导致了延迟通知seek完成,
    // 也就导致进度条一直获取的是seek位置值
    // 见下面的分析
    // wait until 1st frame comes out to signal resume complete
    notifyResumeCompleteIfNecessary();

    if (mRenderer != NULL) {
        // 真正的发送视频帧buffer给到renderer进行尝试渲染播放
        // send the buffer to renderer.
        mRenderer->queueBuffer(mIsAudio, buffer, reply);
        if (eos && !isDiscontinuityPending()) {
            mRenderer->queueEOS(mIsAudio, ERROR_END_OF_STREAM);
        }
    }

    return true;
}

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerDecoder.cpp]
void NuPlayer::Decoder::notifyResumeCompleteIfNecessary() {
    if (mResumePending) {
        // 根据上面分析可知第一帧待播放数据会进行该通知
        mResumePending = false;

        sp<AMessage> notify = mNotify->dup();
        notify->setInt32("what", kWhatResumeCompleted);
        notify->post();
    }
}

void NuPlayer::onMessageReceived(const sp<AMessage> &msg) {
    switch (msg->what()) {
        // 省略其他代码
        case kWhatVideoNotify:
        case kWhatAudioNotify:
        {
            int32_t what;
            CHECK(msg->findInt32("what", &what));
            if {
                // 省略其他代码
            } else if (what == DecoderBase::kWhatResumeCompleted) {
                // 最后调用了该方法即上面分析过的通知seek位置完成事件给上层APP
                finishResume();
            }
            // 省略其他代码
         }
      }
}

2.10、然后分析第二阶段黑屏卡顿原因:即第一阶段卡顿处理完成后视频已解码到seek位置时就会尝试渲染,但此时由于有音视频同步时钟机制即视频播放以音频时间戳为参考,而此时音频播放肯定已超过seek位置时间点了,因此此时视频还是不能正常渲染,分析流程如下:
由2.9小节分析可知:第一阶段处理完成后会进行渲染请求,即调用渲染模块的【mRenderer->queueBuffer(mIsAudio, buffer, reply);】该方法

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::queueBuffer(
        bool audio,
        const sp<MediaCodecBuffer> &buffer,
        const sp<AMessage> &notifyConsumed) {
    // 发送【kWhatQueueBuffer】消息事件给到自己的【void NuPlayer::Renderer::onMessageReceived(const sp<AMessage> &msg)】方法去接收处理
    // 该方法直接调用了【onQueueBuffer(msg);】方法,见下面的分析
    sp<AMessage> msg = new AMessage(kWhatQueueBuffer, this);
    msg->setInt32("queueGeneration", getQueueGeneration(audio));
    msg->setInt32("audio", static_cast<int32_t>(audio));
    msg->setObject("buffer", buffer);
    msg->setMessage("notifyConsumed", notifyConsumed);
    msg->post();
}

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::onQueueBuffer(const sp<AMessage> &msg) {
    int32_t audio;
    CHECK(msg->findInt32("audio", &audio));

    // 此处判断是否需要drop掉老旧buffer数据,判断条件为【mVideoQueueGeneration】值,
    // 该值会与在上面消息中赋值的【msg->setInt32("queueGeneration", getQueueGeneration(audio));】
    // 值进行比较,相等则表示有效数据,而该值只在flush时改变,因此此处不会导致此问题的原因
    if (dropBufferIfStale(audio, msg)) {
        return;
    }

    if (audio) {
        mHasAudio = true;
    } else {
        mHasVideo = true;
    }

    if (mHasVideo) {
        if (mVideoScheduler == NULL) {
            mVideoScheduler = new VideoFrameScheduler();
            mVideoScheduler->init();
        }
    }

    // 拿到当前有效的视频帧buffer
    sp<RefBase> obj;
    CHECK(msg->findObject("buffer", &obj));
    sp<MediaCodecBuffer> buffer = static_cast<MediaCodecBuffer *>(obj.get());

    sp<AMessage> notifyConsumed;
    CHECK(msg->findMessage("notifyConsumed", &notifyConsumed));

    // 封装队列实体数据类
    QueueEntry entry;
    entry.mBuffer = buffer;
    entry.mNotifyConsumed = notifyConsumed;
    entry.mOffset = 0;
    entry.mFinalResult = OK;
    entry.mBufferOrdinal = ++mTotalBuffersQueued;

    if (audio) {
        Mutex::Autolock autoLock(mLock);
        mAudioQueue.push_back(entry);
        postDrainAudioQueue_l();
    } else {
        // 放入视频队列中
        mVideoQueue.push_back(entry);
        // 请求消耗即播放视频队列中的数据
        // 见2.11小节分析
        postDrainVideoQueue();
    }

    Mutex::Autolock autoLock(mLock);
    // 注意此处的【mSyncQueues】值在安卓高版本中都是false,因此直接此处return了,不执行下面的处理。
    // 其实下面的处理为音频播放时间需要以视频时间戳为参考,刚好和高版本中的音视频同步机制相反,
    // 而为什么需要改为以音频时间戳为准进行播放呢,原因应该是和用户体验有关,毕竟音频播放是用户最直接的感官,
    // 如果视频解码器就会导音频一直也卡顿
    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();
}

2.11、postDrainVideoQueue实现分析:

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
// Called without mLock acquired.
void NuPlayer::Renderer::postDrainVideoQueue() {
    if (mDrainVideoQueuePending
            || getSyncQueues()
            || (mPaused && mVideoSampleReceived)) {
        return;
    }

    if (mVideoQueue.empty()) {
        return;
    }

    QueueEntry &entry = *mVideoQueue.begin();

    sp<AMessage> msg = new AMessage(kWhatDrainVideoQueue, this);
    msg->setInt32("drainGeneration", getDrainGeneration(false /* audio */));

    // 省略多余代码
    
    // 分析此处会执行【kWhatDrainVideoQueue】消息事件的处理流程,该事件调用了【onDrainVideoQueue】方法,该方法见2.12小节分析
    // 备注:其实此处若走了else的流程也是可能导致其黑屏卡顿问题的,其原因是:MediaClock音视频同步时钟机制导致的,
    // 比如若当前音频播放也会不连续样本数据播放时就会导致当前的视频迟迟得不到渲染,当然当前问题是音频播放正常,
    // 因此不再分析此处处理问题,在后续MediaPlayer架构分析章节中会详细分析。
    if (!mVideoSampleReceived || mediaTimeUs < mAudioFirstAnchorTimeMediaUs || getVideoLateByUs() > 40000) {
        msg->post();
    } else {
        int64_t twoVsyncsUs = 2 * (mVideoScheduler->getVsyncPeriod() / 1000);

        // post 2 display refreshes before rendering is due
        mMediaClock->addTimer(msg, mediaTimeUs, -twoVsyncsUs);
    }

    mDrainVideoQueuePending = true;
}

2.12、onDrainVideoQueue实现分析:

// [frameworks/av/media/libmediaplayerservice/nuplayer/NuPlayerRenderer.cpp]
void NuPlayer::Renderer::onDrainVideoQueue() {
    if (mVideoQueue.empty()) {
        return;
    }

    QueueEntry *entry = &*mVideoQueue.begin();

    if (entry->mBuffer == NULL) {
        // EOS

        notifyEOS(false /* audio */, entry->mFinalResult);

        mVideoQueue.erase(mVideoQueue.begin());
        entry = NULL;

        setVideoLateByUs(0);
        return;
    }

    int64_t nowUs = ALooper::GetNowUs();
    int64_t realTimeUs;
    int64_t mediaTimeUs = -1;
    if (mFlags & FLAG_REAL_TIME) {
        CHECK(entry->mBuffer->meta()->findInt64("timeUs", &realTimeUs));
    } else {
        CHECK(entry->mBuffer->meta()->findInt64("timeUs", &mediaTimeUs));

        realTimeUs = getRealTimeUs(mediaTimeUs, nowUs);
    }
    realTimeUs = mVideoScheduler->schedule(realTimeUs * 1000) / 1000;

    bool tooLate = false;

    if (!mPaused) {
        setVideoLateByUs(nowUs - realTimeUs);
        // 异常关键点:此处就是音视频同步机制中视频的同步处理判断,
        // 默认40ms内才认为视频和音频是同步的才能渲染
        tooLate = (mVideoLateByUs > 40000);
        
        // tooLate若为true则在下面的处理中会标记当前buffer不能渲染
        if (tooLate) {
            // 根据上面的分析,此时seek位置时间肯定延迟了,因此不会渲染,
            // 因此视频帧数据一直toolate不能渲染,直到解码视频帧时间符合音视频同步机制才会正常播放
            ALOGV("video late by %lld us (%.2f secs)",
                 (long long)mVideoLateByUs, mVideoLateByUs / 1E6);
        } else {
            int64_t mediaUs = 0;
            mMediaClock->getMediaTime(realTimeUs, &mediaUs);
            ALOGV("rendering video at media time %.2f secs",
                    (mFlags & FLAG_REAL_TIME ? realTimeUs :
                    mediaUs) / 1E6);

            if (!(mFlags & FLAG_REAL_TIME)
                    && mLastAudioMediaTimeUs != -1
                    && mediaTimeUs > mLastAudioMediaTimeUs) {
                // If audio ends before video, video continues to drive media clock.
                // Also smooth out videos >= 10fps.
                mMediaClock->updateMaxTimeMedia(mediaTimeUs + kDefaultVideoFrameIntervalUs);
            }
        }
    } else {
        setVideoLateByUs(0);
        if (!mVideoSampleReceived && !mHasAudio) {
            // This will ensure that the first frame after a flush won't be used as anchor
            // when renderer is in paused state, because resume can happen any time after seek.
            clearAnchorTime();
        }
    }

    // Always render the first video frame while keeping stats on A/V sync.
    if (!mVideoSampleReceived) {
        realTimeUs = nowUs;
        tooLate = false;
    }

    entry->mNotifyConsumed->setInt64("timestampNs", realTimeUs * 1000LL);
    // 此次即标记其不能渲染
    entry->mNotifyConsumed->setInt32("render", !tooLate);
    entry->mNotifyConsumed->post();
    mVideoQueue.erase(mVideoQueue.begin());
    entry = NULL;

    mVideoSampleReceived = true;

    if (!mPaused) {
        if (!mVideoRenderingStarted) {
            mVideoRenderingStarted = true;
            notifyVideoRenderingStart();
        }
        Mutex::Autolock autoLock(mLock);
        notifyIfMediaRenderingStarted_l();
    }
}

综上,我们分析出了该问题黑屏卡顿的原因。

结束语:
可能分析到此处我们可以知道视频解码器慢了,但是革命尚未成功,分析还未结束,不能只以为是解码器慢了就不分析了,因为其实我们还需要分析为什么慢了,这个分析至少涉及到两个方面的分析:
1、解复用层解析视频数据慢,导致数据不足;【不同的平台实现可能不同,比如高通使用自己的解复用实现模块,并且解复用接口有可能是各个厂商的私有库,如我遇到的该问题就向高通提CASE了】
2、视频解码器拉取视频数据慢即解码速度慢也就导致渲染速度慢,单纯的视频解码器慢。

第2点可以不分析,因为单纯的视频解码器慢对于当前问题分析后的结果看不是这个原因,原因是第1点的解复用层解析视频数据慢,导致数据不足造成的卡顿。
但由于解析数据接口跟厂商私有库相关,此处就不再分析了,若有小伙伴感兴趣请留言我也可以稍微说一下

多谢大家的支持,共同进步!

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,下面是一个简单的 Android 音乐播放器的代码和布局文件,支持播放多首音乐,有进度条和拖动进度条的功能: 布局文件 `activity_main.xml`: ```xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <!-- 音乐标题 --> <TextView android:id="@+id/titleTextView" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Title" android:gravity="center" android:textSize="24sp" android:padding="16dp" /> <!-- 进度条 --> <SeekBar android:id="@+id/seekBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" /> <!-- 播放控制按钮 --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="16dp"> <Button android:id="@+id/previousButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Previous" /> <Button android:id="@+id/playButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Play" /> <Button android:id="@+id/nextButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Next" /> </LinearLayout> </LinearLayout> ``` Java 代码 `MainActivity.java`: ```java import android.media.MediaPlayer; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.widget.Button; import android.widget.SeekBar; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import java.util.ArrayList; public class MainActivity extends AppCompatActivity { private ArrayList<Integer> musicList; // 音乐列表 private int currentMusicIndex; // 当前播放的音乐索引 private MediaPlayer mediaPlayer; // 媒体播放器 private TextView titleTextView; // 音乐标题 private SeekBar seekBar; // 进度条 private Button previousButton; // 上一首按钮 private Button playButton; // 播放按钮 private Button nextButton; // 下一首按钮 private Handler handler; // 处理进度条更新的 Handler @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化音乐列表 musicList = new ArrayList<>(); musicList.add(R.raw.music1); musicList.add(R.raw.music2); musicList.add(R.raw.music3); currentMusicIndex = 0; // 初始化媒体播放mediaPlayer = MediaPlayer.create(this, musicList.get(currentMusicIndex)); // 初始化视图 initView(); // 开始播放音乐 playMusic(); } private void initView() { // 获取视图控件 titleTextView = findViewById(R.id.titleTextView); seekBar = findViewById(R.id.seekBar); previousButton = findViewById(R.id.previousButton); playButton = findViewById(R.id.playButton); nextButton = findViewById(R.id.nextButton); // 设置进度条最大值 seekBar.setMax(mediaPlayer.getDuration()); // 处理进度条更新 handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { if (mediaPlayer.isPlaying()) { int currentPosition = mediaPlayer.getCurrentPosition(); seekBar.setProgress(currentPosition); handler.postDelayed(this, 1000); // 每隔 1 秒更新一次进度条 } } }, 1000); // 设置按钮点击事件 previousButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { playPreviousMusic(); } }); playButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mediaPlayer.isPlaying()) { pauseMusic(); } else { playMusic(); } } }); nextButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { playNextMusic(); } }); // 设置进度条拖动事件 seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { mediaPlayer.seekTo(progress); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { // do nothing } @Override public void onStopTrackingTouch(SeekBar seekBar) { // do nothing } }); } private void playMusic() { mediaPlayer.start(); playButton.setText("Pause"); titleTextView.setText("Music " + (currentMusicIndex + 1)); } private void pauseMusic() { mediaPlayer.pause(); playButton.setText("Play"); } private void playPreviousMusic() { if (currentMusicIndex > 0) { currentMusicIndex--; } else { currentMusicIndex = musicList.size() - 1; } mediaPlayer.stop(); mediaPlayer = MediaPlayer.create(this, musicList.get(currentMusicIndex)); playMusic(); } private void playNextMusic() { if (currentMusicIndex < musicList.size() - 1) { currentMusicIndex++; } else { currentMusicIndex = 0; } mediaPlayer.stop(); mediaPlayer = MediaPlayer.create(this, musicList.get(currentMusicIndex)); playMusic(); } @Override protected void onDestroy() { super.onDestroy(); mediaPlayer.stop(); mediaPlayer.release(); } } ``` 上面的代码实现了一个简单的 Android 音乐播放器,支持播放多首音乐,有进度条和拖动进度条的功能。需要注意的是,上面的代码使用了 `MediaPlayer` 类来实现音乐播放,而且将音乐文件放在了 `res/raw` 目录下。如果想要自己实现一个 Android 音乐播放器,可以根据自己的需求来修改上面的代码。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值