Bug 1038031 - Make MP4Reader decode asynchronously - r=kinetik,cpearce
authorChris Pearce <cpearce@mozilla.com>, Edwin Flores <edwin@mozilla.com>, Matt Woodrow <mwoodrow@mozilla.com>
Sun, 20 Jul 2014 12:54:00 +1200
changeset 241126 deb0b389efa7a82812c04a73657aeecf1ff5d663
parent 241125 bbc87380fd6a1ef4c16cf619e8ba56a97241ba1e
child 241127 d2828e139d252c1d2c5027dc38d0b96ac645d663
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskinetik, cpearce
bugs1038031
milestone36.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 1038031 - Make MP4Reader decode asynchronously - r=kinetik,cpearce
dom/media/MediaDecoderReader.h
dom/media/MediaDecoderStateMachine.cpp
dom/media/fmp4/MP4Reader.cpp
dom/media/fmp4/MP4Reader.h
dom/media/fmp4/PlatformDecoderModule.h
dom/media/gtest/TestMP4Reader.cpp
--- a/dom/media/MediaDecoderReader.h
+++ b/dom/media/MediaDecoderReader.h
@@ -273,17 +273,18 @@ private:
 // BreakCycles() during shutdown.
 class RequestSampleCallback {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RequestSampleCallback)
 
   enum NotDecodedReason {
     END_OF_STREAM,
     DECODE_ERROR,
-    WAITING_FOR_DATA
+    WAITING_FOR_DATA,
+    CANCELED
   };
 
   // Receives the result of a RequestAudioData() call.
   virtual void OnAudioDecoded(AudioData* aSample) = 0;
 
   // Receives the result of a RequestVideoData() call.
   virtual void OnVideoDecoded(VideoData* aSample) = 0;
 
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -627,16 +627,21 @@ MediaDecoderStateMachine::DecodeVideo()
 
   mReader->RequestVideoData(skipToNextKeyFrame, currentTime);
 }
 
 bool
 MediaDecoderStateMachine::NeedToDecodeAudio()
 {
   AssertCurrentThreadInMonitor();
+  SAMPLE_LOG("NeedToDecodeAudio() isDec=%d decToTar=%d minPrl=%d seek=%d enufAud=%d",
+             IsAudioDecoding(), mDecodeToSeekTarget, mMinimizePreroll,
+             mState == DECODER_STATE_SEEKING,
+             HaveEnoughDecodedAudio(mAmpleAudioThresholdUsecs * mPlaybackRate));
+
   return IsAudioDecoding() &&
          ((mState == DECODER_STATE_SEEKING && mDecodeToSeekTarget) ||
           (!mMinimizePreroll &&
           !HaveEnoughDecodedAudio(mAmpleAudioThresholdUsecs * mPlaybackRate) &&
           (mState != DECODER_STATE_SEEKING || mDecodeToSeekTarget)));
 }
 
 void
@@ -828,16 +833,21 @@ MediaDecoderStateMachine::OnNotDecoded(M
     bool outOfSamples = isAudio ? !AudioQueue().GetSize() : !VideoQueue().GetSize();
     if (outOfSamples) {
       StartBuffering();
     }
 
     return;
   }
 
+  if (aReason == RequestSampleCallback::CANCELED) {
+    DispatchDecodeTasksIfNeeded();
+    return;
+  }
+
   // This is an EOS. Finish off the queue, and then handle things based on our
   // state.
   MOZ_ASSERT(aReason == RequestSampleCallback::END_OF_STREAM);
   if (!isAudio && mState == DECODER_STATE_SEEKING &&
       mCurrentSeekTarget.IsValid() && 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()
@@ -1717,27 +1727,28 @@ MediaDecoderStateMachine::DispatchDecode
              (!needToDecodeAudio && !needToDecodeVideo));
 
   bool needIdle = !mDecoder->IsLogicallyPlaying() &&
                   mState != DECODER_STATE_SEEKING &&
                   !needToDecodeAudio &&
                   !needToDecodeVideo &&
                   !IsPlaying();
 
+  SAMPLE_LOG("DispatchDecodeTasksIfNeeded needAudio=%d dispAudio=%d needVideo=%d dispVid=%d needIdle=%d",
+             needToDecodeAudio, mAudioRequestPending,
+             needToDecodeVideo, mVideoRequestPending,
+             needIdle);
+
   if (needToDecodeAudio) {
     EnsureAudioDecodeTaskQueued();
   }
   if (needToDecodeVideo) {
     EnsureVideoDecodeTaskQueued();
   }
 
-  SAMPLE_LOG("DispatchDecodeTasksIfNeeded needAudio=%d dispAudio=%d needVideo=%d dispVid=%d needIdle=%d",
-             needToDecodeAudio, mAudioRequestPending,
-             needToDecodeVideo, mVideoRequestPending,
-             needIdle);
 
   if (needIdle) {
     RefPtr<nsIRunnable> event = NS_NewRunnableMethod(
         this, &MediaDecoderStateMachine::SetReaderIdle);
     nsresult rv = mDecodeTaskQueue->Dispatch(event.forget());
     if (NS_FAILED(rv) && mState != DECODER_STATE_SHUTDOWN) {
       DECODER_WARN("Failed to dispatch event to set decoder idle state");
     }
@@ -2246,19 +2257,20 @@ void MediaDecoderStateMachine::DecodeSee
       NS_NewRunnableMethod(mDecoder, &MediaDecoder::SeekingStarted);
     NS_DispatchToMainThread(startEvent, NS_DISPATCH_SYNC);
   }
   if (mState != DECODER_STATE_SEEKING) {
     // May have shutdown while we released the monitor.
     return;
   }
 
