Bug 1295921 - PD: Keep last keyframe in MFR. r?jwwang, jya draft
authorDan Glastonbury <dglastonbury@mozilla.com>
Mon, 19 Dec 2016 11:47:27 +1000
changeset 450833 913cd310b0d1c1f1303e67b73c5d189bc7da8b06
parent 450832 c727af5916c4847841afd4b11c5ea646b7a7538a
child 539837 3aa175a3fad496aa7b86cab5079ee6963c1c9ecc
push id38957
push userbmo:dglastonbury@mozilla.com
push dateMon, 19 Dec 2016 02:15:56 +0000
reviewersjwwang, jya
bugs1295921
milestone53.0a1
Bug 1295921 - PD: Keep last keyframe in MFR. r?jwwang, jya To implement resuming video playback when drawImage() is invoked with a suspended video element, the MFR is extended to keep the last demuxed keyframe. This is done to avoid invoking track demuxing which can cause a dead lock via the media cache which ends up blocking on main thread, which is blocked. A new seek type, PrevFrame, is used to invoke the MFR to decode the stored keyframe and flush the decoder to force out the image. Because the decoder has been flushed, the MDSM starts a second seek to the current time stamp to recover the video stream. MozReview-Commit-ID: HiPxfVnG5j5
dom/media/MediaDecoderStateMachine.cpp
dom/media/MediaDecoderStateMachine.h
dom/media/MediaFormatReader.cpp
dom/media/MediaFormatReader.h
dom/media/SeekTarget.h
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -82,16 +82,20 @@ using namespace mozilla::media;
 
 // Used by StateObject and its sub-classes
 #define SFMT(x, ...) "Decoder=%p state=%s " x, mMaster->mDecoderID, ToStateStr(GetState()), ##__VA_ARGS__
 #define SLOG(x, ...) MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (SFMT(x, ##__VA_ARGS__)))
 #define SWARN(x, ...) NS_WARNING(nsPrintfCString(SFMT(x, ##__VA_ARGS__)).get())
 #define SDUMP(x, ...) NS_DebugBreak(NS_DEBUG_WARNING, nsPrintfCString(SFMT(x, ##__VA_ARGS__)).get(), nullptr, nullptr, -1)
 #define SSAMPLELOG(x, ...) MOZ_LOG(gMediaSampleLog, LogLevel::Debug, (SFMT(x, ##__VA_ARGS__)))
 
+LazyLogModule gDecoderSuspendLog("DecodeSuspend");
+#define SUSPEND_LOG(x, ...) MOZ_LOG(gDecoderSuspendLog, LogLevel::Debug, (FMT(x, ##__VA_ARGS__)))
+#define SSUSPEND_LOG(x, ...) MOZ_LOG(gDecoderSuspendLog, LogLevel::Debug, (SFMT(x, ##__VA_ARGS__)))
+
 // Certain constants get stored as member variables and then adjusted by various
 // scale factors on a per-decoder basis. We want to make sure to avoid using these
 // constants directly, so we put them in a namespace.
 namespace detail {
 
 // If audio queue has less than this many usecs of decoded audio, we won't risk
 // trying to decode the video, we'll skip decoding video up to the next
 // keyframe. We may increase this value for an individual decoder if we
@@ -613,16 +617,17 @@ public:
       return;
     }
 
     // The decoder is tainted, so nothing to suspend.
     if (mMaster->mHasSuspendTaint) {
       return;
     }
 
