Bug 1235301 - part 2 - implement NextFrameSeekTask; r=jwwang
authorKaku Kuo <tkuo@mozilla.com>
Tue, 24 May 2016 11:03:12 +0800
changeset 338551 7e82fc4bc49ee64e95316c4afbfebef3660e7ba3
parent 338550 a462681c5a773844a8cb362a83e04fa7d24602a9
child 338552 c70fa3dc42c07926cd94c8d66a52fcf431e1aac9
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwwang
bugs1235301
milestone49.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 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
@@ -128,16 +128,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',
@@ -239,16 +240,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',