Bug 1224973 - Part 5: Implement suspend decoding for background video. r?jwwang draft
authorDan Glastonbury <dglastonbury@mozilla.com>
Mon, 18 Apr 2016 16:33:52 +1000
changeset 363160 143e4346c3ee4e101e37657cd66335e81a225c9c
parent 363159 63013b3800f4741682a7c836eecc8dc4ee42a32a
child 519957 04605a80a36582190a5f419600503f6d67ae04e9
push id17118
push userbmo:dglastonbury@mozilla.com
push dateWed, 04 May 2016 02:49:28 +0000
reviewersjwwang
bugs1224973
milestone49.0a1
Bug 1224973 - Part 5: Implement suspend decoding for background video. r?jwwang Implemented by short-circuiting calls to RequestVideoData in MDSM so no frames are decoded. Resuming playback when video moves to foreground by using the SeekTask/SeekJob/Seek in MDSM with result of GetMediaTime(). Special consideration is made to only seek the video part of Seek() to remove an audible glitch in the audio playback when the video becomes visible again. MozReview-Commit-ID: 7YFDTanslXu
dom/media/MediaDecoderReader.cpp
dom/media/MediaDecoderReader.h
dom/media/MediaDecoderReaderWrapper.cpp
dom/media/MediaDecoderReaderWrapper.h
dom/media/MediaDecoderStateMachine.cpp
dom/media/MediaDecoderStateMachine.h
dom/media/MediaFormatReader.cpp
dom/media/MediaFormatReader.h
dom/media/SeekJob.cpp
dom/media/SeekJob.h
dom/media/SeekTarget.h
dom/media/SeekTask.cpp
dom/media/SeekTask.h
dom/media/ogg/OggReader.cpp
dom/media/ogg/OggReader.h
dom/media/raw/RawReader.cpp
dom/media/raw/RawReader.h
xpcom/threads/MozPromise.h
--- a/dom/media/MediaDecoderReader.cpp
+++ b/dom/media/MediaDecoderReader.cpp
@@ -128,26 +128,27 @@ size_t MediaDecoderReader::SizeOfVideoQu
   return mVideoQueue.GetSize();
 }
 
 size_t MediaDecoderReader::SizeOfAudioQueueInFrames()
 {
   return mAudioQueue.GetSize();
 }
 