+    SSUSPEND_LOG("Setting Video Decode Suspended");
     mMaster->mVideoDecodeSuspended = true;
     mMaster->mOnPlaybackEvent.Notify(MediaEventType::EnterVideoSuspend);
     Reader()->SetVideoBlankDecode(true);
   }
 
   void HandlePlayStateChanged(MediaDecoder::PlayState aPlayState) override
   {
     if (aPlayState == MediaDecoder::PLAY_STATE_PLAYING) {
@@ -771,16 +776,17 @@ public:
                                           EventVisibility aVisibility)
   {
     mSeekJob = Move(aSeekJob);
 
     // Always switch off the blank decoder otherwise we might become visible
     // in the middle of seeking and won't have a valid video frame to show
     // when seek is done.
     if (mMaster->mVideoDecodeSuspended) {
+      SSUSPEND_LOG("Cancel Video Decode Suspend");
       mMaster->mVideoDecodeSuspended = false;
       mMaster->mOnPlaybackEvent.Notify(MediaEventType::ExitVideoSuspend);
       Reader()->SetVideoBlankDecode(false);
     }
 
     CreateSeekTask();
 
     // Don't stop playback for a video-only seek since audio is playing.
@@ -1429,16 +1435,258 @@ private:
     }
 
     mMaster->DecodeError(aValue.mError);
   }
 
 };
 
 /**
+ * Purpose: Resume video decoding when invoked from blocking
+ * operation, such as drawImage() call from JS.
+ */
+class MediaDecoderStateMachine::BlockingResumeVideoState
+  : public MediaDecoderStateMachine::SeekingState
+{
+public:
+  explicit BlockingResumeVideoState(Master* aPtr) : SeekingState(aPtr)
+  {
+  }
+
+  RefPtr<MediaDecoder::SeekPromise> Enter(SeekJob aSeekJob,
+                                          EventVisibility aVisibility)
+  {
+    MOZ_ASSERT(aSeekJob.mTarget.IsPrevFrame());
+    MOZ_ASSERT(aSeekJob.mTarget.IsVideoOnly());
+    return SeekingState::Enter(Move(aSeekJob), aVisibility);
+  }
+
+  void Exit() override
+  {
+    // Unblock main thread if it is waiting for seek to finish.
+    mMaster->MarkWaitForFrameDone();
+
+    mSeekJob.RejectIfExists(__func__);
+    mSeekRequest.DisconnectIfExists();
+  }
+
+  void HandleAudioDecoded(MediaData* aAudio) override
+  {
+    MOZ_ASSERT(!mDoneVideoSeeking, "Seek shouldn't be finished");
+
+    RefPtr<MediaData> audio(aAudio);
+    MOZ_ASSERT(audio);
+
+    // The MDSM::mDecodedAudioEndTime will be updated once the whole SeekTask is
+    // resolved.
+
+    SSAMPLELOG("HandleAudioDecoded [%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().
+    mSeekedAudioData = audio.forget();
+  }
+
+  void HandleVideoDecoded(MediaData* aVideo, TimeStamp aDecodeStart) override
+  {
+    MOZ_ASSERT(!mDoneVideoSeeking, "Seek shouldn't be finished");
+
+    RefPtr<MediaData> video(aVideo);
+    MOZ_ASSERT(video);
+
+    // The MDSM::mDecodedVideoEndTime will be updated once the whole SeekTask is
+    // resolved.
+
+    SSAMPLELOG("HandleVideoDecoded [%lld,%lld]", video->mTime, video->GetEndTime());
+
+    // Non-precise seek. We can stop the seek at the first sample.
+    mSeekedVideoData = video;
+    mDoneVideoSeeking = true;
+
+    MaybeFinishSeek();
+  }
+
+  void HandleNotDecoded(MediaData::Type aType, const MediaResult& aError) override
+  {
+    MOZ_ASSERT(!mDoneVideoSeeking, "Seek shouldn't be finished");
+
+    SSAMPLELOG("OnNotDecoded type=%d reason=%u", aType, aError.Code());
+
+    // Ignore pending requests from video-only seek.
+    if (aType == MediaData::AUDIO_DATA) {
+      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) {
+      Reader()->WaitForData(aType);
+      return;
+    }
+
+    if (aError == NS_ERROR_DOM_MEDIA_CANCELED &&
+        aType == MediaData::VIDEO_DATA) {
+      RequestVideoData();
+      return;
+    }
+
+    if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
+      if (aType == MediaData::AUDIO_DATA) {
+        mIsAudioQueueFinished = true;
+      } else {
+        mIsVideoQueueFinished = true;
+        mDoneVideoSeeking = true;
+      }
+      MaybeFinishSeek();
+      return;
+    }
+
+    // This is a decode error, delegate to the generic error path.
+    OnSeekTaskRejected(aError);
+  }
+
+  void HandleAudioWaited(MediaData::Type aType) override
+  {
+    MOZ_ASSERT(!mDoneVideoSeeking, "Seek shouldn't be finished");
+
+    // Ignore pending requests from video-only seek.
+  }
+
+  void HandleVideoWaited(MediaData::Type aType) override
+  {
+    MOZ_ASSERT(!mDoneVideoSeeking, "Seek shouldn't be finished");
+
+    RequestVideoData();
+  }
+
+  void HandleNotWaited(const WaitForDataRejectValue& aRejection) override
+  {
+    MOZ_ASSERT(!mDoneVideoSeeking, "Seek shouldn't be finished");
+  }
+
+private:
+  void CreateSeekTask() override
+  {
+    mDoneVideoSeeking = false;
+  }
+
+  void ResetMDSM() override
+  {
+    mMaster->Reset(TrackInfo::kVideoTrack);
+  }
+
+  void DoSeek() override
+  {
+    mSeekRequest.Begin(Reader()->Seek(mSeekJob.mTarget)
+      ->Then(OwnerThread(), __func__,
+             [this] (media::TimeUnit aUnit) {
+               OnSeekResolved(aUnit);
+             },
+             [this] (nsresult aResult) {
+               OnSeekRejected(aResult);
+             }));
+  }
+
+  int64_t CalculateNewCurrentTime() const override
+  {
+    return mSeekJob.mTarget.GetTime().ToMicroseconds();
+  }
+
+  void OnSeekResolved(media::TimeUnit)
+  {
+    mSeekRequest.Complete();
+
+    if (!mDoneVideoSeeking) {
+      RequestVideoData();
+    }
+  }
+
+  void OnSeekRejected(nsresult aResult)
+  {
+    mSeekRequest.Complete();
+
+    MOZ_ASSERT(NS_FAILED(aResult), "Cancels should also disconnect mSeekRequest");
+    OnSeekTaskRejected(aResult);
+  }
+
+  void RequestVideoData()
+  {
+    MOZ_ASSERT(!mDoneVideoSeeking);
+    MOZ_ASSERT(!Reader()->IsRequestingVideoData());
+    MOZ_ASSERT(!Reader()->IsWaitingVideoData());
+    Reader()->RequestVideoData(false, media::TimeUnit());
+  }
+
+  void MaybeFinishSeek()
+  {
+    if (mDoneVideoSeeking) {
+      OnSeekTaskResolved();
+    }
+  }
+
+  void OnSeekTaskResolved()
+  {
+    if (mSeekedAudioData) {
+      mMaster->Push(mSeekedAudioData);
+      mMaster->mDecodedAudioEndTime = std::max(
+        mSeekedAudioData->GetEndTime(), mMaster->mDecodedAudioEndTime);
+    }
+
+    if (mSeekedVideoData) {
+      mMaster->Push(mSeekedVideoData);
+      mMaster->mDecodedVideoEndTime = std::max(
+        mSeekedVideoData->GetEndTime(), mMaster->mDecodedVideoEndTime);
+    }
+
+    if (mIsAudioQueueFinished) {
+      AudioQueue().Finish();
+    }
+
+    if (mIsVideoQueueFinished) {
+      VideoQueue().Finish();
+    }
+
+    SeekCompleted();
+  }
+
+  void OnSeekTaskRejected(MediaResult aError)
+  {
+    if (mIsAudioQueueFinished) {
+      AudioQueue().Finish();
+    }
+
+    if (mIsVideoQueueFinished) {
+      VideoQueue().Finish();
+    }
+
+    mMaster->DecodeError(aError);
+  }
+
+  /*
+   * Track the current seek promise made by the reader.
+   */
+  MozPromiseRequestHolder<MediaDecoderReader::SeekPromise> mSeekRequest;
+
+  /*
+   * Internal state.
+   */
+  media::TimeUnit mCurrentTimeBeforeSeek;
+  bool mDoneVideoSeeking = false;
+
+  /*
+   * Information which are going to be returned to MDSM.
+   */
+  RefPtr<MediaData> mSeekedAudioData;
+  RefPtr<MediaData> mSeekedVideoData;
+  bool mIsAudioQueueFinished = false;
+  bool mIsVideoQueueFinished = false;
+};
+
+/**
  * Purpose: stop playback until enough data is decoded to continue playback.
  *
  * Transition to:
  *   SEEKING if any seek request.
  *   SHUTDOWN if any decode error.
  *   COMPLETED when having decoded all audio/video data.
  *   DECODING when having decoded enough data to continue playback.
  */
