Bug 1135170 - Rewrite the MediaDecoder-to-MediaDecoderStateMachine interface to be Promise-based. r=mattwoodrow
authorBobby Holley <bobbyholley@gmail.com>
Mon, 02 Mar 2015 16:46:32 -0800
changeset 232372 f256c4348bacfed6e8ff4f8740c8b5b981e2a09c
parent 232371 537948bc60078543196f98bce042c6dba34cb199
child 232373 995c97c870a23d6d9e7942d1669ddc4317d3dcb1
push id56528
push userbobbyholley@gmail.com
push dateSat, 07 Mar 2015 03:18:56 +0000
treeherdermozilla-inbound@cb5ec48a3391 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmattwoodrow
bugs1135170
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1135170 - Rewrite the MediaDecoder-to-MediaDecoderStateMachine interface to be Promise-based. r=mattwoodrow
dom/media/MediaDecoder.cpp
dom/media/MediaDecoder.h
dom/media/MediaDecoderStateMachine.cpp
dom/media/MediaDecoderStateMachine.h
dom/media/MediaPromise.h
dom/media/omx/AudioOffloadPlayer.cpp
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -789,16 +789,17 @@ nsresult MediaDecoder::Seek(double aTime
   NS_ENSURE_SUCCESS(rv, rv);
 
   mRequestedSeekTarget = SeekTarget(timeUsecs, aSeekType);
   mCurrentTime = aTime;
   mWasEndedWhenEnteredDormant = false;
 
   // If we are already in the seeking state, the new seek overrides the old one.
   if (mPlayState != PLAY_STATE_LOADING) {
+    mSeekRequest.DisconnectIfExists();
     bool paused = false;
     if (mOwner) {
       paused = mOwner->GetPaused();
     }
     mNextState = paused ? PLAY_STATE_PAUSED : PLAY_STATE_PLAYING;
     PinForSeek();
     ChangeState(PLAY_STATE_SEEKING);
   }
@@ -1204,53 +1205,17 @@ void MediaDecoder::UpdateReadyStateForDa
   if (!mOwner || mShuttingDown || !mDecoderStateMachine) {
     return;
   }
   MediaDecoderOwner::NextFrameStatus frameStatus =
     mDecoderStateMachine->GetNextFrameStatus();
   mOwner->UpdateReadyStateForData(frameStatus);
 }
 
-void MediaDecoder::SeekingStopped(MediaDecoderEventVisibility aEventVisibility)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-
-  if (mShuttingDown)
-    return;
-
-  bool seekWasAborted = false;
-  {
-    ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
-
-    // An additional seek was requested while the current seek was
-    // in operation.
-    if (mRequestedSeekTarget.IsValid()) {
-      ChangeState(PLAY_STATE_SEEKING);
-      seekWasAborted = true;
-    } else {
-      UnpinForSeek();
-      if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
-        ChangeState(mNextState);
-      }
-    }
-  }
-
-  PlaybackPositionChanged(aEventVisibility);
-
-  if (mOwner) {
-    UpdateReadyStateForData();
-    if (!seekWasAborted && (aEventVisibility != MediaDecoderEventVisibility::Suppressed)) {
-      mOwner->SeekCompleted();
-    }
-  }
-}
-
-// This is called when seeking stopped *and* we're at the end of the
-// media.
-void MediaDecoder::SeekingStoppedAtEnd(MediaDecoderEventVisibility aEventVisibility)
+void MediaDecoder::OnSeekResolvedInternal(bool aAtEnd, MediaDecoderEventVisibility aEventVisibility)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (mShuttingDown)
     return;
 
   bool fireEnded = false;
   bool seekWasAborted = false;
