diff options
Diffstat (limited to 'dom/media/AccurateSeekTask.cpp')
-rw-r--r-- | dom/media/AccurateSeekTask.cpp | 470 |
1 files changed, 470 insertions, 0 deletions
diff --git a/dom/media/AccurateSeekTask.cpp b/dom/media/AccurateSeekTask.cpp new file mode 100644 index 000000000..30eeb5904 --- /dev/null +++ b/dom/media/AccurateSeekTask.cpp @@ -0,0 +1,470 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "AccurateSeekTask.h" +#include "MediaDecoderReaderWrapper.h" +#include "mozilla/AbstractThread.h" +#include "mozilla/Assertions.h" +#include "nsPrintfCString.h" + +namespace mozilla { + +extern LazyLogModule gMediaDecoderLog; +extern LazyLogModule gMediaSampleLog; + +// avoid redefined macro in unified build +#undef FMT +#undef DECODER_LOG +#undef SAMPLE_LOG +#undef DECODER_WARN + +#define FMT(x, ...) "[AccurateSeekTask] Decoder=%p " x, mDecoderID, ##__VA_ARGS__ +#define DECODER_LOG(...) MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (FMT(__VA_ARGS__))) +#define SAMPLE_LOG(...) MOZ_LOG(gMediaSampleLog, LogLevel::Debug, (FMT(__VA_ARGS__))) +#define DECODER_WARN(...) NS_WARNING(nsPrintfCString(FMT(__VA_ARGS__)).get()) + +AccurateSeekTask::AccurateSeekTask(const void* aDecoderID, + AbstractThread* aThread, + MediaDecoderReaderWrapper* aReader, + const SeekTarget& aTarget, + const MediaInfo& aInfo, + const media::TimeUnit& aEnd, + int64_t aCurrentMediaTime) + : SeekTask(aDecoderID, aThread, aReader, aTarget) + , mCurrentTimeBeforeSeek(media::TimeUnit::FromMicroseconds(aCurrentMediaTime)) + , mAudioRate(aInfo.mAudio.mRate) + , mDoneAudioSeeking(!aInfo.HasAudio() || aTarget.IsVideoOnly()) + , mDoneVideoSeeking(!aInfo.HasVideo()) +{ + AssertOwnerThread(); + + // Bound the seek time to be inside the media range. + NS_ASSERTION(aEnd.ToMicroseconds() != -1, "Should know end time by now"); + mTarget.SetTime(std::max(media::TimeUnit(), std::min(mTarget.GetTime(), aEnd))); + + // Configure MediaDecoderReaderWrapper. + SetCallbacks(); +} + +AccurateSeekTask::~AccurateSeekTask() +{ + AssertOwnerThread(); + MOZ_ASSERT(mIsDiscarded); +} + +void +AccurateSeekTask::Discard() +{ + AssertOwnerThread(); + + // Disconnect MDSM. + RejectIfExist(NS_ERROR_DOM_MEDIA_CANCELED, __func__); + + // Disconnect MediaDecoderReaderWrapper. + mSeekRequest.DisconnectIfExists(); + CancelCallbacks(); + + mIsDiscarded = true; +} + +bool +AccurateSeekTask::NeedToResetMDSM() const +{ + AssertOwnerThread(); + return true; +} + +RefPtr<AccurateSeekTask::SeekTaskPromise> +AccurateSeekTask::Seek(const media::TimeUnit& aDuration) +{ + AssertOwnerThread(); + + // Do the seek. + mSeekRequest.Begin(mReader->Seek(mTarget, aDuration) + ->Then(OwnerThread(), __func__, this, + &AccurateSeekTask::OnSeekResolved, &AccurateSeekTask::OnSeekRejected)); + + return mSeekTaskPromise.Ensure(__func__); +} + +void +AccurateSeekTask::RequestAudioData() +{ + AssertOwnerThread(); + MOZ_ASSERT(!mDoneAudioSeeking); + MOZ_ASSERT(!mReader->IsRequestingAudioData()); + MOZ_ASSERT(!mReader->IsWaitingAudioData()); + mReader->RequestAudioData(); +} + +void +AccurateSeekTask::RequestVideoData() +{ + AssertOwnerThread(); + MOZ_ASSERT(!mDoneVideoSeeking); + MOZ_ASSERT(!mReader->IsRequestingVideoData()); + MOZ_ASSERT(!mReader->IsWaitingVideoData()); + mReader->RequestVideoData(false, media::TimeUnit()); +} + +nsresult +AccurateSeekTask::DropAudioUpToSeekTarget(MediaData* aSample) +{ + AssertOwnerThread(); + + RefPtr<AudioData> audio(aSample->As<AudioData>()); + MOZ_ASSERT(audio && mTarget.IsAccurate()); + + CheckedInt64 sampleDuration = FramesToUsecs(audio->mFrames, mAudioRate); + if (!sampleDuration.isValid()) { + return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR; + } + + if (audio->mTime + sampleDuration.value() <= mTarget.GetTime().ToMicroseconds()) { + // Our seek target lies after the frames in this AudioData. Don't + // push it onto the audio queue, and keep decoding forwards. + return NS_OK; + } + + if (audio->mTime > mTarget.GetTime().ToMicroseconds()) { + // The seek target doesn't lie in the audio block just after the last + // audio frames we've seen which were before the seek target. This + // could have been the first audio data we've seen after seek, i.e. the + // seek terminated after the seek target in the audio stream. Just + // abort the audio decode-to-target, the state machine will play + // silence to cover the gap. Typically this happens in poorly muxed + // files. + DECODER_WARN("Audio not synced after seek, maybe a poorly muxed file?"); + mSeekedAudioData = audio; + mDoneAudioSeeking = true; + return NS_OK; + } + + // The seek target lies somewhere in this AudioData's frames, strip off + // any frames which lie before the seek target, so we'll begin playback + // exactly at the seek target. + NS_ASSERTION(mTarget.GetTime().ToMicroseconds() >= audio->mTime, + "Target must at or be after data start."); + NS_ASSERTION(mTarget.GetTime().ToMicroseconds() < audio->mTime + sampleDuration.value(), + "Data must end after target."); + + CheckedInt64 framesToPrune = + UsecsToFrames(mTarget.GetTime().ToMicroseconds() - audio->mTime, mAudioRate); + if (!framesToPrune.isValid()) { + return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR; + } + if (framesToPrune.value() > audio->mFrames) { + // We've messed up somehow. Don't try to trim frames, the |frames| + // variable below will overflow. + DECODER_WARN("Can't prune more frames that we have!"); + return NS_ERROR_FAILURE; + } + uint32_t frames = audio->mFrames - static_cast<uint32_t>(framesToPrune.value()); + uint32_t channels = audio->mChannels; + AlignedAudioBuffer audioData(frames * channels); + if (!audioData) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memcpy(audioData.get(), + audio->mAudioData.get() + (framesToPrune.value() * channels), + frames * channels * sizeof(AudioDataValue)); + CheckedInt64 duration = FramesToUsecs(frames, mAudioRate); + if (!duration.isValid()) { + return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR; + } + RefPtr<AudioData> data(new AudioData(audio->mOffset, + mTarget.GetTime().ToMicroseconds(), + duration.value(), + frames, + Move(audioData), + channels, + audio->mRate)); + MOZ_ASSERT(!mSeekedAudioData, "Should be the 1st sample after seeking"); + mSeekedAudioData = data; + mDoneAudioSeeking = true; + + return NS_OK; +} + +nsresult +AccurateSeekTask::DropVideoUpToSeekTarget(MediaData* aSample) +{ + AssertOwnerThread(); + + RefPtr<VideoData> video(aSample->As<VideoData>()); + MOZ_ASSERT(video); + DECODER_LOG("DropVideoUpToSeekTarget() frame [%lld, %lld]", + video->mTime, video->GetEndTime()); + const int64_t target = mTarget.GetTime().ToMicroseconds(); + + // If the frame end time is less than the seek target, we won't want + // to display this frame after the seek, so discard it. + if (target >= video->GetEndTime()) { + DECODER_LOG("DropVideoUpToSeekTarget() pop video frame [%lld, %lld] target=%lld", + video->mTime, video->GetEndTime(), target); + mFirstVideoFrameAfterSeek = video; + } else { + if (target >= video->mTime && video->GetEndTime() >= target) { + // The seek target lies inside this frame's time slice. Adjust the frame's + // start time to match the seek target. We do this by replacing the + // first frame with a shallow copy which has the new timestamp. + RefPtr<VideoData> temp = VideoData::ShallowCopyUpdateTimestamp(video.get(), target); + video = temp; + } + mFirstVideoFrameAfterSeek = nullptr; + + DECODER_LOG("DropVideoUpToSeekTarget() found video frame [%lld, %lld] containing target=%lld", + video->mTime, video->GetEndTime(), target); + + MOZ_ASSERT(!mSeekedVideoData, "Should be the 1st sample after seeking"); + mSeekedVideoData = video; + mDoneVideoSeeking = true; + } + + return NS_OK; +} + +void +AccurateSeekTask::MaybeFinishSeek() +{ + AssertOwnerThread(); + if (mDoneAudioSeeking && mDoneVideoSeeking) { + Resolve(__func__); // Call to MDSM::SeekCompleted(); + } +} + +void +AccurateSeekTask::OnSeekResolved(media::TimeUnit) +{ + AssertOwnerThread(); + + mSeekRequest.Complete(); + // We must decode the first samples of active streams, so we can determine + // the new stream time. So dispatch tasks to do that. + if (!mDoneVideoSeeking) { + RequestVideoData(); + } + if (!mDoneAudioSeeking) { + RequestAudioData(); + } +} + +void +AccurateSeekTask::OnSeekRejected(nsresult aResult) +{ + AssertOwnerThread(); + + mSeekRequest.Complete(); + MOZ_ASSERT(NS_FAILED(aResult), "Cancels should also disconnect mSeekRequest"); + RejectIfExist(aResult, __func__); +} + +void +AccurateSeekTask::AdjustFastSeekIfNeeded(MediaData* aSample) +{ + AssertOwnerThread(); + if (mTarget.IsFast() && + mTarget.GetTime() > mCurrentTimeBeforeSeek && + aSample->mTime < mCurrentTimeBeforeSeek.ToMicroseconds()) { + // We are doing a fastSeek, but we ended up *before* the previous + // playback position. This is surprising UX, so switch to an accurate + // seek and decode to the seek target. This is not conformant to the + // spec, fastSeek should always be fast, but until we get the time to + // change all Readers to seek to the keyframe after the currentTime + // in this case, we'll just decode forward. Bug 1026330. + mTarget.SetType(SeekTarget::Accurate); + } +} + +void +AccurateSeekTask::OnAudioDecoded(MediaData* aAudioSample) +{ + AssertOwnerThread(); + MOZ_ASSERT(!mSeekTaskPromise.IsEmpty(), "Seek shouldn't be finished"); + + RefPtr<MediaData> audio(aAudioSample); + MOZ_ASSERT(audio); + + // The MDSM::mDecodedAudioEndTime will be updated once the whole SeekTask is + // resolved. + + SAMPLE_LOG("OnAudioDecoded [%lld,%lld]", audio->mTime, audio->GetEndTime()); + + // Video-only seek doesn't reset audio decoder. There might be pending audio + // requests when AccurateSeekTask::Seek() begins. We will just store the data + // without checking |mDiscontinuity| or calling DropAudioUpToSeekTarget(). + if (mTarget.IsVideoOnly()) { + mSeekedAudioData = audio.forget(); + return; + } + + AdjustFastSeekIfNeeded(audio); + + if (mTarget.IsFast()) { + // Non-precise seek; we can stop the seek at the first sample. + mSeekedAudioData = audio; + mDoneAudioSeeking = true; + } else { + nsresult rv = DropAudioUpToSeekTarget(audio); + if (NS_FAILED(rv)) { + CancelCallbacks(); + RejectIfExist(rv, __func__); + return; + } + } + + if (!mDoneAudioSeeking) { + RequestAudioData(); + return; + } + MaybeFinishSeek(); +} + +void +AccurateSeekTask::OnNotDecoded(MediaData::Type aType, + const MediaResult& aError) +{ + AssertOwnerThread(); + MOZ_ASSERT(!mSeekTaskPromise.IsEmpty(), "Seek shouldn't be finished"); + + SAMPLE_LOG("OnNotDecoded type=%d reason=%u", aType, aError.Code()); + + // Ignore pending requests from video-only seek. + if (aType == MediaData::AUDIO_DATA && mTarget.IsVideoOnly()) { + return; + } + + // If the decoder is waiting for data, we tell it to call us back when the + // data arrives. + if (aError == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) { + mReader->WaitForData(aType); + return; + } + + if (aError == NS_ERROR_DOM_MEDIA_CANCELED) { + if (aType == MediaData::AUDIO_DATA) { + RequestAudioData(); + } else { + RequestVideoData(); + } + return; + } + + if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) { + if (aType == MediaData::AUDIO_DATA) { + mIsAudioQueueFinished = true; + mDoneAudioSeeking = true; + } else { + mIsVideoQueueFinished = true; + mDoneVideoSeeking = true; + if (mFirstVideoFrameAfterSeek) { + // Hit the end of stream. Move mFirstVideoFrameAfterSeek into + // mSeekedVideoData so we have something to display after seeking. + mSeekedVideoData = mFirstVideoFrameAfterSeek.forget(); + } + } + MaybeFinishSeek(); + return; + } + + // This is a decode error, delegate to the generic error path. + CancelCallbacks(); + RejectIfExist(aError, __func__); +} + +void +AccurateSeekTask::OnVideoDecoded(MediaData* aVideoSample) +{ + AssertOwnerThread(); + MOZ_ASSERT(!mSeekTaskPromise.IsEmpty(), "Seek shouldn't be finished"); + + RefPtr<MediaData> video(aVideoSample); + MOZ_ASSERT(video); + + // The MDSM::mDecodedVideoEndTime will be updated once the whole SeekTask is + // resolved. + + SAMPLE_LOG("OnVideoDecoded [%lld,%lld]", video->mTime, video->GetEndTime()); + + AdjustFastSeekIfNeeded(video); + + if (mTarget.IsFast()) { + // Non-precise seek. We can stop the seek at the first sample. + mSeekedVideoData = video; + mDoneVideoSeeking = true; + } else { + nsresult rv = DropVideoUpToSeekTarget(video.get()); + if (NS_FAILED(rv)) { + CancelCallbacks(); + RejectIfExist(rv, __func__); + return; + } + } + + if (!mDoneVideoSeeking) { + RequestVideoData(); + return; + } + MaybeFinishSeek(); +} + +void +AccurateSeekTask::SetCallbacks() +{ + AssertOwnerThread(); + + RefPtr<AccurateSeekTask> self = this; + mAudioCallback = mReader->AudioCallback().Connect( + OwnerThread(), [self] (AudioCallbackData aData) { + if (aData.is<MediaData*>()) { + self->OnAudioDecoded(aData.as<MediaData*>()); + } else { + self->OnNotDecoded(MediaData::AUDIO_DATA, + aData.as<MediaResult>()); + } + }); + + mVideoCallback = mReader->VideoCallback().Connect( + OwnerThread(), [self] (VideoCallbackData aData) { + typedef Tuple<MediaData*, TimeStamp> Type; + if (aData.is<Type>()) { + self->OnVideoDecoded(Get<0>(aData.as<Type>())); + } else { + self->OnNotDecoded(MediaData::VIDEO_DATA, + aData.as<MediaResult>()); + } + }); + + mAudioWaitCallback = mReader->AudioWaitCallback().Connect( + OwnerThread(), [self] (WaitCallbackData aData) { + // Ignore pending requests from video-only seek. + if (self->mTarget.IsVideoOnly()) { + return; + } + if (aData.is<MediaData::Type>()) { + self->RequestAudioData(); + } + }); + + mVideoWaitCallback = mReader->VideoWaitCallback().Connect( + OwnerThread(), [self] (WaitCallbackData aData) { + if (aData.is<MediaData::Type>()) { + self->RequestVideoData(); + } + }); +} + +void +AccurateSeekTask::CancelCallbacks() +{ + AssertOwnerThread(); + mAudioCallback.DisconnectIfExists(); + mVideoCallback.DisconnectIfExists(); + mAudioWaitCallback.DisconnectIfExists(); + mVideoWaitCallback.DisconnectIfExists(); +} +} // namespace mozilla |