+  mDecodeToSeekTarget = false;
+
   if (!currentTimeChanged) {
     DECODER_LOG("Seek !currentTimeChanged...");
-    mDecodeToSeekTarget = false;
     nsresult rv = mDecodeTaskQueue->Dispatch(
       NS_NewRunnableMethod(this, &MediaDecoderStateMachine::SeekCompleted));
     if (NS_FAILED(rv)) {
       DecodeError();
     }
   } else {
     // The seek target is different than the current playback position,
     // we'll need to seek the playback position, so shutdown our decode
@@ -2382,29 +2394,31 @@ MediaDecoderStateMachine::SeekCompleted(
   UpdatePlaybackPositionInternal(newCurrentTime);
   if (mDecoder->GetDecodedStream()) {
     SetSyncPointForMediaStream();
   }
 
   // Try to decode another frame to detect if we're at the end...
   DECODER_LOG("Seek completed, mCurrentFrameTime=%lld", mCurrentFrameTime);
 
+  mCurrentSeekTarget = SeekTarget();
+
+  // 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);
   }
 
-  // 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;
-
   ScheduleStateMachine();
   mScheduler->ThawScheduling();
 }
 
 // Runnable to dispose of the decoder and state machine on the main thread.
 class nsDecoderDisposeEvent : public nsRunnable {
 public:
   nsDecoderDisposeEvent(already_AddRefed<MediaDecoder> aDecoder,
--- a/dom/media/fmp4/MP4Reader.cpp
+++ b/dom/media/fmp4/MP4Reader.cpp
@@ -28,18 +28,20 @@ using mozilla::layers::LayersBackend;
 PRLogModuleInfo* GetDemuxerLog() {
   static PRLogModuleInfo* log = nullptr;
   if (!log) {
     log = PR_NewLogModule("MP4Demuxer");
   }
   return log;
 }
 #define LOG(...) PR_LOG(GetDemuxerLog(), PR_LOG_DEBUG, (__VA_ARGS__))
+#define VLOG(...) PR_LOG(GetDemuxerLog(), PR_LOG_DEBUG+1, (__VA_ARGS__))
 #else
 #define LOG(...)
+#define VLOG(...)
 #endif
 
 using namespace mp4_demuxer;
 
 namespace mozilla {
 
 // Uncomment to enable verbose per-sample logging.
 //#define LOG_SAMPLE_DECODE 1
@@ -129,24 +131,38 @@ MP4Reader::MP4Reader(AbstractMediaDecode
   , mIndexMonitor("MP4 index")
 {
   MOZ_ASSERT(NS_IsMainThread(), "Must be on main thread.");
   MOZ_COUNT_CTOR(MP4Reader);
 }
 
 MP4Reader::~MP4Reader()
 {
-  MOZ_ASSERT(NS_IsMainThread(), "Must be on main thread.");
   MOZ_COUNT_DTOR(MP4Reader);
-  Shutdown();
 }
 
+class DestroyPDMTask : public nsRunnable {
+public:
+  DestroyPDMTask(nsAutoPtr<PlatformDecoderModule>& aPDM)
+    : mPDM(aPDM)
+  {}
+  NS_IMETHOD Run() MOZ_OVERRIDE {
+    MOZ_ASSERT(NS_IsMainThread());
+    mPDM = nullptr;
+    return NS_OK;
+  }
+private:
+  nsAutoPtr<PlatformDecoderModule> mPDM;
+};
+
 void
 MP4Reader::Shutdown()
 {
+  MOZ_ASSERT(GetTaskQueue()->IsCurrentThreadIn());
+
   if (mAudio.mDecoder) {
     Flush(kAudio);
     mAudio.mDecoder->Shutdown();
     mAudio.mDecoder = nullptr;
   }
   if (mAudio.mTaskQueue) {
     mAudio.mTaskQueue->Shutdown();
     mAudio.mTaskQueue = nullptr;
@@ -159,18 +175,20 @@ MP4Reader::Shutdown()
   if (mVideo.mTaskQueue) {
     mVideo.mTaskQueue->Shutdown();
     mVideo.mTaskQueue = nullptr;
   }
   // Dispose of the queued sample before shutting down the demuxer
   mQueuedVideoSample = nullptr;
 
   if (mPlatform) {
-    mPlatform->Shutdown();
-    mPlatform = nullptr;
+    // PDMs are supposed to be destroyed on the main thread...
+    nsRefPtr<DestroyPDMTask> task(new DestroyPDMTask(mPlatform));
+    MOZ_ASSERT(!mPlatform);
+    NS_DispatchToMainThread(task);
   }
 }
 
 void
 MP4Reader::InitLayersBackendType()
 {
   if (!IsVideoContentType(mDecoder->GetResource()->GetContentType())) {
     // Not playing video, we don't care about the layers backend type.
@@ -201,22 +219,20 @@ nsresult
 MP4Reader::Init(MediaDecoderReader* aCloneDonor)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Must be on main thread.");
   PlatformDecoderModule::Init();
   mDemuxer = new MP4Demuxer(new MP4Stream(mDecoder->GetResource()));
 
   InitLayersBackendType();
 
-  mAudio.mTaskQueue = new MediaTaskQueue(
-    SharedThreadPool::Get(NS_LITERAL_CSTRING("MP4 Audio Decode")));
+  mAudio.mTaskQueue = new MediaTaskQueue(GetMediaDecodeThreadPool());
   NS_ENSURE_TRUE(mAudio.mTaskQueue, NS_ERROR_FAILURE);
 
-  mVideo.mTaskQueue = new MediaTaskQueue(
-    SharedThreadPool::Get(NS_LITERAL_CSTRING("MP4 Video Decode")));
+  mVideo.mTaskQueue = new MediaTaskQueue(GetMediaDecodeThreadPool());
   NS_ENSURE_TRUE(mVideo.mTaskQueue, NS_ERROR_FAILURE);
 
   static bool sSetupPrefCache = false;
   if (!sSetupPrefCache) {
     sSetupPrefCache = true;
     Preferences::AddBoolVarCache(&sIsEMEEnabled, "media.eme.enabled", false);
   }
 
@@ -478,16 +494,186 @@ MP4Reader::HasVideo()
 
 MP4Reader::DecoderData&
 MP4Reader::GetDecoderData(TrackType aTrack)
 {
   MOZ_ASSERT(aTrack == kAudio || aTrack == kVideo);
   return (aTrack == kAudio) ? mAudio : mVideo;
 }
 
+void
+MP4Reader::RequestVideoData(bool aSkipToNextKeyframe,
+                            int64_t aTimeThreshold)
+{
+  MOZ_ASSERT(GetTaskQueue()->IsCurrentThreadIn());
+  VLOG("RequestVideoData skip=%d time=%lld", aSkipToNextKeyframe, aTimeThreshold);
+
+  // Record number of frames decoded and parsed. Automatically update the
+  // stats counters using the AutoNotifyDecoded stack-based class.
+  uint32_t parsed = 0, decoded = 0;
+  AbstractMediaDecoder::AutoNotifyDecoded autoNotify(mDecoder, parsed, decoded);
+
+  MOZ_ASSERT(HasVideo() && mPlatform && mVideo.mDecoder);
+
+  if (aSkipToNextKeyframe) {
+    if (!SkipVideoDemuxToNextKeyFrame(aTimeThreshold, parsed) ||
+        NS_FAILED(mVideo.mDecoder->Flush())) {
+      NS_WARNING("Failed to skip/flush video when skipping-to-next-keyframe.");
+    }
+  }
+
+  auto& decoder = GetDecoderData(kVideo);
+  MonitorAutoLock lock(decoder.mMonitor);
+  decoder.mOutputRequested = true;
+  ScheduleUpdate(kVideo);
+
+  // Report the number of "decoded" frames as the difference in the
+  // mNumSamplesOutput field since the last time we were called.
+  uint64_t delta = mVideo.mNumSamplesOutput - mLastReportedNumDecodedFrames;
+  decoded = static_cast<uint32_t>(delta);
+  mLastReportedNumDecodedFrames = mVideo.mNumSamplesOutput;
+}
+
+void
+MP4Reader::RequestAudioData()
+{
+  MOZ_ASSERT(GetTaskQueue()->IsCurrentThreadIn());
+  VLOG("RequestAudioData");
+  auto& decoder = GetDecoderData(kAudio);
+  MonitorAutoLock lock(decoder.mMonitor);
+  decoder.mOutputRequested = true;
+  ScheduleUpdate(kAudio);
+}
+
+void
+MP4Reader::ScheduleUpdate(TrackType aTrack)
+{
+  auto& decoder = GetDecoderData(aTrack);
+  decoder.mMonitor.AssertCurrentThreadOwns();
+  if (decoder.mUpdateScheduled) {
+    return;
+  }
+  VLOG("SchedulingUpdate(%s)", TrackTypeToStr(aTrack));
+  decoder.mUpdateScheduled = true;
+  RefPtr<nsIRunnable> task(
+    NS_NewRunnableMethodWithArg<TrackType>(this, &MP4Reader::Update, aTrack));
+  GetTaskQueue()->Dispatch(task.forget());
+}
+
+bool
+MP4Reader::NeedInput(DecoderData& aDecoder)
+{
+  aDecoder.mMonitor.AssertCurrentThreadOwns();
+  // We try to keep a few more compressed samples input than decoded samples
+  // have been output, provided the state machine has requested we send it a
+  // decoded sample. To account for H.264 streams which may require a longer
+  // run of input than we input, decoders fire an "input exhausted" callback,
+  // which overrides our "few more samples" threshold.
+  return
+    !aDecoder.mError &&
+    !aDecoder.mEOS &&
+    aDecoder.mOutputRequested &&
+    aDecoder.mOutput.IsEmpty() &&
+    (aDecoder.mInputExhausted ||
+     aDecoder.mNumSamplesInput - aDecoder.mNumSamplesOutput < aDecoder.mDecodeAhead);
+}
+
+void
+MP4Reader::Update(TrackType aTrack)
+{
+  MOZ_ASSERT(GetTaskQueue()->IsCurrentThreadIn());
+
+  bool needInput = false;
+  bool needOutput = false;
+  bool eos = false;
+  auto& decoder = GetDecoderData(aTrack);
+  nsRefPtr<MediaData> output;
+  {
+    MonitorAutoLock lock(decoder.mMonitor);
+    decoder.mUpdateScheduled = false;
+    if (NeedInput(decoder)) {
+      needInput = true;
+      decoder.mInputExhausted = false;
+      decoder.mNumSamplesInput++;
+    }
+    needOutput = decoder.mOutputRequested;
+    if (needOutput && !decoder.mOutput.IsEmpty()) {
+      output = decoder.mOutput[0];
+      decoder.mOutput.RemoveElementAt(0);
+    }
+    eos = decoder.mEOS;
+  }
+  VLOG("Update(%s) ni=%d no=%d iex=%d or=%d fl=%d",
+       TrackTypeToStr(aTrack),
+       needInput,
+       needOutput,
+       decoder.mInputExhausted,
+       decoder.mOutputRequested,
+       decoder.mIsFlushing);
+
+  if (needInput) {
+    MP4Sample* sample = PopSample(aTrack);
+    if (sample) {
+      decoder.mDecoder->Input(sample);
+    } else {
+      {
+        MonitorAutoLock lock(decoder.mMonitor);
+        MOZ_ASSERT(!decoder.mEOS);
+        eos = decoder.mEOS = true;
+      }
+      decoder.mDecoder->Drain();
+    }
+  }
+  if (needOutput) {
+    if (output) {
+      ReturnOutput(output, aTrack);
+    } else if (eos) {
+      ReturnEOS(aTrack);
+    }
+  }
+}
+
+void
+MP4Reader::ReturnOutput(MediaData* aData, TrackType aTrack)
+{
+  auto& decoder = GetDecoderData(aTrack);
+  {
+    MonitorAutoLock lock(decoder.mMonitor);
+    MOZ_ASSERT(decoder.mOutputRequested);
+    decoder.mOutputRequested = false;
+    if (decoder.mDiscontinuity) {
+      decoder.mDiscontinuity = false;
+      aData->mDiscontinuity = true;
+    }
+  }
+
+  if (aTrack == kAudio) {
+    AudioData* audioData = static_cast<AudioData*>(aData);
+
+    if (audioData->mChannels != mInfo.mAudio.mChannels ||
+        audioData->mRate != mInfo.mAudio.mRate) {
+      LOG("MP4Reader::ReturnOutput change of sampling rate:%d->%d",
+          mInfo.mAudio.mRate, audioData->mRate);
+      mInfo.mAudio.mRate = audioData->mRate;
+      mInfo.mAudio.mChannels = audioData->mChannels;
+    }
+
+    GetCallback()->OnAudioDecoded(audioData);
+  } else if (aTrack == kVideo) {
+    GetCallback()->OnVideoDecoded(static_cast<VideoData*>(aData));
+  }
+}
+
+void
+MP4Reader::ReturnEOS(TrackType aTrack)
+{
+  GetCallback()->OnNotDecoded(aTrack == kAudio ? MediaData::AUDIO_DATA : MediaData::VIDEO_DATA,
+                              RequestSampleCallback::END_OF_STREAM);
+}
+
 MP4Sample*
 MP4Reader::PopSample(TrackType aTrack)
 {
   switch (aTrack) {
     case kAudio:
       return mDemuxer->DemuxAudioSample();
 
     case kVideo:
@@ -496,169 +682,53 @@ MP4Reader::PopSample(TrackType aTrack)
       }
       return mDemuxer->DemuxVideoSample();
 
     default:
       return nullptr;
   }
 }
 
-// How async decoding works:
-//
-// When MP4Reader::Decode() is called:
-// * Lock the DecoderData. We assume the state machine wants
-//   output from the decoder (in future, we'll assume decoder wants input
-//   when the output MediaQueue isn't "full").
-// * Cache the value of mNumSamplesOutput, as prevFramesOutput.
-// * While we've not output data (mNumSamplesOutput != prevNumFramesOutput)
-//   and while we still require input, we demux and input data in the reader.
-//   We assume we require input if
-//   ((mNumSamplesInput - mNumSamplesOutput) < sDecodeAheadMargin) or
-//   mInputExhausted is true. Before we send input, we reset mInputExhausted
-//   and increment mNumFrameInput, and drop the lock on DecoderData.
-// * Once we no longer require input, we wait on the DecoderData
-//   lock for output, or for the input exhausted callback. If we receive the
-//   input exhausted callback, we go back and input more data.
-// * When our output callback is called, we take the DecoderData lock and
-//   increment mNumSamplesOutput. We notify the DecoderData lock. This will
-//   awaken the Decode thread, and unblock it, and it will return.
-bool
-MP4Reader::Decode(TrackType aTrack)
-{
-  DecoderData& data = GetDecoderData(aTrack);
-  MOZ_ASSERT(data.mDecoder);
-
-  data.mMonitor.Lock();
-  uint64_t prevNumFramesOutput = data.mNumSamplesOutput;
-  while (prevNumFramesOutput == data.mNumSamplesOutput) {
-    data.mMonitor.AssertCurrentThreadOwns();
-    if (data.mError) {
-      // Decode error!
-      data.mMonitor.Unlock();
-      return false;
-    }
-    // Send input to the decoder, if we need to. We assume the decoder
-    // needs input if it's told us it's out of input, or we're beneath the
-    // "low water mark" for the amount of input we've sent it vs the amount
-    // out output we've received. We always try to keep the decoder busy if
-    // possible, so we try to maintain at least a few input samples ahead,
-    // if we need output.
-    while (prevNumFramesOutput == data.mNumSamplesOutput &&
-           (data.mInputExhausted ||
-           (data.mNumSamplesInput - data.mNumSamplesOutput) < data.mDecodeAhead) &&
-           !data.mEOS) {
-      data.mMonitor.AssertCurrentThreadOwns();
-      data.mMonitor.Unlock();
-      nsAutoPtr<MP4Sample> compressed(PopSample(aTrack));
-      if (!compressed) {
-        // EOS, or error. Send the decoder a signal to drain.
-        LOG("MP4Reader: EOS or error - no samples available");
-        LOG("Draining %s", TrackTypeToStr(aTrack));
-        data.mMonitor.Lock();
-        MOZ_ASSERT(!data.mEOS);
-        data.mEOS = true;
-        MOZ_ASSERT(!data.mDrainComplete);
-        data.mDrainComplete = false;
-        data.mMonitor.Unlock();
-        data.mDecoder->Drain();
-      } else {
-#ifdef LOG_SAMPLE_DECODE
-        LOG("PopSample %s time=%lld dur=%lld", TrackTypeToStr(aTrack),
-            compressed->composition_timestamp, compressed->duration);
-#endif
-        data.mMonitor.Lock();
-        data.mDrainComplete = false;
-        data.mInputExhausted = false;
-        data.mNumSamplesInput++;
-        data.mMonitor.Unlock();
-
-        if (NS_FAILED(data.mDecoder->Input(compressed))) {
-          return false;
-        }
-        // If Input() failed, we let the auto pointer delete |compressed|.
-        // Otherwise, we assume the decoder will delete it when it's finished
-        // with it.
-        compressed.forget();
-      }
-      data.mMonitor.Lock();
-    }
-    data.mMonitor.AssertCurrentThreadOwns();
-    while (!data.mError &&
-           prevNumFramesOutput == data.mNumSamplesOutput &&
-           (!data.mInputExhausted || data.mEOS) &&
-           !data.mDrainComplete) {
-      data.mMonitor.Wait();
-    }
-    if (data.mError ||
-        (data.mEOS && data.mDrainComplete)) {
-      break;
-    }
-  }
-  data.mMonitor.AssertCurrentThreadOwns();
-  bool rv = !(data.mDrainComplete || data.mError);
-  data.mMonitor.Unlock();
-  return rv;
-}
-
 nsresult
 MP4Reader::ResetDecode()
 {
+  MOZ_ASSERT(GetTaskQueue()->IsCurrentThreadIn());
+  Flush(kVideo);
   Flush(kAudio);
-  Flush(kVideo);
   return MediaDecoderReader::ResetDecode();
 }
 
 void
 MP4Reader::Output(TrackType aTrack, MediaData* aSample)
 {
 #ifdef LOG_SAMPLE_DECODE
-  LOG("Decoded %s sample time=%lld dur=%lld",
+  VLOG("Decoded %s sample time=%lld dur=%lld",
       TrackTypeToStr(aTrack), aSample->mTime, aSample->mDuration);
 #endif
 
   if (!aSample) {
     NS_WARNING("MP4Reader::Output() passed a null sample");
     Error(aTrack);
     return;
   }
 
-  DecoderData& data = GetDecoderData(aTrack);
+  auto& decoder = GetDecoderData(aTrack);
   // Don't accept output while we're flushing.
-  MonitorAutoLock mon(data.mMonitor);
-  if (data.mIsFlushing) {
+  MonitorAutoLock mon(decoder.mMonitor);
+  if (decoder.mIsFlushing) {
     LOG("MP4Reader produced output while flushing, discarding.");
     mon.NotifyAll();
     return;
   }
 
-  switch (aTrack) {
-    case kAudio: {
-      MOZ_ASSERT(aSample->mType == MediaData::AUDIO_DATA);
-      AudioData* audioData = static_cast<AudioData*>(aSample);
-      AudioQueue().Push(audioData);
-      if (audioData->mChannels != mInfo.mAudio.mChannels ||
-          audioData->mRate != mInfo.mAudio.mRate) {
-        LOG("MP4Reader::Output change of sampling rate:%d->%d",
-            mInfo.mAudio.mRate, audioData->mRate);
-        mInfo.mAudio.mRate = audioData->mRate;
-        mInfo.mAudio.mChannels = audioData->mChannels;
-      }
-      break;
-    }
-    case kVideo: {
-      MOZ_ASSERT(aSample->mType == MediaData::VIDEO_DATA);
-      VideoQueue().Push(static_cast<VideoData*>(aSample));
-      break;
-    }
-    default:
-      break;
+  decoder.mOutput.AppendElement(aSample);
+  decoder.mNumSamplesOutput++;
+  if (NeedInput(decoder) || decoder.mOutputRequested) {
+    ScheduleUpdate(aTrack);
   }
-
-  data.mNumSamplesOutput++;
-  mon.NotifyAll();
 }
 
 void
 MP4Reader::DrainComplete(TrackType aTrack)
 {
   DecoderData& data = GetDecoderData(aTrack);
   MonitorAutoLock mon(data.mMonitor);
   data.mDrainComplete = true;
@@ -666,38 +736,36 @@ MP4Reader::DrainComplete(TrackType aTrac
 }
 
 void
 MP4Reader::InputExhausted(TrackType aTrack)
 {
   DecoderData& data = GetDecoderData(aTrack);
   MonitorAutoLock mon(data.mMonitor);
   data.mInputExhausted = true;
-  mon.NotifyAll();
+  ScheduleUpdate(aTrack);
 }
 
 void
 MP4Reader::Error(TrackType aTrack)
 {
   DecoderData& data = GetDecoderData(aTrack);
-  MonitorAutoLock mon(data.mMonitor);
-  data.mError = true;
-  mon.NotifyAll();
-}
-
-bool
-MP4Reader::DecodeAudioData()
-{
-  MOZ_ASSERT(HasAudio() && mPlatform && mAudio.mDecoder);
-  return Decode(kAudio);
+  {
+    MonitorAutoLock mon(data.mMonitor);
+    data.mError = true;
+  }
+  GetCallback()->OnNotDecoded(aTrack == kVideo ? MediaData::VIDEO_DATA : MediaData::AUDIO_DATA,
+                              RequestSampleCallback::DECODE_ERROR);
 }
 
 void
 MP4Reader::Flush(TrackType aTrack)
 {
+  MOZ_ASSERT(GetTaskQueue()->IsCurrentThreadIn());
+  VLOG("Flush(%s) BEGIN", TrackTypeToStr(aTrack));
   DecoderData& data = GetDecoderData(aTrack);
   if (!data.mDecoder) {
     return;
   }
   // Purge the current decoder's state.
   // Set a flag so that we ignore all output while we call
   // MediaDataDecoder::Flush().
   {
@@ -705,100 +773,91 @@ MP4Reader::Flush(TrackType aTrack)
     data.mIsFlushing = true;
     data.mDrainComplete = false;
     data.mEOS = false;
   }
   data.mDecoder->Flush();
   {
     MonitorAutoLock mon(data.mMonitor);
     data.mIsFlushing = false;
+    data.mOutput.Clear();
+    data.mNumSamplesInput = 0;
+    data.mNumSamplesOutput = 0;
+    data.mInputExhausted = false;
+    if (data.mOutputRequested) {
+      GetCallback()->OnNotDecoded(aTrack == kVideo ? MediaData::VIDEO_DATA : MediaData::AUDIO_DATA,
+                                  RequestSampleCallback::CANCELED);
+    }
+    data.mOutputRequested = false;
+    data.mDiscontinuity = true;
   }
+  if (aTrack == kVideo) {
+    mQueuedVideoSample = nullptr;
+  }
+  VLOG("Flush(%s) END", TrackTypeToStr(aTrack));
 }
 
 bool
 MP4Reader::SkipVideoDemuxToNextKeyFrame(int64_t aTimeThreshold, uint32_t& parsed)
 {
+  MOZ_ASSERT(GetTaskQueue()->IsCurrentThreadIn());
+
   MOZ_ASSERT(mVideo.mDecoder);
 
   Flush(kVideo);
 
   // Loop until we reach the next keyframe after the threshold.
   while (true) {
     nsAutoPtr<MP4Sample> compressed(PopSample(kVideo));
     if (!compressed) {
       // EOS, or error. Let the state machine know.
+      GetCallback()->OnNotDecoded(MediaData::VIDEO_DATA,
+                                  RequestSampleCallback::END_OF_STREAM);
+      {
+        MonitorAutoLock mon(mVideo.mMonitor);
+        mVideo.mEOS = true;
+      }
       return false;
     }
     parsed++;
     if (!compressed->is_sync_point ||
         compressed->composition_timestamp < aTimeThreshold) {
       continue;
     }
     mQueuedVideoSample = compressed;
     break;
   }
 
   return true;
 }
 
-bool
-MP4Reader::DecodeVideoFrame(bool &aKeyframeSkip,
-                            int64_t aTimeThreshold)
-{
-  // Record number of frames decoded and parsed. Automatically update the
-  // stats counters using the AutoNotifyDecoded stack-based class.
-  uint32_t parsed = 0, decoded = 0;
-  AbstractMediaDecoder::AutoNotifyDecoded autoNotify(mDecoder, parsed, decoded);
-
-  MOZ_ASSERT(HasVideo() && mPlatform && mVideo.mDecoder);
-
-  if (aKeyframeSkip) {
-    bool ok = SkipVideoDemuxToNextKeyFrame(aTimeThreshold, parsed);
-    if (!ok) {
-      NS_WARNING("Failed to skip demux up to next keyframe");
-      return false;
-    }
-    aKeyframeSkip = false;
-    nsresult rv = mVideo.mDecoder->Flush();
-    NS_ENSURE_SUCCESS(rv, false);
-  }
-
-  bool rv = Decode(kVideo);
-  {
-    // Report the number of "decoded" frames as the difference in the
-    // mNumSamplesOutput field since the last time we were called.
-    MonitorAutoLock mon(mVideo.mMonitor);
-    uint64_t delta = mVideo.mNumSamplesOutput - mLastReportedNumDecodedFrames;
-    decoded = static_cast<uint32_t>(delta);
-    mLastReportedNumDecodedFrames = mVideo.mNumSamplesOutput;
-  }
-  return rv;
-}
-
 void
 MP4Reader::Seek(int64_t aTime,
                 int64_t aStartTime,
                 int64_t aEndTime,
                 int64_t aCurrentTime)
 {
+  LOG("MP4Reader::Seek(%lld)", aTime);
+  MOZ_ASSERT(GetTaskQueue()->IsCurrentThreadIn());
   if (!mDecoder->GetResource()->IsTransportSeekable() || !mDemuxer->CanSeek()) {
+    VLOG("Seek() END (Unseekable)");
     GetCallback()->OnSeekCompleted(NS_ERROR_FAILURE);
     return;
   }
 
   mQueuedVideoSample = nullptr;
   if (mDemuxer->HasValidVideo()) {
     mDemuxer->SeekVideo(aTime);
     mQueuedVideoSample = PopSample(kVideo);
   }
   if (mDemuxer->HasValidAudio()) {
     mDemuxer->SeekAudio(
       mQueuedVideoSample ? mQueuedVideoSample->composition_timestamp : aTime);
   }
-
+  LOG("MP4Reader::Seek(%lld) exit", aTime);
   GetCallback()->OnSeekCompleted(NS_OK);
 }
 
 void
 MP4Reader::NotifyDataArrived(const char* aBuffer, uint32_t aLength,
                              int64_t aOffset)
 {
   UpdateIndex();
--- a/dom/media/fmp4/MP4Reader.h
+++ b/dom/media/fmp4/MP4Reader.h
@@ -23,27 +23,30 @@ class TimeRanges;
 }
 
 typedef std::deque<mp4_demuxer::MP4Sample*> MP4SampleQueue;
 
 class MP4Stream;
 
 class MP4Reader : public MediaDecoderReader
 {
+  typedef mp4_demuxer::TrackType TrackType;
+
 public:
   explicit MP4Reader(AbstractMediaDecoder* aDecoder);
 
   virtual ~MP4Reader();
 
   virtual nsresult Init(MediaDecoderReader* aCloneDonor) MOZ_OVERRIDE;
 
-  virtual bool DecodeAudioData() MOZ_OVERRIDE;
-  virtual bool DecodeVideoFrame(bool &aKeyframeSkip,
+  virtual void RequestVideoData(bool aSkipToNextKeyframe,
                                 int64_t aTimeThreshold) MOZ_OVERRIDE;
 
+  virtual void RequestAudioData() MOZ_OVERRIDE;
+
   virtual bool HasAudio() MOZ_OVERRIDE;
   virtual bool HasVideo() MOZ_OVERRIDE;
 
   virtual nsresult ReadMetadata(MediaInfo* aInfo,
                                 MetadataTags** aTags) MOZ_OVERRIDE;
 
   virtual void ReadUpdatedMetadata(MediaInfo* aInfo) MOZ_OVERRIDE;
 
@@ -70,31 +73,43 @@ public:
     MOZ_OVERRIDE;
 
   virtual nsresult ResetDecode() MOZ_OVERRIDE;
 
   virtual void Shutdown() MOZ_OVERRIDE;
 
 private:
 
+  void ReturnEOS(TrackType aTrack);
+  void ReturnOutput(MediaData* aData, TrackType aTrack);
+
+  // Sends input to decoder for aTrack, and output to the state machine,
+  // if necessary.
+  void Update(TrackType aTrack);
+
+  // Enqueues a task to call Update(aTrack) on the decoder task queue.
+  // Lock for corresponding track must be held.
+  void ScheduleUpdate(TrackType aTrack);
+
   void ExtractCryptoInitData(nsTArray<uint8_t>& aInitData);
 
   // Initializes mLayersBackendType if possible.
   void InitLayersBackendType();
 
   // Blocks until the demuxer produces an sample of specified type.
   // Returns nullptr on error on EOS. Caller must delete sample.
   mp4_demuxer::MP4Sample* PopSample(mp4_demuxer::TrackType aTrack);
 
   bool SkipVideoDemuxToNextKeyFrame(int64_t aTimeThreshold, uint32_t& parsed);
 
+  // DecoderCallback proxies the MediaDataDecoderCallback calls to these
+  // functions.
   void Output(mp4_demuxer::TrackType aType, MediaData* aSample);
   void InputExhausted(mp4_demuxer::TrackType aTrack);
   void Error(mp4_demuxer::TrackType aTrack);
-  bool Decode(mp4_demuxer::TrackType aTrack);
   void Flush(mp4_demuxer::TrackType aTrack);
   void DrainComplete(mp4_demuxer::TrackType aTrack);
   void UpdateIndex();
   bool IsSupportedAudioMimeType(const char* aMimeType);
   void NotifyResourcesStatusChanged();
   bool IsWaitingOnCodecResource();
   virtual bool IsWaitingOnCDMResource() MOZ_OVERRIDE;
 
@@ -139,47 +154,61 @@ private:
       , mNumSamplesInput(0)
       , mNumSamplesOutput(0)
       , mDecodeAhead(aDecodeAhead)
       , mActive(false)
       , mInputExhausted(false)
       , mError(false)
       , mIsFlushing(false)
       , mDrainComplete(false)
+      , mOutputRequested(false)
+      , mUpdateScheduled(false)
       , mEOS(false)
+      , mDiscontinuity(false)
     {
     }
 
     // The platform decoder.
     nsRefPtr<MediaDataDecoder> mDecoder;
     // TaskQueue on which decoder can choose to decode.
     // Only non-null up until the decoder is created.
     nsRefPtr<MediaTaskQueue> mTaskQueue;
     // Callback that receives output and error notifications from the decoder.
     nsAutoPtr<DecoderCallback> mCallback;
+    // Decoded samples returned my mDecoder awaiting being returned to
+    // state machine upon request.
+    nsTArray<nsRefPtr<MediaData> > mOutput;
+
     // Monitor that protects all non-threadsafe state; the primitives
     // that follow.
     Monitor mMonitor;
     uint64_t mNumSamplesInput;
     uint64_t mNumSamplesOutput;
     uint32_t mDecodeAhead;
     // Whether this stream exists in the media.
     bool mActive;
     bool mInputExhausted;
     bool mError;
     bool mIsFlushing;
     bool mDrainComplete;
+    bool mOutputRequested;
+    bool mUpdateScheduled;
     bool mEOS;
+    bool mDiscontinuity;
   };
   DecoderData mAudio;
   DecoderData mVideo;
   // Queued samples extracted by the demuxer, but not yet sent to the platform
   // decoder.
   nsAutoPtr<mp4_demuxer::MP4Sample> mQueuedVideoSample;
 
+  // Returns true when the decoder for this track needs input.
+  // aDecoder.mMonitor must be locked.
+  bool NeedInput(DecoderData& aDecoder);
+
   // The last number of decoded output frames that we've reported to
   // MediaDecoder::NotifyDecoded(). We diff the number of output video
   // frames every time that DecodeVideoData() is called, and report the
   // delta there.
   uint64_t mLastReportedNumDecodedFrames;
 
   DecoderData& GetDecoderData(mp4_demuxer::TrackType aTrack);
 
--- a/dom/media/fmp4/PlatformDecoderModule.h
+++ b/dom/media/fmp4/PlatformDecoderModule.h
@@ -159,18 +159,19 @@ public:
   virtual void ReleaseMediaResources() {};
 };
 
 // MediaDataDecoder is the interface exposed by decoders created by the
 // PlatformDecoderModule's Create*Decoder() functions. The type of
 // media data that the decoder accepts as valid input and produces as
 // output is determined when the MediaDataDecoder is created.
 //
-// All functions must be threadsafe, and be able to be called on an
-// arbitrary thread.
+// All functions are only called on the decode task queue. Don't block
+// inside these functions, unless it's explicitly noted that you should
+// (like in Flush() and Drain()).
 //
 // Decoding is done asynchronously. Any async work can be done on the
 // MediaTaskQueue passed into the PlatformDecoderModules's Create*Decoder()
 // function. This may not be necessary for platforms with async APIs
 // for decoding.
 class MediaDataDecoder {
 protected:
   virtual ~MediaDataDecoder() {};
--- a/dom/media/gtest/TestMP4Reader.cpp
+++ b/dom/media/gtest/TestMP4Reader.cpp
@@ -31,16 +31,18 @@ public:
   {
     EXPECT_EQ(NS_OK, Preferences::SetBool(
                        "media.fragmented-mp4.use-blank-decoder", true));
 
     EXPECT_EQ(NS_OK, resource->Open(nullptr));
     decoder->SetResource(resource);
 
     reader->Init(nullptr);
+    reader->SetTaskQueue(
+      new MediaTaskQueue(SharedThreadPool::Get(NS_LITERAL_CSTRING("TestMP4Reader"))));
     {
       // This needs to be done before invoking GetBuffered. This is normally
       // done by MediaDecoderStateMachine.
       ReentrantMonitorAutoEnter mon(decoder->GetReentrantMonitor());
       reader->SetStartTime(0);
     }
   }
 
@@ -50,16 +52,20 @@ public:
                                NS_NewRunnableMethod(this, &TestBinding::ReadMetadata));
     EXPECT_EQ(NS_OK, rv);
     thread->Shutdown();
   }
 
 private:
   virtual ~TestBinding()
   {
+    reader->GetTaskQueue()->Dispatch(NS_NewRunnableMethod(reader,
+                                                          &MP4Reader::Shutdown));
+    reader->GetTaskQueue()->Shutdown();
+
     decoder = nullptr;
     resource = nullptr;
     reader = nullptr;
     SharedThreadPool::SpinUntilShutdown();
   }
 
   void ReadMetadata()
   {