@@ -1488,29 +1736,18 @@ public:
     mMaster->Push(aVideo);
     mMaster->ScheduleStateMachine();
   }
 
   void HandleEndOfStream() override;
 
   void HandleVideoSuspendTimeout() override
   {
-    // No video, so nothing to suspend.
-    if (!mMaster->HasVideo()) {
-      return;
-    }
-
-    // The decoder is tainted, so nothing to suspend.
-    if (mMaster->mHasSuspendTaint) {
-      return;
-    }
-
-    mMaster->mVideoDecodeSuspended = true;
-    mMaster->mOnPlaybackEvent.Notify(MediaEventType::EnterVideoSuspend);
-    Reader()->SetVideoBlankDecode(true);
+    // Don't suspend here. Let the suspend happen when transitioning into
+    // DecodingState
   }
 
 private:
   TimeStamp mBufferingStart;
 
   // The maximum number of second we spend buffering when we are short on
   // unbuffered data.
   const uint32_t mBufferingWait = 15;
@@ -1794,31 +2031,34 @@ ReportRecoveryTelemetry(const TimeStamp&
                         NS_LITERAL_CSTRING("All"),
                         uint32_t(duration_ms + 0.5));
 }
 
 void
 MediaDecoderStateMachine::
 StateObject::HandleResumeVideoDecoding()
 {
+  SSUSPEND_LOG("Resuming from Video Decode Suspend");
   MOZ_ASSERT(mMaster->mVideoDecodeSuspended);
 
   // Start counting recovery time from right now.
   TimeStamp start = TimeStamp::Now();
 
   // Local reference to mInfo, so that it will be copied in the lambda below.
   auto& info = Info();
   bool hw = Reader()->VideoIsHardwareAccelerated();
 
   // Start video-only seek to the current time.
   SeekJob seekJob;
 
-  const SeekTarget::Type type = mMaster->HasAudio()
-                                ? SeekTarget::Type::Accurate
-                                : SeekTarget::Type::PrevSyncPoint;
+  const SeekTarget::Type type = mMaster->mWaitForFrameState
+                                ? SeekTarget::Type::PrevFrame
+                                : (mMaster->HasAudio()
+                                   ? SeekTarget::Type::Accurate
+                                   : SeekTarget::Type::PrevSyncPoint);
 
   seekJob.mTarget = SeekTarget(mMaster->GetMediaTime(),
                                type,
                                true /* aVideoOnly */);
 
   SetSeekingState(Move(seekJob), EventVisibility::Suppressed)->Then(
     AbstractThread::MainThread(), __func__,
     [start, info, hw](){ ReportRecoveryTelemetry(start, info, hw); },
@@ -1832,16 +2072,20 @@ StateObject::SetSeekingState(SeekJob&& a
   if (aSeekJob.mTarget.IsAccurate() || aSeekJob.mTarget.IsFast()) {
     return SetState<AccurateSeekingState>(Move(aSeekJob), aVisibility);
   }
 
   if (aSeekJob.mTarget.IsNextFrame()) {
     return SetState<NextFrameSeekingState>(Move(aSeekJob), aVisibility);
   }
 
+  if (aSeekJob.mTarget.IsPrevFrame()) {
+    return SetState<BlockingResumeVideoState>(Move(aSeekJob), aVisibility);
+  }
+
   MOZ_ASSERT_UNREACHABLE("Unknown SeekTarget::Type.");
   return nullptr;
 }
 
 void
 MediaDecoderStateMachine::
 DecodeMetadataState::OnMetadataRead(MetadataHolder* aMetadata)
 {
@@ -1947,16 +2191,17 @@ void
 MediaDecoderStateMachine::
 DecodingState::Enter()
 {
   MOZ_ASSERT(mMaster->mSentFirstFrameLoadedEvent);
 
   if (!mMaster->mIsVisible &&
       !mMaster->mVideoDecodeSuspendTimer.IsScheduled() &&
       !mMaster->mVideoDecodeSuspended) {
+    SSUSPEND_LOG("SuspendTimer expired on entry to decoding state");
     // If we are not visible and the timer is not schedule, it means the timer
     // has timed out and we should suspend video decoding now if necessary.
     HandleVideoSuspendTimeout();
   }
 
   if (mMaster->CheckIfDecodeComplete()) {
     SetState<CompletedState>();
     return;
@@ -2043,41 +2288,58 @@ SeekingState::SeekCompleted()
     // remain false and 'playbackEnded' won't be notified. Therefore we
     // need to set these flags explicitly when seeking to the end.
     mMaster->mAudioCompleted = true;
     mMaster->mVideoCompleted = true;
   }
 
   // We want to resolve the seek request prior finishing the first frame
   // to ensure that the seeked event is fired prior loadeded.
+
+  // mSeekJob.Resolve() resets mSeekJob.mTarget, so save it to be used later.
+  SeekTarget target = mSeekJob.mTarget;
   mSeekJob.Resolve(__func__);
 
   // Notify FirstFrameLoaded now if we haven't since we've decoded some data
   // for readyState to transition to HAVE_CURRENT_DATA and fire 'loadeddata'.
   if (!mMaster->mSentFirstFrameLoadedEvent) {
     mMaster->FinishDecodeFirstFrame();
   }
 
   // Ensure timestamps are up to date.
-  if (!mSeekJob.mTarget.IsVideoOnly()) {
+  if (!target.IsVideoOnly()) {
     // Don't update playback position for video-only seek.
     // Otherwise we might have |newCurrentTime > mMediaSink->GetPosition()|
     // and fail the assertion in GetClock() since we didn't stop MediaSink.
     mMaster->UpdatePlaybackPositionInternal(newCurrentTime);
   }
 
   // Try to decode another frame to detect if we're at the end...
   SLOG("Seek completed, mCurrentPosition=%lld", mMaster->mCurrentPosition.Ref());
 
   if (mMaster->VideoQueue().PeekFront()) {
     mMaster->mMediaSink->Redraw(Info().mVideo);
     mMaster->mOnPlaybackEvent.Notify(MediaEventType::Invalidate);
   }
 
-  SetState<DecodingState>();
+  if (target.IsPrevFrame()) {
+    // Unblock main thread if it is waiting for seek to finish.
+    mMaster->MarkWaitForFrameDone();
+
+    // If completing a PrevFrame seek, issue a seek again to start the MFR which
+    // has been drained
+    target.SetType(mMaster->HasAudio() ? SeekTarget::Type::Accurate
+                                       : SeekTarget::Type::PrevSyncPoint);
+
+    SeekJob seekJob;
+    seekJob.mTarget = target;
+    SetSeekingState(Move(seekJob), EventVisibility::Suppressed);
+  } else {
+    SetState<DecodingState>();
+  }
 }
 
 void
 MediaDecoderStateMachine::
 BufferingState::Step()
 {
   TimeStamp now = TimeStamp::Now();
   MOZ_ASSERT(!mBufferingStart.IsNull(), "Must know buffering start time.");
@@ -2878,21 +3140,23 @@ void MediaDecoderStateMachine::PlayState
   }
 
   mStateObj->HandlePlayStateChanged(mPlayState);
 }
 
 void MediaDecoderStateMachine::VisibilityChanged()
 {
   MOZ_ASSERT(OnTaskQueue());
-  DECODER_LOG("VisibilityChanged: mIsVisible=%d, mVideoDecodeSuspended=%c",
+  SUSPEND_LOG("VisibilityChanged: mIsVisible=%d, mVideoDecodeSuspended=%c",
               mIsVisible.Ref(), mVideoDecodeSuspended ? 'T' : 'F');
 
   // Start timer to trigger suspended decoding state when going invisible.
   if (!mIsVisible) {
+    SUSPEND_LOG("Scheduling timer for %" PRIu64 "ms",
+                DurationToUsecs(SuspendBackgroundVideoDelay()) / 1000);
     TimeStamp target = TimeStamp::Now() + SuspendBackgroundVideoDelay();
 
     RefPtr<MediaDecoderStateMachine> self = this;
     mVideoDecodeSuspendTimer.Ensure(target,
                                     [=]() { self->OnSuspendTimerResolved(); },
                                     [] () { MOZ_DIAGNOSTIC_ASSERT(false); });
     mOnPlaybackEvent.Notify(MediaEventType::BeginVideoSuspend);
 
@@ -2936,27 +3200,27 @@ MediaDecoderStateMachine::DispatchSetWai
   OwnerThread()->DispatchStateChange(
     NewRunnableMethod<WaitForFrame*>(this,
       &MediaDecoderStateMachine::SetWaitForFrameState, aState));
 }
 
 void
 MediaDecoderStateMachine::SetWaitForFrameState(WaitForFrame* aState)
 {
-  DECODER_LOG("SetWaitForFrameMonitor: state = %p", aState);
+  SUSPEND_LOG("SetWaitForFrameMonitor: state = %p", aState);
 
   MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(!mWaitForFrameState);
   mWaitForFrameState = aState;
 }
 
 void
 MediaDecoderStateMachine::MarkWaitForFrameDone()
 {
-  DECODER_LOG("MarkWaitForFrameDone: state = %p", mWaitForFrameState);
+  SUSPEND_LOG("MarkWaitForFrameDone: state = %p", mWaitForFrameState);
   MOZ_ASSERT(OnTaskQueue());
 
   WaitForFrame* state = mWaitForFrameState;
   if (state) {
     MonitorAutoLock lock(state->mMon);
     mWaitForFrameState = nullptr;
     state->mDone = true;
     lock.NotifyAll();
@@ -3825,25 +4089,25 @@ MediaDecoderStateMachine::VideoRequestSt
     return "waiting";
   }
   return "idle";
 }
 
 void
 MediaDecoderStateMachine::OnSuspendTimerResolved()
 {
-  DECODER_LOG("OnSuspendTimerResolved");
+  SUSPEND_LOG("OnSuspendTimerResolved");
   mVideoDecodeSuspendTimer.CompleteRequest();
   mStateObj->HandleVideoSuspendTimeout();
 }
 
 void
 MediaDecoderStateMachine::CancelSuspendTimer()
 {
-  DECODER_LOG("CancelSuspendTimer: State: %s, Timer.IsScheduled: %c",
+  SUSPEND_LOG("CancelSuspendTimer: State: %s, Timer.IsScheduled: %c",
               ToStateStr(mStateObj->GetState()),
               mVideoDecodeSuspendTimer.IsScheduled() ? 'T' : 'F');
   MOZ_ASSERT(OnTaskQueue());
   if (mVideoDecodeSuspendTimer.IsScheduled()) {
     mOnPlaybackEvent.Notify(MediaEventType::CancelVideoSuspend);
   }
   mVideoDecodeSuspendTimer.Reset();
 }
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -290,16 +290,17 @@ private:
   class DecodingFirstFrameState;
   class DecodingState;
   class SeekingState;
   class AccurateSeekingState;
   class NextFrameSeekingState;
   class BufferingState;
   class CompletedState;
   class ShutdownState;
+  class BlockingResumeVideoState;
 
   static const char* ToStateStr(State aState);
   static const char* ToStr(NextFrameStatus aStatus);
   const char* ToStateStr();
 
   // Functions used by assertions to ensure we're calling things
   // on the appropriate threads.
   bool OnTaskQueue() const;
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -1261,16 +1261,25 @@ MediaFormatReader::OnDemuxFailed(TrackTy
       NotifyError(aTrack, aError);
       break;
   }
 }
 
 void
 MediaFormatReader::DoDemuxVideo()
 {
+  LOGV("");
+  if (mVideo.mForceLastKeyframe) {
+    LOGV("Forcing last demuxed keyframe");
+    MOZ_ASSERT(mVideo.mQueuedSamples.IsEmpty());
+    mVideo.mQueuedSamples.AppendElement(mVideo.mLastDemuxedKeyframe);
+    ScheduleUpdate(TrackInfo::kVideoTrack);
+    return;
+  }
+
   auto p = mVideo.mTrackDemuxer->GetSamples(1);
 
   if (mVideo.mFirstDemuxedSampleTime.isNothing()) {
     RefPtr<MediaFormatReader> self = this;
     p = p->Then(OwnerThread(), __func__,
                 [self] (RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
                   self->OnFirstDemuxCompleted(TrackInfo::kVideoTrack, aSamples);
                 },
@@ -1380,16 +1389,23 @@ MediaFormatReader::NotifyNewOutput(Track
 
 void
 MediaFormatReader::NotifyInputExhausted(TrackType aTrack)
 {
   MOZ_ASSERT(OnTaskQueue());
   LOGV("Decoder has requested more %s data", TrackTypeToStr(aTrack));
   auto& decoder = GetDecoderData(aTrack);
   decoder.mDecodePending = false;
+  // If forcing the last keyframe and the frame is pending decode, drain
+  // the decoder.
+  if (decoder.mForceLastKeyframe) {
+    LOGV("ForceLastKeyframe set. Clearing and forcing need draining.");
+    decoder.mForceLastKeyframe = false;
+    decoder.mNeedDraining = true;
+  }
   ScheduleUpdate(aTrack);
 }
 
 void
 MediaFormatReader::NotifyDrainComplete(TrackType aTrack)
 {
   MOZ_ASSERT(OnTaskQueue());
   auto& decoder = GetDecoderData(aTrack);
@@ -1683,16 +1699,21 @@ MediaFormatReader::HandleDemuxedSamples(
          sample->mTime, sample->mTimecode, sample->mKeyframe);
     decoder.mOutputRequested = true;
     decoder.mNumSamplesInput++;
     decoder.mSizeOfQueue++;
     if (aTrack == TrackInfo::kVideoTrack) {
       aA.mStats.mParsedFrames++;
     }
 
+    // If the sample is a keyframe, keep a reference to it.
+    if (aTrack == TrackInfo::kVideoTrack && sample->mKeyframe) {
+      decoder.mLastDemuxedKeyframe = sample;
+    }
+
     DecodeDemuxedSamples(aTrack, sample);
 
     decoder.mQueuedSamples.RemoveElementAt(0);
     samplesPending = true;
   }
 }
 
 void
@@ -2402,16 +2423,37 @@ MediaFormatReader::OnSeekFailed(TrackTyp
   mSeekPromise.Reject(aError, __func__);
 }
 
 void
 MediaFormatReader::DoVideoSeek()
 {
   MOZ_ASSERT(mPendingSeekTime.isSome());
   LOGV("Seeking video to %lld", mPendingSeekTime.ref().ToMicroseconds());
+  LOGV("IsPrevFrame %c IsVideoOnly %c",
+       mOriginalSeekTarget.IsPrevFrame() ? 'T' : 'F',
+       mOriginalSeekTarget.IsVideoOnly() ? 'T' : 'F');
+  if (mOriginalSeekTarget.IsPrevFrame()) {
+    LOGV("Seek to prev keyframe");
+    //MOZ_ASSERT(mOriginalSeekTarget.IsVideoOnly());
+    if (!mOriginalSeekTarget.IsVideoOnly()) {
+      MediaResult error(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR,
+                        "Seek of type PrevFrame required to be video only");
+      mSeekPromise.Reject(error, __func__);
+    }
+
+    // Skip resetting the track demuxer and force returning the last
+    // demuxed keyframe.
+    mVideo.mForceLastKeyframe = true;
+    mSeekPromise.Resolve(*mPendingSeekTime, __func__);
+    mPendingSeekTime.reset();
+
+    return;
+  }
+
   media::TimeUnit seekTime = mPendingSeekTime.ref();
   mVideo.mSeekRequest.Begin(mVideo.mTrackDemuxer->Seek(seekTime)
                           ->Then(OwnerThread(), __func__, this,
                                  &MediaFormatReader::OnVideoSeekCompleted,
                                  &MediaFormatReader::OnVideoSeekFailed));
 }
 
 void
--- a/dom/media/MediaFormatReader.h
+++ b/dom/media/MediaFormatReader.h
@@ -225,16 +225,17 @@ private:
       , mNumSamplesInput(0)
       , mNumSamplesOutput(0)
       , mNumSamplesOutputTotal(0)
       , mNumSamplesSkippedTotal(0)
       , mSizeOfQueue(0)
       , mIsHardwareAccelerated(false)
       , mLastStreamSourceID(UINT32_MAX)
       , mIsBlankDecode(false)
+      , mForceLastKeyframe(false)
     {}
 
     MediaFormatReader* mOwner;
     // Disambiguate Audio vs Video.
     MediaData::Type mType;
     RefPtr<MediaTrackDemuxer> mTrackDemuxer;
     // TaskQueue on which decoder can choose to decode.
     // Only non-null up until the decoder is created.
@@ -262,16 +263,19 @@ private:
     bool mDemuxEOS;
     bool mWaitingForData;
     bool mWaitingForKey;
     bool mReceivedNewData;
 
     // Pending seek.
     MozPromiseRequestHolder<MediaTrackDemuxer::SeekPromise> mSeekRequest;
 
+    // Last demuxed raw keyframe
+    RefPtr<MediaRawData> mLastDemuxedKeyframe;
+
     // Queued demux samples waiting to be decoded.
     nsTArray<RefPtr<MediaRawData>> mQueuedSamples;
     MozPromiseRequestHolder<MediaTrackDemuxer::SamplesPromise> mDemuxRequest;
     // A WaitingPromise is pending if the demuxer is waiting for data or
     // if the decoder is waiting for a key.
     MozPromiseHolder<WaitForDataPromise> mWaitingPromise;
     bool HasWaitingPromise() const
     {
@@ -396,16 +400,17 @@ private:
       mOutput.Clear();
       mNumSamplesInput = 0;
       mNumSamplesOutput = 0;
       mSizeOfQueue = 0;
       mNextStreamSourceID.reset();
       if (!HasFatalError()) {
         mError.reset();
       }
+      mForceLastKeyframe = false;
     }
 
     bool HasInternalSeekPending() const
     {
       return mTimeThreshold && !mTimeThreshold.ref().mHasSeeked;
     }
 
     // Used by the MDSM for logging purposes.
@@ -419,17 +424,18 @@ private:
     media::TimeIntervals mTimeRanges;
     Maybe<media::TimeUnit> mLastTimeRangesEnd;
     // TrackInfo as first discovered during ReadMetadata.
     UniquePtr<TrackInfo> mOriginalInfo;
     RefPtr<SharedTrackInfo> mInfo;
     Maybe<media::TimeUnit> mFirstDemuxedSampleTime;
     // Use BlankDecoderModule or not.
     bool mIsBlankDecode;
-
+    // On next DoDemuxVideo, return the last keyframe demuxed.
+    bool mForceLastKeyframe;
   };
 
   class DecoderDataWithPromise : public DecoderData {
   public:
     DecoderDataWithPromise(MediaFormatReader* aOwner,
                            MediaData::Type aType,
                            uint32_t aNumOfMaxError)
       : DecoderData(aOwner, aType, aNumOfMaxError)
--- a/dom/media/SeekTarget.h
+++ b/dom/media/SeekTarget.h
@@ -20,16 +20,17 @@ enum class MediaDecoderEventVisibility :
 // "Fast" (nearest keyframe), or "Video Only" (no audio seek) seek was
 // requested.
 struct SeekTarget {
   enum Type {
     Invalid,
     PrevSyncPoint,
     Accurate,
     NextFrame,
+    PrevFrame,
   };
   SeekTarget()
     : mTime(media::TimeUnit::Invalid())
     , mType(SeekTarget::Invalid)
     , mVideoOnly(false)
   {
   }
   SeekTarget(int64_t aTimeUsecs,
@@ -80,16 +81,19 @@ struct SeekTarget {
     return mType == SeekTarget::Type::PrevSyncPoint;
   }
   bool IsAccurate() const {
     return mType == SeekTarget::Type::Accurate;
   }
   bool IsNextFrame() const {
     return mType == SeekTarget::Type::NextFrame;
   }
+  bool IsPrevFrame() const {
+    return mType == SeekTarget::Type::PrevFrame;
+  }
   bool IsVideoOnly() const {
     return mVideoOnly;
   }
 
 private:
   // Seek target time.
   media::TimeUnit mTime;
   // Whether we should seek "Fast", or "Accurate".