Bug 1235301 part 2 - implement NextFrameSeekTask; r=jwwang draft
authorKaku Kuo <tkuo@mozilla.com>
Tue, 24 May 2016 11:03:12 +0800
changeset 372167 1a205db83a22c9d91f7fac0ddf0d184d0591cba7
parent 372166 74fe1a49abb17a9b8129330bcb70181520146140
child 372168 bce191db59199d25f79d954c9573512961b57afd
push id19455
push usertkuo@mozilla.com
push dateFri, 27 May 2016 16:28:45 +0000
reviewersjwwang
bugs1235301
milestone49.0a1
Bug 1235301 part 2 - implement NextFrameSeekTask; r=jwwang MozReview-Commit-ID: 3ucCLzT6w27
dom/media/NextFrameSeekTask.cpp
dom/media/NextFrameSeekTask.h
dom/media/SeekTarget.h
dom/media/moz.build
new file mode 100644
--- /dev/null
+++ b/dom/media/NextFrameSeekTask.cpp
@@ -0,0 +1,491 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "NextFrameSeekTask.h"
+#include "MediaDecoderReaderWrapper.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Assertions.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+
+extern LazyLogModule gMediaDecoderLog;
+extern LazyLogModule gMediaSampleLog;
+
+// avoid redefined macro in unified build
+#undef LOG
+#undef DECODER_LOG
+#undef VERBOSE_LOG
+
+#define LOG(m, l, x, ...) \
+  MOZ_LOG(m, l, ("[NextFrameSeekTask] Decoder=%p " x, mDecoderID, ##__VA_ARGS__))
+#define DECODER_LOG(x, ...) \
+  LOG(gMediaDecoderLog, LogLevel::Debug, x, ##__VA_ARGS__)
+#define VERBOSE_LOG(x, ...) \
+  LOG(gMediaDecoderLog, LogLevel::Verbose, x, ##__VA_ARGS__)
+#define SAMPLE_LOG(x, ...) \
+  LOG(gMediaSampleLog, LogLevel::Debug, x, ##__VA_ARGS__)
+
+// Somehow MSVC doesn't correctly delete the comma before ##__VA_ARGS__
+// when __VA_ARGS__ expands to nothing. This is a workaround for it.
+#define DECODER_WARN_HELPER(a, b) NS_WARNING b
+#define DECODER_WARN(x, ...) \
+  DECODER_WARN_HELPER(0, (nsPrintfCString("Decoder=%p " x, mDecoderID, ##__VA_ARGS__).get()))
+
+namespace media {
+
+NextFrameSeekTask::NextFrameSeekTask(const void* aDecoderID,
+                                   AbstractThread* aThread,
+                                   MediaDecoderReaderWrapper* aReader,
+                                   SeekJob&& aSeekJob,
+                                   const MediaInfo& aInfo,
+                                   const media::TimeUnit& aDuration,
+                                   int64_t aCurrentMediaTime,
+                                   MediaQueue<MediaData>& aAudioQueue,
+                                   MediaQueue<MediaData>& aVideoQueue)
+  : SeekTask(aDecoderID, aThread, aReader, Move(aSeekJob))
+  , mAudioQueue(aAudioQueue)
+  , mVideoQueue(aVideoQueue)
+  , mCurrentTimeBeforeSeek(aCurrentMediaTime)
+  , mHasAudio(aInfo.HasAudio())
+  , mHasVideo(aInfo.HasVideo())
+  , mDuration(aDuration)
+{
+  AssertOwnerThread();
+  MOZ_ASSERT(HasVideo());
+
+  // Configure MediaDecoderReaderWrapper.
+  SetMediaDecoderReaderWrapperCallback();
+}
+
+NextFrameSeekTask::~NextFrameSeekTask()
+{
+  AssertOwnerThread();
+  MOZ_ASSERT(mIsDiscarded);
+}
+
+bool
+NextFrameSeekTask::HasAudio() const
+{
+  AssertOwnerThread();
+  return mHasAudio;
+}
+
+bool
+NextFrameSeekTask::HasVideo() const
+{
+  AssertOwnerThread();
+  return mHasVideo;
+}
+
+void
+NextFrameSeekTask::Discard()
+{
+  AssertOwnerThread();
+
+  // Disconnect MediaDecoder.
+  mSeekJob.RejectIfExists(__func__);
+
+  // Disconnect MDSM.
+  RejectIfExist(__func__);
+
+  // Disconnect MediaDecoderReader.
+  CancelMediaDecoderReaderWrapperCallback();
+
+  mIsDiscarded = true;
+}
+
+bool
+NextFrameSeekTask::NeedToResetMDSM() const
+{
+  AssertOwnerThread();
+  return false;
+}
+
+static int64_t
+FindNextFrame(MediaQueue<MediaData>& aQueue, int64_t aTime)
+{
+  AutoTArray<RefPtr<MediaData>, 16> frames;
+  aQueue.GetFirstElements(aQueue.GetSize(), &frames);
+  for (auto&& frame : frames) {
+    if (frame->mTime > aTime) {
+      return frame->mTime;
+    }
+  }
+  return -1;
+}
+
+static void
+DropFramesUntil(MediaQueue<MediaData>& aQueue, int64_t aTime) {
+  while (aQueue.GetSize() > 0) {
+    if (aQueue.PeekFront()->mTime < aTime) {
+      RefPtr<MediaData> releaseMe = aQueue.PopFront();
+      continue;
+    }
+    break;
+  }
+}
+
+static void
+DropAllFrames(MediaQueue<MediaData>& aQueue) {
+  while(aQueue.GetSize() > 0) {
+    RefPtr<MediaData> releaseMe = aQueue.PopFront();
+  }
+}
+
+static void
+DropAllMediaDataBeforeCurrentPosition(MediaQueue<MediaData>& aAudioQueue,
+                                      MediaQueue<MediaData>& aVideoQueue,
+                                      int64_t const aCurrentTimeBeforeSeek)
+{
+  // Drop all audio/video data before GetMediaTime();
+  int64_t newPos = FindNextFrame(aVideoQueue, aCurrentTimeBeforeSeek);
+  if (newPos < 0) {
+    // In this case, we cannot find the next frame in the video queue, so
+    // the NextFrameSeekTask needs to decode video data.
+    DropAllFrames(aVideoQueue);
+    if (aVideoQueue.IsFinished()) {
+      DropAllFrames(aAudioQueue);
+    }
+  } else {
+    DropFramesUntil(aVideoQueue, newPos);
+    DropFramesUntil(aAudioQueue, newPos);
+    // So now, the 1st data in the video queue should be the target of the
+    // NextFrameSeekTask.
+  }
+}
+
+RefPtr<NextFrameSeekTask::SeekTaskPromise>
+NextFrameSeekTask::Seek(const media::TimeUnit&)
+{
+  AssertOwnerThread();
+
+  DropAllMediaDataBeforeCurrentPosition(mAudioQueue, mVideoQueue,
+                                        mCurrentTimeBeforeSeek);
+
+  // While creating this seek task object, MDSM might had already ask the
+  // wrapper to decode a media sample or the MDSM is waiting a media data.
+  // If so, we cannot resolve the SeekTaskPromise immediately because there is
+  // a latency of running the resolving runnable. Instead, if there is a pending
+  // media request, we wait for it.
+  if ((mVideoQueue.GetSize() > 0
+       && !mReader->IsRequestingAudioData() && !mReader->IsWaitingAudioData()
+       && !mReader->IsRequestingVideoData() && !mReader->IsWaitingVideoData())
+      || mVideoQueue.AtEndOfStream()) {
+    UpdateSeekTargetTime();
+    SeekTaskResolveValue val = {};  // Zero-initialize data members.
+    return SeekTask::SeekTaskPromise::CreateAndResolve(val, __func__);
+  } else {
+    // Only invoke EnsureVideoDecodeTaskQueued() if we have no video data; we
+    // might be here because we are waiting audio data, and don't bother to make
+    // more requests to reader in this case.
+    if (mVideoQueue.GetSize() == 0) {
+      EnsureVideoDecodeTaskQueued();
+    }
+    return mSeekTaskPromise.Ensure(__func__);
+  }
+}
+
+bool
+NextFrameSeekTask::IsVideoDecoding() const
+{
+  AssertOwnerThread();
+  return HasVideo() && !mIsVideoQueueFinished;
+}
+
+nsresult
+NextFrameSeekTask::EnsureVideoDecodeTaskQueued()
+{
+  AssertOwnerThread();
+  SAMPLE_LOG("EnsureVideoDecodeTaskQueued isDecoding=%d status=%s",
+             IsVideoDecoding(), VideoRequestStatus());
+
+  if (!IsVideoDecoding() ||
+      mReader->IsRequestingVideoData() ||
+      mReader->IsWaitingVideoData()) {
+    return NS_OK;
+  }
+
+  RequestVideoData();
+  return NS_OK;
+}
+
+const char*
+NextFrameSeekTask::VideoRequestStatus()
+{
+  AssertOwnerThread();
+
+  if (mReader->IsRequestingVideoData()) {
+    MOZ_DIAGNOSTIC_ASSERT(!mReader->IsWaitingVideoData());
+    return "pending";
+  } else if (mReader->IsWaitingVideoData()) {
+    return "waiting";
+  }
+  return "idle";
+}
+
+void
+NextFrameSeekTask::RequestVideoData()
+{
+  AssertOwnerThread();
+  SAMPLE_LOG("Queueing video task - queued=%i, decoder-queued=%o",
+             !!mSeekedVideoData, mReader->SizeOfVideoQueueInFrames());
+
+  mReader->RequestVideoData(false, media::TimeUnit());
+}
+
+bool
+NextFrameSeekTask::IsAudioSeekComplete()
+{
+  AssertOwnerThread();
+  SAMPLE_LOG("IsAudioSeekComplete() curTarVal=%d aqFin=%d aqSz=%d req=%d wait=%d",
+    mSeekJob.Exists(), mIsAudioQueueFinished, !!mSeekedAudioData,
+    mReader->IsRequestingAudioData(), mReader->IsWaitingAudioData());
+
+  // Just make sure that we are not requesting or waiting for audio data. We
+  // don't really need to get an decoded audio data or get EOS here.
+  return
+    !HasAudio() ||
+    (Exists() && !mReader->IsRequestingAudioData() && !mReader->IsWaitingAudioData());
+}
+
+bool
+NextFrameSeekTask::IsVideoSeekComplete()
+{
+  AssertOwnerThread();
+  SAMPLE_LOG("IsVideoSeekComplete() curTarVal=%d vqFin=%d vqSz=%d",
+      mSeekJob.Exists(), mIsVideoQueueFinished, !!mSeekedVideoData);
+
+  return
+    !HasVideo() || (Exists() && (mIsVideoQueueFinished || mSeekedVideoData));
+}
+
+void
+NextFrameSeekTask::CheckIfSeekComplete()
+{
+  AssertOwnerThread();
+
+  const bool audioSeekComplete = IsAudioSeekComplete();
+
+  const bool videoSeekComplete = IsVideoSeekComplete();
+  if (HasVideo() && !videoSeekComplete) {
+    // We haven't reached the target. Ensure we have requested another sample.
+    if (NS_FAILED(EnsureVideoDecodeTaskQueued())) {
+      DECODER_WARN("Failed to request video during seek");
+      RejectIfExist(__func__);
+    }
+  }
+
+  SAMPLE_LOG("CheckIfSeekComplete() audioSeekComplete=%d videoSeekComplete=%d",
+    audioSeekComplete, videoSeekComplete);
+
+  if (audioSeekComplete && videoSeekComplete) {
+    UpdateSeekTargetTime();
+    Resolve(__func__); // Call to MDSM::SeekCompleted();
+  }
+}
+
+void
+NextFrameSeekTask::OnAudioDecoded(MediaData* aAudioSample)
+{
+  AssertOwnerThread();
+  MOZ_ASSERT(aAudioSample);
+
+  // The MDSM::mDecodedAudioEndTime will be updated once the whole SeekTask is
+  // resolved.
+
+  SAMPLE_LOG("OnAudioDecoded [%lld,%lld] disc=%d",
+             (aAudioSample ? aAudioSample->mTime : -1),
+             (aAudioSample ? aAudioSample->GetEndTime() : -1),
+             (aAudioSample ? aAudioSample->mDiscontinuity : 0));
+
+  if (!Exists()) {
+    // We've received a sample from a previous decode. Discard it.
+    return;
+  }
+
+  // We accept any audio data here.
+  mSeekedAudioData = aAudioSample;
+
+  CheckIfSeekComplete();
+}
+
+void
+NextFrameSeekTask::OnAudioNotDecoded(MediaDecoderReader::NotDecodedReason aReason)
+{
+  AssertOwnerThread();
+  SAMPLE_LOG("OnAudioNotDecoded (aReason=%u)", aReason);
+
+  if (!Exists()) {
+    // We've received a sample from a previous decode. Discard it.
+    return;
+  }
+
+  // We don't really handle audio deocde error here. Let MDSM to trigger further
+  // audio decoding tasks if it needs to play audio, and MDSM will then receive
+  // the decoding state from MediaDecoderReader.
+
+  CheckIfSeekComplete();
+}
+
+void
+NextFrameSeekTask::OnVideoDecoded(MediaData* aVideoSample)
+{
+  AssertOwnerThread();
+  MOZ_ASSERT(aVideoSample);
+
+  // The MDSM::mDecodedVideoEndTime will be updated once the whole SeekTask is
+  // resolved.
+
+  SAMPLE_LOG("OnVideoDecoded [%lld,%lld] disc=%d",
+             (aVideoSample ? aVideoSample->mTime : -1),
+             (aVideoSample ? aVideoSample->GetEndTime() : -1),
+             (aVideoSample ? aVideoSample->mDiscontinuity : 0));
+
+  if (!Exists()) {
+    // We've received a sample from a previous decode. Discard it.
+    return;
+  }
+
+  if (aVideoSample->mTime > mCurrentTimeBeforeSeek) {
+    mSeekedVideoData = aVideoSample;
+  }
+
+  CheckIfSeekComplete();
+}
+
+void
+NextFrameSeekTask::OnVideoNotDecoded(MediaDecoderReader::NotDecodedReason aReason)
+{
+  AssertOwnerThread();
+  SAMPLE_LOG("OnVideoNotDecoded (aReason=%u)", aReason);
+
+  if (!Exists()) {
+    // We've received a sample from a previous decode. Discard it.
+    return;
+  }
+
+  if (aReason == MediaDecoderReader::DECODE_ERROR) {
+    if (mVideoQueue.GetSize() > 0) {
+      // The video decoding request might be filed by MDSM not the
+      // NextFrameSeekTask itself. So, the NextFrameSeekTask might has already
+      // found its target in the VideoQueue but still waits the video decoding
+      // request (which is filed by the MDSM) to be resolved. In this case, we
+      // already have the target of this seek task, try to resolve this task.
+      CheckIfSeekComplete();
+      return;
+    }
+
+    // Otherwise, we cannot get the target video frame of this seek task,
+    // delegate the decode error to the generic error path.
+    RejectIfExist(__func__);
+    return;
+  }
+
+  // If the decoder is waiting for data, we tell it to call us back when the
+  // data arrives.
+  if (aReason == MediaDecoderReader::WAITING_FOR_DATA) {
+    MOZ_ASSERT(mReader->IsWaitForDataSupported(),
+               "Readers that send WAITING_FOR_DATA need to implement WaitForData");
+    mReader->WaitForData(MediaData::VIDEO_DATA);
+
+    // We are out of data to decode and will enter buffering mode soon.
+    // We want to play the frames we have already decoded, so we stop pre-rolling
+    // and ensure that loadeddata is fired as required.
+    mNeedToStopPrerollingVideo = true;
+    return;
+  }
+
+  if (aReason == MediaDecoderReader::CANCELED) {
+    EnsureVideoDecodeTaskQueued();
+    return;
+  }
+
+  if (aReason == MediaDecoderReader::END_OF_STREAM) {
+    mIsVideoQueueFinished = true;
+    CheckIfSeekComplete();
+  }
+}
+
+void
+NextFrameSeekTask::SetMediaDecoderReaderWrapperCallback()
+{
+  AssertOwnerThread();
+
+  // Register dummy callbcak for audio decoding since we don't need to handle
+  // the decoded audio samples.
+  mAudioCallbackID =
+    mReader->SetAudioCallback(this, &NextFrameSeekTask::OnAudioDecoded,
+                                    &NextFrameSeekTask::OnAudioNotDecoded);
+
+  mVideoCallbackID =
+    mReader->SetVideoCallback(this, &NextFrameSeekTask::OnVideoDecoded,
+                                    &NextFrameSeekTask::OnVideoNotDecoded);
+
+  RefPtr<NextFrameSeekTask> self = this;
+  mWaitAudioCallbackID =
+    mReader->SetWaitAudioCallback(
+      [self] (MediaData::Type aType) -> void {
+        self->AssertOwnerThread();
+        // We don't make an audio decode request here, instead, let MDSM to
+        // trigger further audio decode tasks if MDSM itself needs to play audio.
+      },
+      [self] (WaitForDataRejectValue aRejection) -> void {
+        self->AssertOwnerThread();
+      });
+
+  mWaitVideoCallbackID =
+    mReader->SetWaitVideoCallback(
+      [self] (MediaData::Type aType) -> void {
+        self->AssertOwnerThread();
+        self->EnsureVideoDecodeTaskQueued();
+      },
+      [self] (WaitForDataRejectValue aRejection) -> void {
+        self->AssertOwnerThread();
+      });
+
+  DECODER_LOG("NextFrameSeekTask set audio callbacks: mVideoCallbackID = %d\n", (int)mAudioCallbackID);
+  DECODER_LOG("NextFrameSeekTask set video callbacks: mVideoCallbackID = %d\n", (int)mVideoCallbackID);
+  DECODER_LOG("NextFrameSeekTask set wait audio callbacks: mWaitAudioCallbackID = %d\n", (int)mWaitAudioCallbackID);
+  DECODER_LOG("NextFrameSeekTask set wait video callbacks: mWaitVideoCallbackID = %d\n", (int)mWaitVideoCallbackID);
+}
+
+void
+NextFrameSeekTask::CancelMediaDecoderReaderWrapperCallback()
+{
+  AssertOwnerThread();
+
+  DECODER_LOG("NextFrameSeekTask cancel audio callbacks: mAudioCallbackID = %d\n", (int)mAudioCallbackID);
+  mReader->CancelAudioCallback(mAudioCallbackID);
+
+  DECODER_LOG("NextFrameSeekTask cancel video callbacks: mVideoCallbackID = %d\n", (int)mVideoCallbackID);
+  mReader->CancelVideoCallback(mVideoCallbackID);
+
+  DECODER_LOG("NextFrameSeekTask cancel wait audio callbacks: mWaitAudioCallbackID = %d\n", (int)mWaitAudioCallbackID);
+  mReader->CancelWaitAudioCallback(mWaitAudioCallbackID);
+
+  DECODER_LOG("NextFrameSeekTask cancel wait video callbacks: mWaitVideoCallbackID = %d\n", (int)mWaitVideoCallbackID);
+  mReader->CancelWaitVideoCallback(mWaitVideoCallbackID);
+}
+
+void
+NextFrameSeekTask::UpdateSeekTargetTime()
+{
+  AssertOwnerThread();
+
+  RefPtr<MediaData> data = mVideoQueue.PeekFront();
+  if (data) {
+    mSeekJob.mTarget.SetTime(TimeUnit::FromMicroseconds(data->mTime));
+  } else if (mSeekedVideoData) {
+    mSeekJob.mTarget.SetTime(TimeUnit::FromMicroseconds(mSeekedVideoData->mTime));
+  } else if (mIsVideoQueueFinished || mVideoQueue.AtEndOfStream()) {
+    mSeekJob.mTarget.SetTime(mDuration);
+  } else {
+    MOZ_ASSERT(false, "No data!");
+  }
+}
+} // namespace media
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/media/NextFrameSeekTask.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef NEXTFRAME_SEEK_TASK_H
+#define NEXTFRAME_SEEK_TASK_H
+
+#include "SeekTask.h"
+#include "MediaDecoderReader.h"
+
+namespace mozilla {
+namespace media {
+
+/*
+ * While invoking a NextFrameSeekTask, we don't know the seek target time, what
+ * we know is the media's currant position. We use the media's currant position
+ * to find out what the next frame is, by traversing through the video queue or
+ * asking the decoder to decode more video frames. Once we confirm the next
+ * frame, we then know the target time of the NextFrameSeekTask and we update it
+ * so that the MDSM will be able to update the media element's position.
+ */
+
+class NextFrameSeekTask final : public SeekTask {
+public:
+  NextFrameSeekTask(const void* aDecoderID,
+                   AbstractThread* aThread,
+                   MediaDecoderReaderWrapper* aReader,
+                   SeekJob&& aSeekJob,
+                   const MediaInfo& aInfo,
+                   const media::TimeUnit& aDuration,
+                   int64_t aCurrentMediaTime,
+                   MediaQueue<MediaData>& aAudioQueue,
+                   MediaQueue<MediaData>& aVideoQueue);
+
+  void Discard() override;
+
+  RefPtr<SeekTaskPromise> Seek(const media::TimeUnit& aDuration) override;
+
+  bool NeedToResetMDSM() const override;
+
+private:
+  ~NextFrameSeekTask();
+
+  bool HasAudio() const;
+
+  bool HasVideo() const;
+
+  bool IsVideoDecoding() const;
+
+  nsresult EnsureVideoDecodeTaskQueued();
+
+  const char* VideoRequestStatus();
+
+  void RequestVideoData();
+
+  bool IsAudioSeekComplete();
+
+  bool IsVideoSeekComplete();
+
+  void CheckIfSeekComplete();
+
+  void OnAudioDecoded(MediaData* aAudioSample);
+
+  void OnAudioNotDecoded(MediaDecoderReader::NotDecodedReason aReason);
+
+  void OnVideoDecoded(MediaData* aVideoSample);
+
+  void OnVideoNotDecoded(MediaDecoderReader::NotDecodedReason aReason);
+
+  void SetMediaDecoderReaderWrapperCallback();
+
+  void CancelMediaDecoderReaderWrapperCallback();
+
+  // Update the seek target's time before resolving this seek task, the updated
+  // time will be used in the MDSM::SeekCompleted() to update the MDSM's position.
+  void UpdateSeekTargetTime();
+
+  /*
+   * Data shared with MDSM.
+   */
+  MediaQueue<MediaData>& mAudioQueue;
+  MediaQueue<MediaData>& mVideoQueue;
+
+  /*
+   * Internal state.
+   */
+  const int64_t mCurrentTimeBeforeSeek;
+  const bool mHasAudio;
+  const bool mHasVideo;
+  media::TimeUnit mDuration;
+
+  /*
+   * Track the current seek promise made by the reader.
+   */
+  CallbackID mAudioCallbackID;
+  CallbackID mVideoCallbackID;
+  CallbackID mWaitAudioCallbackID;
+  CallbackID mWaitVideoCallbackID;
+};
+
+} // namespace media
+} // namespace mozilla
+
+#endif /* NEXTFRAME_SEEK_TASK_H */
--- 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,
     AccurateVideoOnly,
+    NextFrame,
   };
   SeekTarget()
     : mEventVisibility(MediaDecoderEventVisibility::Observable)
     , mTime(media::TimeUnit::Invalid())
     , mType(SeekTarget::Invalid)
   {
   }
   SeekTarget(int64_t aTimeUsecs,
@@ -78,16 +79,19 @@ struct SeekTarget {
     return mType == SeekTarget::Type::PrevSyncPoint;
   }
   bool IsAccurate() const {
     return mType == SeekTarget::Type::Accurate;
   }
   bool IsVideoOnly() const {
     return mType == SeekTarget::Type::AccurateVideoOnly;
   }
+  bool IsNextFrame() const {
+    return mType == SeekTarget::Type::NextFrame;
+  }
 
   MediaDecoderEventVisibility mEventVisibility;
 
 private:
   // Seek target time.
   media::TimeUnit mTime;
   // Whether we should seek "Fast", or "Accurate".
   // "Fast" seeks to the seek point preceding mTime, whereas
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -129,16 +129,17 @@ EXPORTS += [
     'MediaStatistics.h',
     'MediaStreamGraph.h',
     'MediaTimer.h',
     'MediaTrack.h',
     'MediaTrackList.h',
     'MP3Decoder.h',
     'MP3Demuxer.h',
     'MP3FrameParser.h',
+    'NextFrameSeekTask.h',
     'nsIDocumentActivity.h',
     'PrincipalChangeObserver.h',
     'QueueObject.h',
     'RtspMediaResource.h',
     'SeekJob.h',
     'SeekTarget.h',
     'SeekTask.h',
     'SelfRef.h',
@@ -241,16 +242,17 @@ UNIFIED_SOURCES += [
     'MediaStreamGraph.cpp',
     'MediaStreamTrack.cpp',
     'MediaTimer.cpp',
     'MediaTrack.cpp',
     'MediaTrackList.cpp',
     'MP3Decoder.cpp',
     'MP3Demuxer.cpp',
     'MP3FrameParser.cpp',
+    'NextFrameSeekTask.cpp',
     'QueueObject.cpp',
     'RtspMediaResource.cpp',
     'SeekJob.cpp',
     'SeekTask.cpp',
     'StreamTracks.cpp',
     'TextTrack.cpp',
     'TextTrackCue.cpp',
     'TextTrackCueList.cpp',