@@ -1259,18 +1224,22 @@ void MediaDecoder::SeekingStoppedAtEnd(M
 
     // An additional seek was requested while the current seek was
     // in operation.
     if (mRequestedSeekTarget.IsValid()) {
       ChangeState(PLAY_STATE_SEEKING);
       seekWasAborted = true;
     } else {
       UnpinForSeek();
-      fireEnded = true;
-      ChangeState(PLAY_STATE_ENDED);
+      fireEnded = aAtEnd;
+      if (aAtEnd) {
+        ChangeState(PLAY_STATE_ENDED);
+      } else if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
+        ChangeState(aAtEnd ? PLAY_STATE_ENDED : mNextState);
+      }
     }
   }
 
   PlaybackPositionChanged(aEventVisibility);
 
   if (mOwner) {
     UpdateReadyStateForData();
     if (!seekWasAborted && (aEventVisibility != MediaDecoderEventVisibility::Suppressed)) {
@@ -1343,17 +1312,21 @@ void MediaDecoder::ApplyStateToStateMach
   GetReentrantMonitor().AssertCurrentThreadIn();
 
   if (mDecoderStateMachine) {
     switch (aState) {
       case PLAY_STATE_PLAYING:
         mDecoderStateMachine->Play();
         break;
       case PLAY_STATE_SEEKING:
-        mDecoderStateMachine->Seek(mRequestedSeekTarget);
+        mSeekRequest.Begin(ProxyMediaCall(mDecoderStateMachine->GetStateMachineThread(),
+                                          mDecoderStateMachine.get(), __func__,
+                                          &MediaDecoderStateMachine::Seek, mRequestedSeekTarget)
+          ->RefableThen(NS_GetCurrentThread(), __func__, this,
+                        &MediaDecoder::OnSeekResolved, &MediaDecoder::OnSeekRejected));
         mRequestedSeekTarget.Reset();
         break;
       default:
         // The state machine checks for things like PAUSED in RunStateMachine.
         // Make sure to keep it in the loop.
         ScheduleStateMachineThread();
         break;
     }
--- a/dom/media/MediaDecoder.h
+++ b/dom/media/MediaDecoder.h
@@ -268,16 +268,24 @@ struct SeekTarget {
   Type mType;
   MediaDecoderEventVisibility mEventVisibility;
 };
 
 class MediaDecoder : public nsIObserver,
                      public AbstractMediaDecoder
 {
 public:
+  struct SeekResolveValue {
+    SeekResolveValue(bool aAtEnd, MediaDecoderEventVisibility aEventVisibility)
+      : mAtEnd(aAtEnd), mEventVisibility(aEventVisibility) {}
+    bool mAtEnd;
+    MediaDecoderEventVisibility mEventVisibility;
+  };
+
+  typedef MediaPromise<SeekResolveValue, bool /* aIgnored */, /* IsExclusive = */ true> SeekPromise;
   class DecodedStreamGraphListener;
 
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIOBSERVER
 
   // Enumeration for the valid play states (see mPlayState)
   enum PlayState {
     PLAY_STATE_START,
@@ -790,23 +798,32 @@ public:
   //
   // Acquires the monitor. Call from any thread.
   virtual bool IsExpectingMoreData();
 
   // Called when the video has completed playing.
   // Call on the main thread only.
   void PlaybackEnded();
 
-  // Seeking has stopped. Inform the element on the main
-  // thread.
-  void SeekingStopped(MediaDecoderEventVisibility aEventVisibility = MediaDecoderEventVisibility::Observable);
+  void OnSeekRejected() { mSeekRequest.Complete(); }
+  void OnSeekResolvedInternal(bool aAtEnd, MediaDecoderEventVisibility aEventVisibility);
 
-  // Seeking has stopped at the end of the resource. Inform the element on the main
-  // thread.
-  void SeekingStoppedAtEnd(MediaDecoderEventVisibility aEventVisibility = MediaDecoderEventVisibility::Observable);
+  void OnSeekResolved(SeekResolveValue aVal)
+  {
+    mSeekRequest.Complete();
+    OnSeekResolvedInternal(aVal.mAtEnd, aVal.mEventVisibility);
+  }
+
+#ifdef MOZ_AUDIO_OFFLOAD
+  // Temporary hack - see bug 1139206.
+  void SimulateSeekResolvedForAudioOffload(MediaDecoderEventVisibility aEventVisibility)
+  {
+    OnSeekResolvedInternal(false, aEventVisibility);
+  }
+#endif
 
   // Seeking has started. Inform the element on the main
   // thread.
   void SeekingStarted(MediaDecoderEventVisibility aEventVisibility = MediaDecoderEventVisibility::Observable);
 
   // Called when the backend has changed the current playback
   // position. It dispatches a timeupdate event and invalidates the frame.
   // This must be called on the main thread only.
@@ -1129,16 +1146,18 @@ protected:
   // decode thread.
   // This can only be changed on the main thread while holding the decoder
   // monitor. Thus, it can be safely read while holding the decoder monitor
   // OR on the main thread.
   // If the SeekTarget's IsValid() accessor returns false, then no seek has
   // been requested. When a seek is started this is reset to invalid.
   SeekTarget mRequestedSeekTarget;
 
+  MediaPromiseConsumerHolder<SeekPromise> mSeekRequest;
+
   // True when seeking or otherwise moving the play position around in
   // such a manner that progress event data is inaccurate. This is set
   // during seek and duration operations to prevent the progress indicator
   // from jumping around. Read/Write from any thread. Must have decode monitor
   // locked before accessing.
   bool mIgnoreProgressData;
 
   // True if the stream is infinite (e.g. a webradio).
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -675,33 +675,33 @@ MediaDecoderStateMachine::NeedToDecodeAu
           (mState != DECODER_STATE_SEEKING || mDecodeToSeekTarget)));
 }
 
 bool
 MediaDecoderStateMachine::IsAudioSeekComplete()
 {
   AssertCurrentThreadInMonitor();
   SAMPLE_LOG("IsAudioSeekComplete() curTarVal=%d mAudDis=%d aqFin=%d aqSz=%d",
-    mCurrentSeekTarget.IsValid(), mDropAudioUntilNextDiscontinuity, AudioQueue().IsFinished(), AudioQueue().GetSize());
+    mCurrentSeek.Exists(), mDropAudioUntilNextDiscontinuity, AudioQueue().IsFinished(), AudioQueue().GetSize());
   return
     !HasAudio() ||
-    (mCurrentSeekTarget.IsValid() &&
+    (mCurrentSeek.Exists() &&
      !mDropAudioUntilNextDiscontinuity &&
      (AudioQueue().IsFinished() || AudioQueue().GetSize() > 0));
 }
 
 bool
 MediaDecoderStateMachine::IsVideoSeekComplete()
 {
   AssertCurrentThreadInMonitor();
   SAMPLE_LOG("IsVideoSeekComplete() curTarVal=%d mVidDis=%d vqFin=%d vqSz=%d",
-    mCurrentSeekTarget.IsValid(), mDropVideoUntilNextDiscontinuity, VideoQueue().IsFinished(), VideoQueue().GetSize());
+    mCurrentSeek.Exists(), mDropVideoUntilNextDiscontinuity, VideoQueue().IsFinished(), VideoQueue().GetSize());
   return
     !HasVideo() ||
-    (mCurrentSeekTarget.IsValid() &&
+    (mCurrentSeek.Exists() &&
      !mDropVideoUntilNextDiscontinuity &&
      (VideoQueue().IsFinished() || VideoQueue().GetSize() > 0));
 }
 
 void
 MediaDecoderStateMachine::OnAudioDecoded(AudioData* aAudioSample)
 {
   MOZ_ASSERT(OnStateMachineThread());
@@ -739,38 +739,38 @@ MediaDecoderStateMachine::OnAudioDecoded
       // Schedule the state machine to send stream data as soon as possible.
       if (mAudioCaptured) {
         ScheduleStateMachine();
       }
       return;
     }
 
     case DECODER_STATE_SEEKING: {
-      if (!mCurrentSeekTarget.IsValid()) {
+      if (!mCurrentSeek.Exists()) {
         // We've received a sample from a previous decode. Discard it.
         return;
       }
       if (audio->mDiscontinuity) {
         mDropAudioUntilNextDiscontinuity = false;
       }
       if (!mDropAudioUntilNextDiscontinuity) {
         // We must be after the discontinuity; we're receiving samples
         // at or after the seek target.
-        if (mCurrentSeekTarget.mType == SeekTarget::PrevSyncPoint &&
-            mCurrentSeekTarget.mTime > mCurrentTimeBeforeSeek &&
+        if (mCurrentSeek.mTarget.mType == SeekTarget::PrevSyncPoint &&
+            mCurrentSeek.mTarget.mTime > mCurrentTimeBeforeSeek &&
             audio->mTime < mCurrentTimeBeforeSeek) {
           // 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.
-          mCurrentSeekTarget.mType = SeekTarget::Accurate;
+          mCurrentSeek.mTarget.mType = SeekTarget::Accurate;
         }
-        if (mCurrentSeekTarget.mType == SeekTarget::PrevSyncPoint) {
+        if (mCurrentSeek.mTarget.mType == SeekTarget::PrevSyncPoint) {
           // Non-precise seek; we can stop the seek at the first sample.
           AudioQueue().Push(audio);
         } else {
           // We're doing an accurate seek. We must discard
           // MediaData up to the one containing exact seek target.
           if (NS_FAILED(DropAudioUpToSeekTarget(audio))) {
             DecodeError();
             return;
@@ -860,17 +860,17 @@ MediaDecoderStateMachine::OnNotDecoded(M
     DispatchDecodeTasksIfNeeded();
     return;
   }
 
   // This is an EOS. Finish off the queue, and then handle things based on our
   // state.
   MOZ_ASSERT(aReason == MediaDecoderReader::END_OF_STREAM);
   if (!isAudio && mState == DECODER_STATE_SEEKING &&
-      mCurrentSeekTarget.IsValid() && mFirstVideoFrameAfterSeek) {
+      mCurrentSeek.Exists() && mFirstVideoFrameAfterSeek) {
     // Null sample. Hit end of stream. If we have decoded a frame,
     // insert it into the queue so that we have something to display.
     // We make sure to do this before invoking VideoQueue().Finish()
     // below.
     VideoQueue().Push(mFirstVideoFrameAfterSeek);
     mFirstVideoFrameAfterSeek = nullptr;
   }
   if (isAudio) {
@@ -895,17 +895,17 @@ MediaDecoderStateMachine::OnNotDecoded(M
       mDecoder->GetReentrantMonitor().NotifyAll();
       // Schedule the state machine to notify track ended as soon as possible.
       if (mAudioCaptured) {
         ScheduleStateMachine();
       }
       return;
     }
     case DECODER_STATE_SEEKING: {
-      if (!mCurrentSeekTarget.IsValid()) {
+      if (!mCurrentSeek.Exists()) {
         // We've received a sample from a previous decode. Discard it.
         return;
       }
 
       if (isAudio) {
         mDropAudioUntilNextDiscontinuity = false;
       } else {
         mDropVideoUntilNextDiscontinuity = false;
@@ -998,40 +998,40 @@ MediaDecoderStateMachine::OnVideoDecoded
 
       // Schedule the state machine to send stream data as soon as possible.
       if (mAudioCaptured) {
         ScheduleStateMachine();
       }
       return;
     }
     case DECODER_STATE_SEEKING: {
-      if (!mCurrentSeekTarget.IsValid()) {
+      if (!mCurrentSeek.Exists()) {
         // We've received a sample from a previous decode. Discard it.
         return;
       }
       if (mDropVideoUntilNextDiscontinuity) {
         if (video->mDiscontinuity) {
           mDropVideoUntilNextDiscontinuity = false;
         }
       }
       if (!mDropVideoUntilNextDiscontinuity) {
         // We must be after the discontinuity; we're receiving samples
         // at or after the seek target.
-        if (mCurrentSeekTarget.mType == SeekTarget::PrevSyncPoint &&
-            mCurrentSeekTarget.mTime > mCurrentTimeBeforeSeek &&
+        if (mCurrentSeek.mTarget.mType == SeekTarget::PrevSyncPoint &&
+            mCurrentSeek.mTarget.mTime > mCurrentTimeBeforeSeek &&
             video->mTime < mCurrentTimeBeforeSeek) {
           // 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.
-          mCurrentSeekTarget.mType = SeekTarget::Accurate;
+          mCurrentSeek.mTarget.mType = SeekTarget::Accurate;
         }
-        if (mCurrentSeekTarget.mType == SeekTarget::PrevSyncPoint) {
+        if (mCurrentSeek.mTarget.mType == SeekTarget::PrevSyncPoint) {
           // Non-precise seek; we can stop the seek at the first sample.
           VideoQueue().Push(video);
         } else {
           // We're doing an accurate seek. We still need to discard
           // MediaData up to the one containing exact seek target.
           if (NS_FAILED(DropVideoUpToSeekTarget(video))) {
             DecodeError();
             return;
@@ -1458,34 +1458,40 @@ void MediaDecoderStateMachine::SetDorman
   if (!mReader) {
     return;
   }
 
   DECODER_LOG("SetDormant=%d", aDormant);
 
   if (aDormant) {
     if (mState == DECODER_STATE_SEEKING) {
-      if (mQueuedSeekTarget.IsValid()) {
+      if (mQueuedSeek.Exists()) {
         // Keep latest seek target
-      } else if (mSeekTarget.IsValid()) {
-        mQueuedSeekTarget = mSeekTarget;
-      } else if (mCurrentSeekTarget.IsValid()) {
-        mQueuedSeekTarget = mCurrentSeekTarget;
+      } else if (mPendingSeek.Exists()) {
+        mQueuedSeek.Steal(mPendingSeek);
+      } else if (mCurrentSeek.Exists()) {
+        mQueuedSeek.Steal(mCurrentSeek);
       } else {
-        mQueuedSeekTarget = SeekTarget(mCurrentFrameTime,
+        mQueuedSeek.mTarget = SeekTarget(mCurrentFrameTime,
+                                         SeekTarget::Accurate,
+                                         MediaDecoderEventVisibility::Suppressed);
+        // XXXbholley - Nobody is listening to this promise. Do we need to pass it
+        // back to MediaDecoder when we come out of dormant?
+        nsRefPtr<MediaDecoder::SeekPromise> unused = mQueuedSeek.mPromise.Ensure(__func__);
+      }
+    } else {
+      mQueuedSeek.mTarget = SeekTarget(mCurrentFrameTime,
                                        SeekTarget::Accurate,
                                        MediaDecoderEventVisibility::Suppressed);
-      }
-    } else {
-      mQueuedSeekTarget = SeekTarget(mCurrentFrameTime,
-                                     SeekTarget::Accurate,
-                                     MediaDecoderEventVisibility::Suppressed);
+      // XXXbholley - Nobody is listening to this promise. Do we need to pass it
+      // back to MediaDecoder when we come out of dormant?
+      nsRefPtr<MediaDecoder::SeekPromise> unused = mQueuedSeek.mPromise.Ensure(__func__);
     }
-    mSeekTarget.Reset();
-    mCurrentSeekTarget.Reset();
+    mPendingSeek.RejectIfExists(__func__);
+    mCurrentSeek.RejectIfExists(__func__);
     ScheduleStateMachine();
     SetState(DECODER_STATE_DORMANT);
     mDecoder->GetReentrantMonitor().NotifyAll();
   } else if ((aDormant != true) && (mState == DECODER_STATE_DORMANT)) {
     mDecodingFrozenAtStateDecoding = true;
     ScheduleStateMachine();
     mCurrentFrameTime = 0;
     SetState(DECODER_STATE_DECODING_NONE);
@@ -1669,59 +1675,53 @@ void MediaDecoderStateMachine::NotifyDat
       double end = 0;
       buffered->End(length - 1, &end);
       ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
       mEndTime = std::max<int64_t>(mEndTime, end * USECS_PER_S);
     }
   }
 }
 
-void MediaDecoderStateMachine::Seek(const SeekTarget& aTarget)
+nsRefPtr<MediaDecoder::SeekPromise>
+MediaDecoderStateMachine::Seek(SeekTarget aTarget)
 {
-  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+  MOZ_ASSERT(OnStateMachineThread());
   ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
 
   mDecodingFrozenAtStateDecoding = false;
 
   if (mState == DECODER_STATE_SHUTDOWN) {
-    return;
+    return MediaDecoder::SeekPromise::CreateAndReject(/* aIgnored = */ true, __func__);
   }
 
   // We need to be able to seek both at a transport level and at a media level
   // to seek.
   if (!mDecoder->IsMediaSeekable()) {
     DECODER_WARN("Seek() function should not be called on a non-seekable state machine");
-    return;
+    return MediaDecoder::SeekPromise::CreateAndReject(/* aIgnored = */ true, __func__);
   }
 
   NS_ASSERTION(mState > DECODER_STATE_DECODING_METADATA,
                "We should have got duration already");
 
   if (mState < DECODER_STATE_DECODING) {
     DECODER_LOG("Seek() Not Enough Data to continue at this stage, queuing seek");
-    mQueuedSeekTarget = aTarget;
-    return;
+    mQueuedSeek.RejectIfExists(__func__);
+    mQueuedSeek.mTarget = aTarget;
+    return mQueuedSeek.mPromise.Ensure(__func__);
   }
-  mQueuedSeekTarget.Reset();
-  mSeekTarget = aTarget;
-
-  DECODER_LOG("Changed state to SEEKING (to %lld)", mSeekTarget.mTime);
+  mQueuedSeek.RejectIfExists(__func__);
+  mPendingSeek.RejectIfExists(__func__);
+  mPendingSeek.mTarget = aTarget;
+
+  DECODER_LOG("Changed state to SEEKING (to %lld)", mPendingSeek.mTarget.mTime);
   SetState(DECODER_STATE_SEEKING);
   ScheduleStateMachine();
-}
-
-void
-MediaDecoderStateMachine::EnqueueStartQueuedSeekTask()
-{
-  MOZ_ASSERT(mQueuedSeekTarget.IsValid());
-  MOZ_ASSERT(mState >= DECODER_STATE_DECODING, "MDSM::Seek will requeue this seek!");
-  DECODER_LOG("Applying queued seek to %lld\n", mQueuedSeekTarget.mTime);
-  nsCOMPtr<nsIRunnable> event =
-    NS_NewRunnableMethodWithArg<SeekTarget>(this, &MediaDecoderStateMachine::Seek, mQueuedSeekTarget);
-  NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
+
+  return mPendingSeek.mPromise.Ensure(__func__);
 }
 
 void MediaDecoderStateMachine::StopAudioThread()
 {
   MOZ_ASSERT(OnStateMachineThread());
   AssertCurrentThreadInMonitor();
 
   if (mStopAudioThread) {
@@ -1851,29 +1851,29 @@ MediaDecoderStateMachine::DispatchDecode
 }
 
 void
 MediaDecoderStateMachine::InitiateSeek()
 {
   MOZ_ASSERT(OnStateMachineThread());
   AssertCurrentThreadInMonitor();
 
-  mCurrentSeekTarget = mSeekTarget;
-  mSeekTarget.Reset();
+  mCurrentSeek.RejectIfExists(__func__);
+  mCurrentSeek.Steal(mPendingSeek);
 
   // Bound the seek time to be inside the media range.
   int64_t end = GetEndTime();
   NS_ASSERTION(mStartTime != -1, "Should know start time by now");
   NS_ASSERTION(end != -1, "Should know end time by now");
-  int64_t seekTime = mCurrentSeekTarget.mTime + mStartTime;
+  int64_t seekTime = mCurrentSeek.mTarget.mTime + mStartTime;
   seekTime = std::min(seekTime, end);
   seekTime = std::max(mStartTime, seekTime);
   NS_ASSERTION(seekTime >= mStartTime && seekTime <= end,
                "Can only seek in range [0,duration]");
-  mCurrentSeekTarget.mTime = seekTime;
+  mCurrentSeek.mTarget.mTime = seekTime;
 
   if (mAudioCaptured) {
     // TODO: We should re-create the decoded stream after seek completed as we do
     // for audio thread since it is until then we know which position we seek to
     // as far as fast-seek is concerned. It also fix the problem where stream
     // clock seems to go backwards during seeking.
     nsCOMPtr<nsIRunnable> event =
       NS_NewRunnableMethodWithArg<int64_t>(mDecoder, &MediaDecoder::RecreateDecodedStream,
@@ -1886,40 +1886,40 @@ MediaDecoderStateMachine::InitiateSeek()
 
   mDecoder->StopProgressUpdates();
   mCurrentTimeBeforeSeek = GetMediaTime();
 
   // Stop playback now to ensure that while we're outside the monitor
   // dispatching SeekingStarted, playback doesn't advance and mess with
   // mCurrentFrameTime that we've setting to seekTime here.
   StopPlayback();
-  UpdatePlaybackPositionInternal(mCurrentSeekTarget.mTime);
+  UpdatePlaybackPositionInternal(mCurrentSeek.mTarget.mTime);
 
   // SeekingStarted will do a UpdateReadyStateForData which will
   // inform the element and its users that we have no frames
   // to display
   nsCOMPtr<nsIRunnable> startEvent =
       NS_NewRunnableMethodWithArg<MediaDecoderEventVisibility>(
         mDecoder,
         &MediaDecoder::SeekingStarted,
-        mCurrentSeekTarget.mEventVisibility);
+        mCurrentSeek.mTarget.mEventVisibility);
   NS_DispatchToMainThread(startEvent, NS_DISPATCH_NORMAL);
 
   // The seek target is different than the current playback position,
   // we'll need to seek the playback position, so shutdown our decode
   // thread and audio sink.
   StopAudioThread();
   ResetPlayback();
 
   // Put a reset in the pipe before seek.
   ResetDecode();
 
   // Do the seek.
   mSeekRequest.Begin(ProxyMediaCall(DecodeTaskQueue(), mReader.get(), __func__,
-                                    &MediaDecoderReader::Seek, mCurrentSeekTarget.mTime,
+                                    &MediaDecoderReader::Seek, mCurrentSeek.mTarget.mTime,
                                     GetEndTime())
     ->RefableThen(mScheduler.get(), __func__, this,
                   &MediaDecoderStateMachine::OnSeekCompleted,
                   &MediaDecoderStateMachine::OnSeekFailed));
 }
 
 nsresult
 MediaDecoderStateMachine::DispatchAudioDecodeTaskIfNeeded()
@@ -2414,18 +2414,20 @@ MediaDecoderStateMachine::FinishDecodeFi
   }
 
   // For very short media the first frame decode can decode the entire media.
   // So we need to check if this has occurred, else our decode pipeline won't
   // run (since it doesn't need to) and we won't detect end of stream.
   CheckIfDecodeComplete();
   MaybeStartPlayback();
 
-  if (mQueuedSeekTarget.IsValid()) {
-    EnqueueStartQueuedSeekTask();
+  if (mQueuedSeek.Exists()) {
+    mPendingSeek.Steal(mQueuedSeek);
+    SetState(DECODER_STATE_SEEKING);
+    ScheduleStateMachine();
   }
 
   return NS_OK;
 }
 
 void
 MediaDecoderStateMachine::OnSeekCompleted(int64_t aTime)
 {
@@ -2451,28 +2453,27 @@ MediaDecoderStateMachine::OnSeekFailed(n
 }
 
 void
 MediaDecoderStateMachine::SeekCompleted()
 {
   MOZ_ASSERT(OnStateMachineThread());
   ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
 
-  // We must reset the seek target when exiting this function, but not
-  // before, as if we dropped the monitor in any function called here,
-  // we may begin a new seek on the state machine thread, and be in
-  // an inconsistent state.
-  AutoSetOnScopeExit<SeekTarget> reset(mCurrentSeekTarget, SeekTarget());
-
   if (mState != DECODER_STATE_SEEKING) {
+    MOZ_DIAGNOSTIC_ASSERT(mState == DECODER_STATE_DORMANT ||
+                          mState == DECODER_STATE_SHUTDOWN);
+    // It would be nice to assert mCurrent.Exists() here, but it's possible that
+    // we've transitioned to DECODER_STATE_SHUTDOWN but not yet gone through
+    // RunStateMachine in that state, which is where this promise gets rejected.
     return;
   }
 
-  int64_t seekTime = mCurrentSeekTarget.mTime;
-  int64_t newCurrentTime = mCurrentSeekTarget.mTime;
+  int64_t seekTime = mCurrentSeek.mTarget.mTime;
+  int64_t newCurrentTime = seekTime;
 
   // Setup timestamp state.
   nsRefPtr<VideoData> video = VideoQueue().PeekFront();
   if (seekTime == mEndTime) {
     newCurrentTime = mAudioStartTime = seekTime;
   } else if (HasAudio()) {
     AudioData* audio = AudioQueue().PeekFront();
     // Though we adjust the newCurrentTime in audio-based, and supplemented
@@ -2491,66 +2492,49 @@ MediaDecoderStateMachine::SeekCompleted(
   mPlayDuration = newCurrentTime - mStartTime;
 
   mDecoder->StartProgressUpdates();
 
   // Change state to DECODING or COMPLETED now. SeekingStopped will
   // call MediaDecoderStateMachine::Seek to reset our state to SEEKING
   // if we need to seek again.
 
-  nsCOMPtr<nsIRunnable> stopEvent;
   bool isLiveStream = mDecoder->GetResource()->GetLength() == -1;
-  if (mSeekTarget.IsValid()) {
+  if (mPendingSeek.Exists()) {
     // A new seek target came in while we were processing the old one. No rest
     // for the seeking.
     DECODER_LOG("A new seek came along while we were finishing the old one - staying in SEEKING");
     SetState(DECODER_STATE_SEEKING);
   } else if (GetMediaTime() == mEndTime && !isLiveStream) {
     // Seeked to end of media, move to COMPLETED state. Note we don't do
     // this if we're playing a live stream, since the end of media will advance
     // once we download more data!
     DECODER_LOG("Changed state from SEEKING (to %lld) to COMPLETED", seekTime);
-    stopEvent = NS_NewRunnableMethodWithArg<MediaDecoderEventVisibility>(
-                  mDecoder,
-                  &MediaDecoder::SeekingStoppedAtEnd,
-                  mCurrentSeekTarget.mEventVisibility);
     // Explicitly set our state so we don't decode further, and so
     // we report playback ended to the media element.
     SetState(DECODER_STATE_COMPLETED);
     DispatchDecodeTasksIfNeeded();
   } else {
     DECODER_LOG("Changed state from SEEKING (to %lld) to DECODING", seekTime);
-    stopEvent = NS_NewRunnableMethodWithArg<MediaDecoderEventVisibility>(
-                  mDecoder,
-                  &MediaDecoder::SeekingStopped,
-                  mCurrentSeekTarget.mEventVisibility);
     StartDecoding();
   }
 
   // Ensure timestamps are up to date.
   UpdatePlaybackPositionInternal(newCurrentTime);
 
   // Try to decode another frame to detect if we're at the end...
   DECODER_LOG("Seek completed, mCurrentFrameTime=%lld", mCurrentFrameTime);
 
   // Reset quick buffering status. This ensures that if we began the
   // seek while quick-buffering, we won't bypass quick buffering mode
   // if we need to buffer after the seek.
   mQuickBuffering = false;
 
-  // Prevent changes in playback position before 'seeked' is fired for we
-  // expect currentTime equals seek target in 'seeked' callback.
-  mScheduler->FreezeScheduling();
-  {
-    ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor());
-    NS_DispatchToMainThread(stopEvent, NS_DISPATCH_SYNC);
-  }
-
+  mCurrentSeek.Resolve(mState == DECODER_STATE_COMPLETED, __func__);
   ScheduleStateMachine();
-  mScheduler->ThawScheduling();
 
   if (video) {
     ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor());
     RenderVideoFrame(video, TimeStamp::Now());
     nsCOMPtr<nsIRunnable> event =
       NS_NewRunnableMethod(mDecoder, &MediaDecoder::Invalidate);
     NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
   }
@@ -2646,16 +2630,20 @@ nsresult MediaDecoderStateMachine::RunSt
 {
   AssertCurrentThreadInMonitor();
 
   MediaResource* resource = mDecoder->GetResource();
   NS_ENSURE_TRUE(resource, NS_ERROR_NULL_POINTER);
 
   switch (mState) {
     case DECODER_STATE_SHUTDOWN: {
+      mQueuedSeek.RejectIfExists(__func__);
+      mPendingSeek.RejectIfExists(__func__);
+      mCurrentSeek.RejectIfExists(__func__);
+
       if (IsPlaying()) {
         StopPlayback();
       }
 
       StopAudioThread();
       FlushDecoding();
 
       // Put a task in the decode queue to shutdown the reader.
@@ -2765,17 +2753,17 @@ nsresult MediaDecoderStateMachine::RunSt
       mDecoder->GetReentrantMonitor().NotifyAll();
       UpdateReadyState();
       MaybeStartPlayback();
       NS_ASSERTION(IsStateMachineScheduled(), "Must have timer scheduled");
       return NS_OK;
     }
 
     case DECODER_STATE_SEEKING: {
-      if (mSeekTarget.IsValid()) {
+      if (mPendingSeek.Exists()) {
         InitiateSeek();
       }
       return NS_OK;
     }
 
     case DECODER_STATE_COMPLETED: {
       // Play the remaining media. We want to run AdvanceFrame() at least
       // once to ensure the current playback position is advanced to the
@@ -3115,17 +3103,18 @@ void MediaDecoderStateMachine::AdvanceFr
 
 nsresult
 MediaDecoderStateMachine::DropVideoUpToSeekTarget(VideoData* aSample)
 {
   nsRefPtr<VideoData> video(aSample);
   MOZ_ASSERT(video);
   DECODER_LOG("DropVideoUpToSeekTarget() frame [%lld, %lld] dup=%d",
               video->mTime, video->GetEndTime(), video->mDuplicate);
-  const int64_t target = mCurrentSeekTarget.mTime;
+  MOZ_ASSERT(mCurrentSeek.Exists());
+  const int64_t target = mCurrentSeek.mTarget.mTime;
 
   // Duplicate handling: if we're dropping frames up the seek target, we must
   // be wary of Theora duplicate frames. They don't have an image, so if the
   // target frame is in a run of duplicates, we won't have an image to draw
   // after the seek. So store the last frame encountered while dropping, and
   // copy its Image forward onto duplicate frames, so that every frame has
   // an Image.
   if (video->mDuplicate &&
@@ -3163,22 +3152,22 @@ MediaDecoderStateMachine::DropVideoUpToS
   return NS_OK;
 }
 
 nsresult
 MediaDecoderStateMachine::DropAudioUpToSeekTarget(AudioData* aSample)
 {
   nsRefPtr<AudioData> audio(aSample);
   MOZ_ASSERT(audio &&
-             mCurrentSeekTarget.IsValid() &&
-             mCurrentSeekTarget.mType == SeekTarget::Accurate);
+             mCurrentSeek.Exists() &&
+             mCurrentSeek.mTarget.mType == SeekTarget::Accurate);
 
   CheckedInt64 startFrame = UsecsToFrames(audio->mTime,
                                           mInfo.mAudio.mRate);
-  CheckedInt64 targetFrame = UsecsToFrames(mCurrentSeekTarget.mTime,
+  CheckedInt64 targetFrame = UsecsToFrames(mCurrentSeek.mTarget.mTime,
                                            mInfo.mAudio.mRate);
   if (!startFrame.isValid() || !targetFrame.isValid()) {
     return NS_ERROR_FAILURE;
   }
   if (startFrame.value() + audio->mFrames <= targetFrame.value()) {
     // 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;
@@ -3217,17 +3206,17 @@ MediaDecoderStateMachine::DropAudioUpToS
   memcpy(audioData.get(),
          audio->mAudioData.get() + (framesToPrune * channels),
          frames * channels * sizeof(AudioDataValue));
   CheckedInt64 duration = FramesToUsecs(frames, mInfo.mAudio.mRate);
   if (!duration.isValid()) {
     return NS_ERROR_FAILURE;
   }
   nsRefPtr<AudioData> data(new AudioData(audio->mOffset,
-                                         mCurrentSeekTarget.mTime,
+                                         mCurrentSeek.mTarget.mTime,
                                          duration.value(),
                                          frames,
                                          audioData.forget(),
                                          channels,
                                          audio->mRate));
   AudioQueue().PushFront(data);
 
   return NS_OK;
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -216,22 +216,18 @@ public:
 
 private:
   // The actual work for the above, which happens asynchronously on the state
   // machine thread.
   void PlayInternal();
 public:
 
   // Seeks to the decoder to aTarget asynchronously.
-  // Must be called from the main thread.
-  void Seek(const SeekTarget& aTarget);
-
-  // Dispatches a task to the main thread to seek to mQueuedSeekTarget.
-  // This is threadsafe and can be called on any thread.
-  void EnqueueStartQueuedSeekTask();
+  // Must be called on the state machine thread.
+  nsRefPtr<MediaDecoder::SeekPromise> Seek(SeekTarget aTarget);
 
   // Returns the current playback position in seconds.
   // Called from the main thread to get the current frame time. The decoder
   // monitor must be obtained before calling this.
   double GetCurrentTime() const;
   int64_t GetCurrentTimeUs() const;
 
   // Clear the flag indicating that a playback position change event
@@ -805,33 +801,62 @@ protected:
   // It will be set to -1 if the duration is infinite
   int64_t mEndTime;
 
   // Will be set when SetDuration has been called with a value != -1
   // mDurationSet false doesn't indicate that we do not have a valid duration
   // as mStartTime and mEndTime could have been set separately.
   bool mDurationSet;
 
-  // Position to seek to in microseconds when the seek state transition occurs.
-  // The decoder monitor lock must be obtained before reading or writing
-  // this value. Accessed on main and decode thread.
-  SeekTarget mSeekTarget;
+  struct SeekJob {
+    void Steal(SeekJob& aOther)
+    {
+      MOZ_DIAGNOSTIC_ASSERT(!Exists());
+      mTarget = aOther.mTarget;
+      aOther.mTarget.Reset();
+      mPromise = Move(aOther.mPromise);
+    }
+
+    bool Exists()
+    {
+      MOZ_ASSERT(mTarget.IsValid() == !mPromise.IsEmpty());
+      return mTarget.IsValid();
+    }
+
+    void Resolve(bool aAtEnd, const char* aCallSite)
+    {
+      mTarget.Reset();
+      MediaDecoder::SeekResolveValue val(aAtEnd, mTarget.mEventVisibility);
+      mPromise.Resolve(val, aCallSite);
+    }
 
-  // Position to seek to in microseconds when DecodeFirstFrame completes.
-  // The decoder monitor lock must be obtained before reading or writing
-  // this value. Accessed on main and decode thread.
-  SeekTarget mQueuedSeekTarget;
+    void RejectIfExists(const char* aCallSite)
+    {
+      mTarget.Reset();
+      mPromise.RejectIfExists(true, aCallSite);
+    }
+
+    ~SeekJob()
+    {
+      MOZ_DIAGNOSTIC_ASSERT(!mTarget.IsValid());
+      MOZ_DIAGNOSTIC_ASSERT(mPromise.IsEmpty());
+    }
 
-  // The position that we're currently seeking to. This differs from
-  // mSeekTarget, as mSeekTarget is the target we'll seek to next, whereas
-  // mCurrentSeekTarget is the position that the decode is in the process
-  // of seeking to.
-  // The decoder monitor lock must be obtained before reading or writing
-  // this value.
-  SeekTarget mCurrentSeekTarget;
+    SeekTarget mTarget;
+    MediaPromiseHolder<MediaDecoder::SeekPromise> mPromise;
+  };
+
+  // Queued seek - moves to mPendingSeek when DecodeFirstFrame completes.
+  SeekJob mQueuedSeek;
+
+  // Position to seek to in microseconds when the seek state transition occurs.
+  SeekJob mPendingSeek;
+
+  // The position that we're currently seeking to.
+  SeekJob mCurrentSeek;
 
   // Media Fragment end time in microseconds. Access controlled by decoder monitor.
   int64_t mFragmentEndTime;
 
   // The audio sink resource.  Used on state machine and audio threads.
   RefPtr<AudioSink> mAudioSink;
 
   // The reader, don't call its methods with the decoder monitor held.
@@ -1084,17 +1109,17 @@ protected:
   // a "discontinuity".
   bool mDropAudioUntilNextDiscontinuity;
   bool mDropVideoUntilNextDiscontinuity;
 
   // True if we need to decode forwards to the seek target inside
   // mCurrentSeekTarget.
   bool mDecodeToSeekTarget;
 
-  // Track the current seek request.
+  // Track the current seek promise made by the reader.
   MediaPromiseConsumerHolder<MediaDecoderReader::SeekPromise> mSeekRequest;
 
   // We record the playback position before we seek in order to
   // determine where the seek terminated relative to the playback position
   // we were at before the seek.
   int64_t mCurrentTimeBeforeSeek;
 
   // Stores presentation info required for playback. The decoder monitor
--- a/dom/media/MediaPromise.h
+++ b/dom/media/MediaPromise.h
@@ -435,16 +435,26 @@ public:
  */
 template<typename PromiseType>
 class MediaPromiseHolder
 {
 public:
   MediaPromiseHolder()
     : mMonitor(nullptr) {}
 
+  // Move semantics.
+  MediaPromiseHolder& operator=(MediaPromiseHolder&& aOther)
+  {
+    MOZ_ASSERT(!mMonitor && !aOther.mMonitor);
+    MOZ_DIAGNOSTIC_ASSERT(!mPromise);
+    mPromise = aOther.mPromise;
+    aOther.mPromise = nullptr;
+    return *this;
+  }
+
   ~MediaPromiseHolder() { MOZ_ASSERT(!mPromise); }
 
   already_AddRefed<PromiseType> Ensure(const char* aMethodName) {
     if (mMonitor) {
       mMonitor->AssertCurrentThreadOwns();
     }
     if (!mPromise) {
       mPromise = new (typename PromiseType::Private)(aMethodName);
--- a/dom/media/omx/AudioOffloadPlayer.cpp
+++ b/dom/media/omx/AudioOffloadPlayer.cpp
@@ -381,17 +381,17 @@ status_t AudioOffloadPlayer::SeekTo(int6
     }
 
     if (mDispatchSeekEvents) {
       mDispatchSeekEvents = false;
       AUDIO_OFFLOAD_LOG(PR_LOG_DEBUG, ("Fake seek complete during pause"));
       nsCOMPtr<nsIRunnable> nsEvent =
         NS_NewRunnableMethodWithArg<MediaDecoderEventVisibility>(
           mObserver,
-          &MediaDecoder::SeekingStopped,
+          &MediaDecoder::SimulateSeekResolvedForAudioOffload,
           MediaDecoderEventVisibility::Observable);
       NS_DispatchToCurrentThread(nsEvent);
     }
   }
 
   return OK;
 }
 
@@ -566,17 +566,17 @@ size_t AudioOffloadPlayer::FillBuffer(vo
 
       if (refreshSeekTime) {
         if (mDispatchSeekEvents && !mSeekDuringPause) {
           mDispatchSeekEvents = false;
           AUDIO_OFFLOAD_LOG(PR_LOG_DEBUG, ("FillBuffer posting SEEK_COMPLETE"));
           nsCOMPtr<nsIRunnable> nsEvent =
             NS_NewRunnableMethodWithArg<MediaDecoderEventVisibility>(
               mObserver,
-              &MediaDecoder::SeekingStopped,
+              &MediaDecoder::SimulateSeekResolvedForAudioOffload,
               MediaDecoderEventVisibility::Observable);
           NS_DispatchToMainThread(nsEvent, NS_DISPATCH_NORMAL);
 
         } else if (mSeekDuringPause) {
           // Callback is already called for seek during pause. Just reset the
           // flag
           AUDIO_OFFLOAD_LOG(PR_LOG_DEBUG, ("Not posting seek complete as its"
               " already faked"));