-nsresult MediaDecoderReader::ResetDecode()
+nsresult MediaDecoderReader::ResetDecode(TargetQueues aQueues /*= AUDIO_VIDEO*/)
 {
   VideoQueue().Reset();
-  AudioQueue().Reset();
+  mVideoDiscontinuity = true;
+  mBaseVideoPromise.RejectIfExists(CANCELED, __func__);
 
-  mAudioDiscontinuity = true;
-  mVideoDiscontinuity = true;
-
-  mBaseAudioPromise.RejectIfExists(CANCELED, __func__);
-  mBaseVideoPromise.RejectIfExists(CANCELED, __func__);
+  if (aQueues == AUDIO_VIDEO) {
+    AudioQueue().Reset();
+    mAudioDiscontinuity = true;
+    mBaseAudioPromise.RejectIfExists(CANCELED, __func__);
+  }
 
   return NS_OK;
 }
 
 RefPtr<MediaDecoderReader::MediaDataPromise>
 MediaDecoderReader::DecodeToFirstVideoData()
 {
   MOZ_ASSERT(OnTaskQueue());
--- a/dom/media/MediaDecoderReader.h
+++ b/dom/media/MediaDecoderReader.h
@@ -68,16 +68,21 @@ class MediaDecoderReader {
 public:
   enum NotDecodedReason {
     END_OF_STREAM,
     DECODE_ERROR,
     WAITING_FOR_DATA,
     CANCELED
   };
 
+  enum TargetQueues {
+    VIDEO_ONLY,
+    AUDIO_VIDEO
+  };
+
   using MetadataPromise =
     MozPromise<RefPtr<MetadataHolder>, ReadMetadataFailureReason, IsExclusive>;
   using MediaDataPromise =
     MozPromise<RefPtr<MediaData>, NotDecodedReason, IsExclusive>;
   using SeekPromise = MozPromise<media::TimeUnit, nsresult, IsExclusive>;
 
   // Note that, conceptually, WaitForData makes sense in a non-exclusive sense.
   // But in the current architecture it's only ever used exclusively (by MDSM),
@@ -120,17 +125,17 @@ public:
   // Request*Data() calls after this is called. Calls to Request*Data()
   // made after this should be processed as usual.
   //
   // Normally this call preceedes a Seek() call, or shutdown.
   //
   // The first samples of every stream produced after a ResetDecode() call
   // *must* be marked as "discontinuities". If it's not, seeking work won't
   // properly!
-  virtual nsresult ResetDecode();
+  virtual nsresult ResetDecode(TargetQueues aQueues = AUDIO_VIDEO);
 
   // Requests one audio sample from the reader.
   //
   // The decode should be performed asynchronously, and the promise should
   // be resolved when it is complete. Don't hold the decoder
   // monitor while calling this, as the implementation may try to wait
   // on something that needs the monitor and deadlock.
   virtual RefPtr<MediaDataPromise> RequestAudioData();
--- a/dom/media/MediaDecoderReaderWrapper.cpp
+++ b/dom/media/MediaDecoderReaderWrapper.cpp
@@ -325,25 +325,27 @@ MediaDecoderReaderWrapper::SetIdle()
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
   nsCOMPtr<nsIRunnable> r =
     NS_NewRunnableMethod(mReader, &MediaDecoderReader::SetIdle);
   mReader->OwnerThread()->Dispatch(r.forget());
 }
 
 void
-MediaDecoderReaderWrapper::ResetDecode()
+MediaDecoderReaderWrapper::ResetDecode(TargetQueues aQueues)
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
 
   mAudioDataRequest.DisconnectIfExists();
   mVideoDataRequest.DisconnectIfExists();
 
   nsCOMPtr<nsIRunnable> r =
-    NS_NewRunnableMethod(mReader, &MediaDecoderReader::ResetDecode);
+    NS_NewRunnableMethodWithArg<TargetQueues>(mReader,
+                                              &MediaDecoderReader::ResetDecode,
+                                              aQueues);
   mReader->OwnerThread()->Dispatch(r.forget());
 }
 
 RefPtr<ShutdownPromise>
 MediaDecoderReaderWrapper::Shutdown()
 {
   MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
   MOZ_ASSERT(!mRequestAudioDataCB);
--- a/dom/media/MediaDecoderReaderWrapper.h
+++ b/dom/media/MediaDecoderReaderWrapper.h
@@ -27,16 +27,17 @@ typedef MozPromise<bool, bool, /* isExcl
  * is passed to the underlying reader.
  */
 class MediaDecoderReaderWrapper {
   typedef MediaDecoderReader::MetadataPromise MetadataPromise;
   typedef MediaDecoderReader::MediaDataPromise MediaDataPromise;
   typedef MediaDecoderReader::SeekPromise SeekPromise;
   typedef MediaDecoderReader::WaitForDataPromise WaitForDataPromise;
   typedef MediaDecoderReader::BufferedUpdatePromise BufferedUpdatePromise;
+  typedef MediaDecoderReader::TargetQueues TargetQueues;
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDecoderReaderWrapper);
 
   /*
    * Type 1: void(MediaData*)
    *         void(RefPtr<MediaData>)
    */
   template <typename T>
   class ArgType1CheckHelper {
@@ -250,17 +251,17 @@ public:
 
   RefPtr<SeekPromise> Seek(SeekTarget aTarget, media::TimeUnit aEndTime);
   RefPtr<WaitForDataPromise> WaitForData(MediaData::Type aType);
   RefPtr<BufferedUpdatePromise> UpdateBufferedWithPromise();
   RefPtr<ShutdownPromise> Shutdown();
 
   void ReleaseMediaResources();
   void SetIdle();
-  void ResetDecode();
+  void ResetDecode(TargetQueues aQueues);
 
   nsresult Init() { return mReader->Init(); }
   bool IsWaitForDataSupported() const { return mReader->IsWaitForDataSupported(); }
   bool IsAsync() const { return mReader->IsAsync(); }
   bool UseBufferingHeuristics() const { return mReader->UseBufferingHeuristics(); }
   bool ForceZeroStartTime() const { return mReader->ForceZeroStartTime(); }
 
   bool VideoIsHardwareAccelerated() const {
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -477,16 +477,20 @@ bool MediaDecoderStateMachine::HaveEnoug
   // at which the DecodedStream is playing.
   return true;
 }
 
 bool MediaDecoderStateMachine::HaveEnoughDecodedVideo()
 {
   MOZ_ASSERT(OnTaskQueue());
 
+  if (IsVideoDecodeSuspended()) {
+    return true;
+  }
+
   if (VideoQueue().GetSize() == 0) {
     return false;
   }
 
   if (VideoQueue().GetSize() - 1 < GetAmpleVideoFrames() * mPlaybackRate) {
     return false;
   }
 
@@ -1342,22 +1346,87 @@ void MediaDecoderStateMachine::PlayState
     StartDecoding();
   }
 
   ScheduleStateMachine();
 }
 
 void MediaDecoderStateMachine::VisibilityChanged()
 {
-  DECODER_LOG("VisibilityChanged: is visible = %c", mIsVisible ? 'T' : 'F');
-
+  MOZ_ASSERT(OnTaskQueue());
+  DECODER_LOG("VisibilityChanged: is visible = %d", mIsVisible.Ref());
+
+  // Not suspending background videos so there's nothing to do.
   if (!sSuspendBackgroundVideos) {
-    // Not suspending background videos so there's nothing to do.
+    return;
+  }
+
+  // If not transitioning to visible and not playing then there's
+  // nothing to do.
+  if (!mIsVisible || mPlayState != MediaDecoder::PLAY_STATE_PLAYING) {
+    return;
+  }
+
+  // If an existing seek is in flight don't bother creating a new one to catch
+  // up.
+  if (mSeekTask || mQueuedSeek.Exists()) {
     return;
   }
+
+  // Start video-only seek to the current time...
+  InitiateVideoDecodeRecoverySeek();
+}
+
+// InitiateVideoDecodeRecoverySeek is responsible for setting up a video-only
+// seek using the seek task. When suspension of decoding for videos that are in
+// background tabs (ie. invisible) is enabled, the audio keeps playing and when
+// switching back to decoding video, it is highly desirable to not cause the
+// audio to pause as the video is seeked else there be a noticeable audio glitch
+// as the tab becomes visible.
+void MediaDecoderStateMachine::InitiateVideoDecodeRecoverySeek()
+{
+  MOZ_ASSERT(OnTaskQueue());
+
+  SeekJob seekJob;
+  seekJob.mTarget = SeekTarget(GetMediaTime(),
+                               SeekTarget::Type::AccurateVideoOnly,
+                               MediaDecoderEventVisibility::Suppressed);
+
+  SetState(DECODER_STATE_SEEKING);
+
+  // Discard the existing seek task.
+  DiscardSeekTaskIfExist();
+
+  mSeekTaskRequest.DisconnectIfExists();
+
+  // SeekTask will register its callbacks to MediaDecoderReaderWrapper.
+  CancelMediaDecoderReaderWrapperCallback();
+
+  // Create a new SeekTask instance for the incoming seek task.
+  mSeekTask = SeekTask::CreateSeekTask(mDecoderID, OwnerThread(),
+                                       mReader.get(), Move(seekJob),
+                                       mInfo, Duration(), GetMediaTime());
+
+  mOnSeekingStart.Notify(MediaDecoderEventVisibility::Suppressed);
+
+  // Reset our state machine and decoding pipeline before seeking.
+  if (mSeekTask->NeedToResetMDSM()) {
+    Reset(MediaDecoderReader::VIDEO_ONLY);
+  }
+
+  // Do the seek.
+  mSeekTaskRequest.Begin(
+    mSeekTask->Seek(Duration())->Then(OwnerThread(), __func__, this,
+                                      &MediaDecoderStateMachine::OnSeekTaskResolved,
+                                      &MediaDecoderStateMachine::OnSeekTaskRejected));
+  // Nobody is listening to this as OnSeekTaskResolved handles what is
+  // required but the promise needs to exist or SeekJob::Exists() will
+  // assert.
+  RefPtr<MediaDecoder::SeekPromise> unused =
+    mSeekTask->GetSeekJob().mPromise.Ensure(__func__);
 }
 
 void MediaDecoderStateMachine::BufferedRangeUpdated()
 {
   MOZ_ASSERT(OnTaskQueue());
 
   // While playing an unseekable stream of unknown duration, mObservedDuration
   // is updated (in AdvanceFrame()) as we play. But if data is being downloaded
@@ -1697,16 +1766,23 @@ MediaDecoderStateMachine::EnsureVideoDec
   SAMPLE_LOG("EnsureVideoDecodeTaskQueued isDecoding=%d status=%s",
              IsVideoDecoding(), VideoRequestStatus());
 
   if (mState != DECODER_STATE_DECODING &&
       mState != DECODER_STATE_BUFFERING) {
     return NS_OK;
   }
 
+  if (IsVideoDecodeSuspended() && !IsDecodingFirstFrame()) {
+    // The element is invisible and background videos should be suspended.
+    // If the first frame has already been decoded, don't request anymore video
+    // frames.
+    return NS_OK;
+  }
+
   if (!IsVideoDecoding() || mReader->IsRequestingVidoeData() ||
       mVideoWaitRequest.Exists()) {
     return NS_OK;
   }
 
   RequestVideoData();
   return NS_OK;
 }
@@ -2277,49 +2353,52 @@ nsresult MediaDecoderStateMachine::RunSt
       return NS_OK;
     }
   }
 
   return NS_OK;
 }
 
 void
-MediaDecoderStateMachine::Reset()
+MediaDecoderStateMachine::Reset(MediaDecoderReader::TargetQueues aQueues /*= AUDIO_VIDEO*/)
 {
   MOZ_ASSERT(OnTaskQueue());
   DECODER_LOG("MediaDecoderStateMachine::Reset");
 
   // We should be resetting because we're seeking, shutting down, or entering
   // dormant state. We could also be in the process of going dormant, and have
   // just switched to exiting dormant before we finished entering dormant,
   // hence the DECODING_NONE case below.
   MOZ_ASSERT(IsShutdown() ||
              mState == DECODER_STATE_SEEKING ||
              mState == DECODER_STATE_DORMANT);
 
-  // Stop the audio thread. Otherwise, MediaSink might be accessing AudioQueue
-  // outside of the decoder monitor while we are clearing the queue and causes
-  // crash for no samples to be popped.
-  StopMediaSink();
 
   mDecodedVideoEndTime = 0;
-  mDecodedAudioEndTime = 0;
-  mAudioCompleted = false;
   mVideoCompleted = false;
-  AudioQueue().Reset();
   VideoQueue().Reset();
+  mVideoWaitRequest.DisconnectIfExists();
+
+  if (aQueues == MediaDecoderReader::AUDIO_VIDEO) {
+    // Stop the audio thread. Otherwise, MediaSink might be accessing AudioQueue
+    // outside of the decoder monitor while we are clearing the queue and causes
+    // crash for no samples to be popped.
+    StopMediaSink();
+    mDecodedAudioEndTime = 0;
+    mAudioCompleted = false;
+    AudioQueue().Reset();
+    mAudioWaitRequest.DisconnectIfExists();
+  }
 
   mMetadataRequest.DisconnectIfExists();
-  mAudioWaitRequest.DisconnectIfExists();
-  mVideoWaitRequest.DisconnectIfExists();
   mSeekTaskRequest.DisconnectIfExists();
 
   mPlaybackOffset = 0;
 
-  mReader->ResetDecode();
+  mReader->ResetDecode(aQueues);
 }
 
 int64_t
 MediaDecoderStateMachine::GetClock(TimeStamp* aTimeStamp) const
 {
   MOZ_ASSERT(OnTaskQueue());
   int64_t clockTime = mMediaSink->GetPosition(aTimeStamp);
   NS_ASSERTION(GetMediaTime() <= clockTime, "Clock should go forwards.");
@@ -2515,16 +2594,22 @@ bool MediaDecoderStateMachine::OnTaskQue
 }
 
 bool MediaDecoderStateMachine::IsStateMachineScheduled() const
 {
   MOZ_ASSERT(OnTaskQueue());
   return mDispatchedStateMachine || mDelayedScheduler.IsScheduled();
 }
 
+bool MediaDecoderStateMachine::IsVideoDecodeSuspended() const
+{
+  MOZ_ASSERT(OnTaskQueue());
+  return sSuspendBackgroundVideos && !mIsVisible;
+}
+
 void
 MediaDecoderStateMachine::LogicalPlaybackRateChanged()
 {
   MOZ_ASSERT(OnTaskQueue());
 
   if (mLogicalPlaybackRate == 0) {
     // This case is handled in MediaDecoder by pausing playback.
     return;
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -363,17 +363,17 @@ private:
   void OnVideoNotDecoded(MediaDecoderReader::NotDecodedReason aReason)
   {
     MOZ_ASSERT(OnTaskQueue());
     OnNotDecoded(MediaData::VIDEO_DATA, aReason);
   }
 
   // Resets all state related to decoding and playback, emptying all buffers
   // and aborting all pending operations on the decode task queue.
-  void Reset();
+  void Reset(MediaDecoderReader::TargetQueues aQueues = MediaDecoderReader::AUDIO_VIDEO);
 
 protected:
   virtual ~MediaDecoderStateMachine();
 
   void SetState(State aState);
 
   void BufferedRangeUpdated();
 
@@ -504,20 +504,25 @@ protected:
 
   // Dispatches a LoadedMetadataEvent.
   // This is threadsafe and can be called on any thread.
   // The decoder monitor must be held.
   void EnqueueLoadedMetadataEvent();
 
   void EnqueueFirstFrameLoadedEvent();
 
-  // Clears any previous seeking state and initiates a new see on the decoder.
+  // Clears any previous seeking state and initiates a new seek on the decoder.
   // The decoder monitor must be held.
   void InitiateSeek(SeekJob aSeekJob);
 
+  // Clears any previous seeking state and initiates a video-only seek on the
+  // decoder to catch up the video to the current audio position, when recovering
+  // from video decoding being suspended in background.
+  void InitiateVideoDecodeRecoverySeek();
+
   nsresult DispatchAudioDecodeTaskIfNeeded();
 
   // Ensures a task to decode audio has been dispatched to the decode task queue.
   // If a task to decode has already been dispatched, this does nothing,
   // otherwise this dispatches a task to do the decode.
   // This is called on the state machine or decode threads.
   // The decoder monitor must be held.
   nsresult EnsureAudioDecodeTaskQueued();
@@ -590,16 +595,20 @@ protected:
 
   bool IsStateMachineScheduled() const;
 
   // Returns true if we're not playing and the decode thread has filled its
   // decode buffers and is waiting. We can shut the decode thread down in this
   // case as it may not be needed again.
   bool IsPausedAndDecoderWaiting();
 
+  // Returns true if the video decoding is suspended because the element is not
+  // visible
+  bool IsVideoDecodeSuspended() const;
+
   // These return true if the respective stream's decode has not yet reached
   // the end of stream.
   bool IsAudioDecoding();
   bool IsVideoDecoding();
 
 private:
   // Resolved by the MediaSink to signal that all audio/video outstanding
   // work is complete and identify which part(a/v) of the sink is shutting down.
@@ -751,17 +760,17 @@ private:
 
   // If we're quick buffering, we'll remain in buffering mode while we have less than
   // QUICK_BUFFERING_LOW_DATA_USECS of decoded data available.
   int64_t mQuickBufferingLowDataThresholdUsecs;
 
   // At the start of decoding we want to "preroll" the decode until we've
   // got a few frames decoded before we consider whether decode is falling
   // behind. Otherwise our "we're falling behind" logic will trigger
-  // unneccessarily if we start playing as soon as the first sample is
+  // unnecessarily if we start playing as soon as the first sample is
   // decoded. These two fields store how many video frames and audio
   // samples we must consume before are considered to be finished prerolling.
   uint32_t AudioPrerollUsecs() const
   {
     MOZ_ASSERT(OnTaskQueue());
     return IsRealTime() ? 0 : mAmpleAudioThresholdUsecs / 2;
   }
 
@@ -776,17 +785,18 @@ private:
     MOZ_ASSERT(OnTaskQueue());
     return !IsAudioDecoding() ||
         GetDecodedAudioDuration() >= AudioPrerollUsecs() * mPlaybackRate;
   }
 
   bool DonePrerollingVideo()
   {
     MOZ_ASSERT(OnTaskQueue());
-    return !IsVideoDecoding() ||
+    return !mIsVisible ||
+        !IsVideoDecoding() ||
         static_cast<uint32_t>(VideoQueue().GetSize()) >=
             VideoPrerollFrames() * mPlaybackRate + 1;
   }
 
   void StopPrerollingAudio()
   {
     MOZ_ASSERT(OnTaskQueue());
     if (mIsAudioPrerolling) {
@@ -857,17 +867,17 @@ private:
   // Note that the odd semantics here are designed to replicate the current
   // behavior where we notify the decoder each time we come out of dormant, but
   // send suppressed event visibility for those cases. This code can probably be
   // simplified.
   bool mNotifyMetadataBeforeFirstFrame;
 
   // True if we've dispatched an event to the decode task queue to call
   // DecodeThreadRun(). We use this flag to prevent us from dispatching
-  // unneccessary runnables, since the decode thread runs in a loop.
+  // unnecessary runnables, since the decode thread runs in a loop.
   bool mDispatchedEventToDecode;
 
   // If this is true while we're in buffering mode, we can exit early,
   // as it's likely we may be able to playback. This happens when we enter
   // buffering mode soon after the decode starts, because the decode-ahead
   // ran fast enough to exhaust all data while the download is starting up.
   // Synchronised via decoder monitor.
   bool mQuickBuffering;
@@ -899,17 +909,17 @@ private:
   nsAutoPtr<MetadataTags> mMetadataTags;
 
   mozilla::MediaMetadataManager mMetadataManager;
 
   // Track our request to update the buffered ranges
   MozPromiseRequestHolder<MediaDecoderReader::BufferedUpdatePromise> mBufferedUpdateRequest;
 
   // True if we need to call FinishDecodeFirstFrame() upon frame decoding
-  // successeeding.
+  // succeeding.
   bool mDecodingFirstFrame;
 
   // True if we are back from DECODER_STATE_DORMANT state and
   // LoadedMetadataEvent was already sent.
   bool mSentLoadedMetadataEvent;
   // True if we are back from DECODER_STATE_DORMANT state and
   // FirstFrameLoadedEvent was already sent, then we can skip
   // SetStartTime because the mStartTime already set before. Also we don't need
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -1271,17 +1271,17 @@ MediaFormatReader::WaitForData(MediaData
     return WaitForDataPromise::CreateAndResolve(decoder.mType, __func__);
   }
   RefPtr<WaitForDataPromise> p = decoder.mWaitingPromise.Ensure(__func__);
   ScheduleUpdate(trackType);
   return p;
 }
 
 nsresult
-MediaFormatReader::ResetDecode()
+MediaFormatReader::ResetDecode(TargetQueues aQueues)
 {
   MOZ_ASSERT(OnTaskQueue());
   LOGV("");
 
   mSeekPromise.RejectIfExists(NS_OK, __func__);
   mSkipRequest.DisconnectIfExists();
 
   // Do the same for any data wait promises.
@@ -1293,24 +1293,25 @@ MediaFormatReader::ResetDecode()
 
   if (HasVideo()) {
     mVideo.ResetDemuxer();
     Flush(TrackInfo::kVideoTrack);
     if (mVideo.HasPromise()) {
       mVideo.RejectPromise(CANCELED, __func__);
     }
   }
-  if (HasAudio()) {
+
+  if (HasAudio() && aQueues == AUDIO_VIDEO) {
     mAudio.ResetDemuxer();
     Flush(TrackInfo::kAudioTrack);
     if (mAudio.HasPromise()) {
       mAudio.RejectPromise(CANCELED, __func__);
     }
   }
-  return MediaDecoderReader::ResetDecode();
+  return MediaDecoderReader::ResetDecode(aQueues);
 }
 
 void
 MediaFormatReader::Output(TrackType aTrack, MediaData* aSample)
 {
   LOGV("Decoded %s sample time=%lld timecode=%lld kf=%d dur=%lld",
        TrackTypeToStr(aTrack), aSample->mTime, aSample->mTimecode,
        aSample->mKeyframe, aSample->mDuration);
@@ -1478,17 +1479,17 @@ MediaFormatReader::Seek(SeekTarget aTarg
 
 void
 MediaFormatReader::AttemptSeek()
 {
   MOZ_ASSERT(OnTaskQueue());
   if (mPendingSeekTime.isNothing()) {
     return;
   }
-  // An internal seek may be pending due to Seek queueing multiple tasks calling
+  // An internal seek may be pending due to Seek queuing multiple tasks calling
   // AttemptSeek ; we can ignore those by resetting any pending demuxer's seek.
   mAudio.mSeekRequest.DisconnectIfExists();
   mVideo.mSeekRequest.DisconnectIfExists();
   if (HasVideo()) {
     DoVideoSeek();
   } else if (HasAudio()) {
     DoAudioSeek();
   } else {
@@ -1558,19 +1559,20 @@ MediaFormatReader::DoVideoSeek()
 
 void
 MediaFormatReader::OnVideoSeekCompleted(media::TimeUnit aTime)
 {
   MOZ_ASSERT(OnTaskQueue());
   LOGV("Video seeked to %lld", aTime.ToMicroseconds());
   mVideo.mSeekRequest.Complete();
 
-  if (HasAudio()) {
-    MOZ_ASSERT(mPendingSeekTime.isSome() && mOriginalSeekTarget.isSome());
-    if (mOriginalSeekTarget.ref().IsFast()) {
+  MOZ_ASSERT(mOriginalSeekTarget.isSome());
+  if (HasAudio() && !mOriginalSeekTarget->IsVideoOnly()) {
+    MOZ_ASSERT(mPendingSeekTime.isSome());
+    if (mOriginalSeekTarget->IsFast()) {
       // We are performing a fast seek. We need to seek audio to where the
       // video seeked to, to ensure proper A/V sync once playback resume.
       mPendingSeekTime = Some(aTime);
     }
     DoAudioSeek();
   } else {
     mPendingSeekTime.reset();
     mSeekPromise.Resolve(aTime, __func__);
--- a/dom/media/MediaFormatReader.h
+++ b/dom/media/MediaFormatReader.h
@@ -57,17 +57,17 @@ public:
 
   RefPtr<BufferedUpdatePromise> UpdateBufferedWithPromise() override;
 
   bool ForceZeroStartTime() const override;
 
   // For Media Resource Management
   void ReleaseMediaResources() override;
 
-  nsresult ResetDecode() override;
+  nsresult ResetDecode(TargetQueues aQueues) override;
 
   RefPtr<ShutdownPromise> Shutdown() override;
 
   bool IsAsync() const override { return true; }
 
   bool VideoIsHardwareAccelerated() const override;
 
   bool IsWaitForDataSupported() const override { return true; }
@@ -314,17 +314,17 @@ private:
     uint64_t mNumSamplesInput;
     uint64_t mNumSamplesOutput;
     uint64_t mNumSamplesOutputTotal;
     uint64_t mNumSamplesSkippedTotal;
 
     uint64_t mNumSamplesOutputTotalSinceTelemetry;
     uint64_t mNumSamplesSkippedTotalSinceTelemetry;
 
-    // These get overriden in the templated concrete class.
+    // These get overridden in the templated concrete class.
     // Indicate if we have a pending promise for decoded frame.
     // Rejecting the promise will stop the reader from decoding ahead.
     virtual bool HasPromise() = 0;
     virtual void RejectPromise(MediaDecoderReader::NotDecodedReason aReason,
                                const char* aMethodName) = 0;
 
     void ResetDemuxer()
     {
--- a/dom/media/SeekJob.cpp
+++ b/dom/media/SeekJob.cpp
@@ -28,17 +28,17 @@ SeekJob& SeekJob::operator=(SeekJob&& aO
 {
   MOZ_DIAGNOSTIC_ASSERT(!Exists());
   mTarget = aOther.mTarget;
   aOther.mTarget.Reset();
   mPromise = Move(aOther.mPromise);
   return *this;
 }
 
-bool SeekJob::Exists()
+bool SeekJob::Exists() const
 {
   MOZ_ASSERT(mTarget.IsValid() == !mPromise.IsEmpty());
   return mTarget.IsValid();
 }
 
 void SeekJob::Resolve(bool aAtEnd, const char* aCallSite)
 {
   MediaDecoder::SeekResolveValue val(aAtEnd, mTarget.mEventVisibility);
--- a/dom/media/SeekJob.h
+++ b/dom/media/SeekJob.h
@@ -16,17 +16,17 @@ namespace mozilla {
 
 struct SeekJob {
   SeekJob();
 
   SeekJob(SeekJob&& aOther);
 
   SeekJob& operator=(SeekJob&& aOther);
 
-  bool Exists();
+  bool Exists() const;
 
   void Resolve(bool aAtEnd, const char* aCallSite);
 
   void RejectIfExists(const char* aCallSite);
 
   ~SeekJob();
 
   SeekTarget mTarget;
--- a/dom/media/SeekTarget.h
+++ b/dom/media/SeekTarget.h
@@ -12,22 +12,24 @@
 namespace mozilla {
 
 enum class MediaDecoderEventVisibility : int8_t {
   Observable,
   Suppressed
 };
 
 // Stores the seek target; the time to seek to, and whether an Accurate,
-// or "Fast" (nearest keyframe) seek was requested.
+// "Fast" (nearest keyframe), or "Video Only" (no audio seek) seek was
+// requested.
 struct SeekTarget {
   enum Type {
     Invalid,
     PrevSyncPoint,
-    Accurate
+    Accurate,
+    AccurateVideoOnly,
   };
   SeekTarget()
     : mEventVisibility(MediaDecoderEventVisibility::Observable)
     , mTime(media::TimeUnit::Invalid())
     , mType(SeekTarget::Invalid)
   {
   }
   SeekTarget(int64_t aTimeUsecs,
@@ -73,23 +75,26 @@ struct SeekTarget {
     mType = aType;
   }
   bool IsFast() const {
     return mType == SeekTarget::Type::PrevSyncPoint;
   }
   bool IsAccurate() const {
     return mType == SeekTarget::Type::Accurate;
   }
+  bool IsVideoOnly() const {
+    return mType == SeekTarget::Type::AccurateVideoOnly;
+  }
 
   MediaDecoderEventVisibility mEventVisibility;
 
 private:
   // Seek target time.
   media::TimeUnit mTime;
   // Whether we should seek "Fast", or "Accurate".
-  // "Fast" seeks to the seek point preceeding mTime, whereas
+  // "Fast" seeks to the seek point preceding mTime, whereas
   // "Accurate" seeks as close as possible to mTime.
   Type mType;
 };
 
 } // namespace mozilla
 
 #endif /* SEEK_TARGET_H */
--- a/dom/media/SeekTask.cpp
+++ b/dom/media/SeekTask.cpp
@@ -172,17 +172,17 @@ SeekTask::NeedToResetMDSM() const
 
 SeekJob&
 SeekTask::GetSeekJob()
 {
   return mSeekJob;
 }
 
 bool
-SeekTask::Exists()
+SeekTask::Exists() const
 {
   return mSeekJob.Exists();
 }
 
 RefPtr<SeekTask::SeekTaskPromise>
 SeekTask::Seek(const media::TimeUnit& aDuration)
 {
   AssertOwnerThread();
@@ -417,16 +417,17 @@ bool
 SeekTask::IsAudioSeekComplete()
 {
   AssertOwnerThread();
 
   SAMPLE_LOG("IsAudioSeekComplete() curTarVal=%d mAudDis=%d aqFin=%d aqSz=%d",
       mSeekJob.Exists(), mDropAudioUntilNextDiscontinuity, mIsAudioQueueFinished, !!mSeekedAudioData);
   return
     !HasAudio() ||
+    mSeekJob.mTarget.IsVideoOnly() ||
     (Exists() && !mDropAudioUntilNextDiscontinuity &&
      (mIsAudioQueueFinished || mSeekedAudioData));
 }
 
 bool
 SeekTask::IsVideoSeekComplete()
 {
   AssertOwnerThread();
@@ -473,18 +474,20 @@ SeekTask::CheckIfSeekComplete()
 
 void
 SeekTask::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.
-  EnsureAudioDecodeTaskQueued();
   EnsureVideoDecodeTaskQueued();
+  if (!mSeekJob.mTarget.IsVideoOnly()) {
+    EnsureAudioDecodeTaskQueued();
+  }
 }
 
 void
 SeekTask::OnSeekRejected(nsresult aResult)
 {
   AssertOwnerThread();
   mSeekRequest.Complete();
   MOZ_ASSERT(NS_FAILED(aResult), "Cancels should also disconnect mSeekRequest");
--- a/dom/media/SeekTask.h
+++ b/dom/media/SeekTask.h
@@ -58,17 +58,17 @@ public:
   virtual void Discard();
 
   virtual RefPtr<SeekTaskPromise> Seek(const media::TimeUnit& aDuration);
 
   virtual bool NeedToResetMDSM() const;
 
   SeekJob& GetSeekJob();
 
-  bool Exists();
+  bool Exists() const;
 
 protected:
   SeekTask(const void* aDecoderID,
            AbstractThread* aThread,
            MediaDecoderReaderWrapper* aReader,
            SeekJob&& aSeekJob,
            const MediaInfo& aInfo,
            const media::TimeUnit& aDuration,
--- a/dom/media/ogg/OggReader.cpp
+++ b/dom/media/ogg/OggReader.cpp
@@ -164,27 +164,27 @@ OggReader::~OggReader()
 }
 
 nsresult OggReader::Init() {
   int ret = ogg_sync_init(&mOggState);
   NS_ENSURE_TRUE(ret == 0, NS_ERROR_FAILURE);
   return NS_OK;
 }
 
-nsresult OggReader::ResetDecode()
+nsresult OggReader::ResetDecode(TargetQueues aQueues)
 {
-  return ResetDecode(false);
+  return ResetDecode(false, aQueues);
 }
 
-nsresult OggReader::ResetDecode(bool start)
+nsresult OggReader::ResetDecode(bool start, TargetQueues aQueues)
 {
   MOZ_ASSERT(OnTaskQueue());
   nsresult res = NS_OK;
 
-  if (NS_FAILED(MediaDecoderReader::ResetDecode())) {
+  if (NS_FAILED(MediaDecoderReader::ResetDecode(aQueues))) {
     res = NS_ERROR_FAILURE;
   }
 
   // Discard any previously buffered packets/pages.
   ogg_sync_reset(&mOggState);
   if (mVorbisState && NS_FAILED(mVorbisState->Reset())) {
     res = NS_ERROR_FAILURE;
   }
--- a/dom/media/ogg/OggReader.h
+++ b/dom/media/ogg/OggReader.h
@@ -46,17 +46,17 @@ class OggReader final : public MediaDeco
 public:
   explicit OggReader(AbstractMediaDecoder* aDecoder);
 
 protected:
   ~OggReader();
 
 public:
   nsresult Init() override;
-  nsresult ResetDecode() override;
+  nsresult ResetDecode(TargetQueues aQueues = AUDIO_VIDEO) override;
   bool DecodeAudioData() override;
 
   // If the Theora granulepos has not been captured, it may read several packets
   // until one with a granulepos has been captured, to ensure that all packets
   // read have valid time info.
   bool DecodeVideoFrame(bool &aKeyframeSkip, int64_t aTimeThreshold) override;
 
   nsresult ReadMetadata(MediaInfo* aInfo, MetadataTags** aTags) override;
@@ -81,25 +81,25 @@ private:
   RefPtr<AudioData> SyncDecodeToFirstAudioData();
   RefPtr<VideoData> SyncDecodeToFirstVideoData();
 
   // This monitor should be taken when reading or writing to mIsChained.
   ReentrantMonitor mMonitor;
 
   // Specialized Reset() method to signal if the seek is
   // to the start of the stream.
-  nsresult ResetDecode(bool start);
+  nsresult ResetDecode(bool start, TargetQueues aQueues = AUDIO_VIDEO);
 
   nsresult SeekInternal(int64_t aTime, int64_t aEndTime);
 
   bool HasSkeleton() {
     return mSkeletonState != 0 && mSkeletonState->mActive;
   }
 
-  // Seeks to the keyframe preceeding the target time using available
+  // Seeks to the keyframe preceding the target time using available
   // keyframe indexes.
   enum IndexedSeekResult {
     SEEK_OK,          // Success.
     SEEK_INDEX_FAIL,  // Failure due to no index, or invalid index.
     SEEK_FATAL_ERROR  // Error returned by a stream operation.
   };
   IndexedSeekResult SeekToKeyframeUsingIndex(int64_t aTarget);
 
--- a/dom/media/raw/RawReader.cpp
+++ b/dom/media/raw/RawReader.cpp
@@ -22,20 +22,20 @@ RawReader::RawReader(AbstractMediaDecode
   MOZ_COUNT_CTOR(RawReader);
 }
 
 RawReader::~RawReader()
 {
   MOZ_COUNT_DTOR(RawReader);
 }
 
-nsresult RawReader::ResetDecode()
+nsresult RawReader::ResetDecode(TargetQueues aQueues)
 {
   mCurrentFrame = 0;
-  return MediaDecoderReader::ResetDecode();
+  return MediaDecoderReader::ResetDecode(aQueues);
 }
 
 nsresult RawReader::ReadMetadata(MediaInfo* aInfo,
                                  MetadataTags** aTags)
 {
   MOZ_ASSERT(OnTaskQueue());
 
   if (!ReadFromResource(reinterpret_cast<uint8_t*>(&mMetadata),
--- a/dom/media/raw/RawReader.h
+++ b/dom/media/raw/RawReader.h
@@ -15,17 +15,17 @@ class RawReader : public MediaDecoderRea
 {
 public:
   explicit RawReader(AbstractMediaDecoder* aDecoder);
 
 protected:
   ~RawReader();
 
 public:
-  nsresult ResetDecode() override;
+  nsresult ResetDecode(TargetQueues aQueues) override;
   bool DecodeAudioData() override;
 
   bool DecodeVideoFrame(bool &aKeyframeSkip,
                         int64_t aTimeThreshold) override;
 
   nsresult ReadMetadata(MediaInfo* aInfo,
                         MetadataTags** aTags) override;
   RefPtr<SeekPromise> Seek(SeekTarget aTarget, int64_t aEndTime) override;
--- a/xpcom/threads/MozPromise.h
+++ b/xpcom/threads/MozPromise.h
@@ -780,17 +780,17 @@ public:
     }
     RefPtr<PromiseType> p = mPromise.get();
     return p.forget();
   }
 
   // Provide a Monitor that should always be held when accessing this instance.
   void SetMonitor(Monitor* aMonitor) { mMonitor = aMonitor; }
 
-  bool IsEmpty()
+  bool IsEmpty() const
   {
     if (mMonitor) {
       mMonitor->AssertCurrentThreadOwns();
     }
     return !mPromise;
   }
 
   already_AddRefed<typename PromiseType::Private> Steal()