Bug 779715. Part 6: Rework capturing MediaStreams from media elements to use TrackUnionStreams. r=cpearce,jesup
authorRobert O'Callahan <robert@ocallahan.org>
Wed, 01 Aug 2012 00:17:22 +1200
changeset 105203 5536f349f504249e5935051c0ef187a7d47aea20
parent 105202 89f96727117692db346acc8faf498f749ec6b4d9
child 105204 98de5d3c227b43feae7dd4d1e8cf72b847fd318a
push id55
push usershu@rfrn.org
push dateThu, 30 Aug 2012 01:33:09 +0000
reviewerscpearce, jesup
bugs779715
milestone17.0a1
Bug 779715. Part 6: Rework capturing MediaStreams from media elements to use TrackUnionStreams. r=cpearce,jesup Moves to a new setup where a decoder manages a single SourceMediaStream internally. Each stream returned from mozCaptureStream(UntilEnded) is a TrackUnionStream which is fed by the decoder's SourceMediaStream. We want the captured streams to be blocked while the media element is not playing. We do that by blocking any captured stream that has no SourceMediaStream feeding into it, and blocking any SourceMediaStream while its decoder is not playing. We arrange for the decoders's PlaybackEnded to be delayed until its SourceMediaStream has finished according to the media stream graph. This ensures the state of captured media streams corresponds more closely to the media element state.
content/html/content/src/nsHTMLMediaElement.cpp
content/media/nsBuiltinDecoder.cpp
content/media/nsBuiltinDecoder.h
content/media/nsBuiltinDecoderStateMachine.cpp
content/media/nsBuiltinDecoderStateMachine.h
content/media/nsDOMMediaStream.cpp
content/media/nsDOMMediaStream.h
content/media/nsMediaDecoder.h
--- a/content/html/content/src/nsHTMLMediaElement.cpp
+++ b/content/html/content/src/nsHTMLMediaElement.cpp
@@ -550,22 +550,16 @@ nsHTMLMediaElement::OnChannelRedirect(ns
 }
 
 void nsHTMLMediaElement::ShutdownDecoder()
 {
   RemoveMediaElementFromURITable();
   NS_ASSERTION(mDecoder, "Must have decoder to shut down");
   mDecoder->Shutdown();
   mDecoder = nullptr;
-  // Discard all output streams. mDecoder->Shutdown() will have finished all
-  // its output streams.
-  // XXX For now we ignore mFinishWhenEnded. We'll fix this later. The
-  // immediate goal is to not crash when reloading a media element with
-  // output streams.
-  mOutputStreams.Clear();
 }
 
 void nsHTMLMediaElement::AbortExistingLoads()
 {
   // Abort any already-running instance of the resource selection algorithm.
   mLoadWaitStatus = NOT_WAITING;
 
   // Set a new load ID. This will cause events which were enqueued
@@ -1519,26 +1513,30 @@ NS_IMETHODIMP nsHTMLMediaElement::SetMut
 
   return NS_OK;
 }
 
 already_AddRefed<nsDOMMediaStream>
 nsHTMLMediaElement::CaptureStreamInternal(bool aFinishWhenEnded)
 {
   OutputMediaStream* out = mOutputStreams.AppendElement();
-  out->mStream = nsDOMMediaStream::CreateInputStream();
+  out->mStream = nsDOMMediaStream::CreateTrackUnionStream();
   nsRefPtr<nsIPrincipal> principal = GetCurrentPrincipal();
   out->mStream->CombineWithPrincipal(principal);
   out->mFinishWhenEnded = aFinishWhenEnded;
 
   mAudioCaptured = true;
+  // Block the output stream initially.
+  // Decoders are responsible for removing the block while they are playing
+  // back into the output stream.
+  out->mStream->GetStream()->ChangeExplicitBlockerCount(1);
   if (mDecoder) {
     mDecoder->SetAudioCaptured(true);
     mDecoder->AddOutputStream(
-        out->mStream->GetStream()->AsSourceStream(), aFinishWhenEnded);
+        out->mStream->GetStream()->AsProcessedStream(), aFinishWhenEnded);
   }
   nsRefPtr<nsDOMMediaStream> result = out->mStream;
   return result.forget();
 }
 
 NS_IMETHODIMP nsHTMLMediaElement::MozCaptureStream(nsIDOMMediaStream** aStream)
 {
   *aStream = CaptureStreamInternal(false).get();
@@ -2471,17 +2469,17 @@ nsresult nsHTMLMediaElement::FinishDecod
 
   // The new stream has not been suspended by us.
   mPausedForInactiveDocument = false;
 
   aDecoder->SetAudioCaptured(mAudioCaptured);
   aDecoder->SetVolume(mMuted ? 0.0 : mVolume);
   for (uint32_t i = 0; i < mOutputStreams.Length(); ++i) {
     OutputMediaStream* ms = &mOutputStreams[i];
-    aDecoder->AddOutputStream(ms->mStream->GetStream()->AsSourceStream(),
+    aDecoder->AddOutputStream(ms->mStream->GetStream()->AsProcessedStream(),
         ms->mFinishWhenEnded);
   }
 
   nsresult rv = aDecoder->Load(aStream, aListener, aCloneDonor);
   if (NS_FAILED(rv)) {
     LOG(PR_LOG_DEBUG, ("%p Failed to load for decoder %p", this, aDecoder));
     return rv;
   }
@@ -2823,16 +2821,24 @@ void nsHTMLMediaElement::Error(uint16_t 
 
 void nsHTMLMediaElement::PlaybackEnded()
 {
   // We changed state which can affect AddRemoveSelfReference
   AddRemoveSelfReference();
 
   NS_ASSERTION(!mDecoder || mDecoder->IsEnded(),
                "Decoder fired ended, but not in ended state");
+
+  // Discard all output streams that have finished now.
+  for (int32_t i = mOutputStreams.Length() - 1; i >= 0; --i) {
+    if (mOutputStreams[i].mFinishWhenEnded) {
+      mOutputStreams.RemoveElementAt(i);
+    }
+  }
+
   if (mSrcStream || (mDecoder && mDecoder->IsInfinite())) {
     LOG(PR_LOG_DEBUG, ("%p, got duration by reaching the end of the resource", this));
     DispatchAsyncEvent(NS_LITERAL_STRING("durationchange"));
   }
 
   if (HasAttr(kNameSpaceID_None, nsGkAtoms::loop)) {
     SetCurrentTime(0);
     return;
--- a/content/media/nsBuiltinDecoder.cpp
+++ b/content/media/nsBuiltinDecoder.cpp
@@ -54,24 +54,136 @@ void nsBuiltinDecoder::SetAudioCaptured(
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
   mInitialAudioCaptured = aCaptured;
   if (mDecoderStateMachine) {
     mDecoderStateMachine->SetAudioCaptured(aCaptured);
   }
 }
 
-void nsBuiltinDecoder::AddOutputStream(SourceMediaStream* aStream, bool aFinishWhenEnded)
+void nsBuiltinDecoder::ConnectDecodedStreamToOutputStream(OutputStreamData* aStream)
+{
+  NS_ASSERTION(!aStream->mPort, "Already connected?");
+
+  // The output stream must stay in sync with the decoded stream, so if
+  // either stream is blocked, we block the other.
+  aStream->mPort = aStream->mStream->AllocateInputPort(mDecodedStream->mStream,
+      MediaInputPort::FLAG_BLOCK_INPUT | MediaInputPort::FLAG_BLOCK_OUTPUT);
+  // Unblock the output stream now. While it's connected to mDecodedStream,
+  // mDecodedStream is responsible for controlling blocking.
+  aStream->mStream->ChangeExplicitBlockerCount(-1);
+}
+
+nsBuiltinDecoder::DecodedStreamData::DecodedStreamData(nsBuiltinDecoder* aDecoder,
+                                                       int64_t aInitialTime,
+                                                       SourceMediaStream* aStream)
+  : mLastAudioPacketTime(-1),
+    mLastAudioPacketEndTime(-1),
+    mAudioFramesWritten(0),
+    mInitialTime(aInitialTime),
+    mNextVideoTime(aInitialTime),
+    mStreamInitialized(false),
+    mHaveSentFinish(false),
+    mHaveSentFinishAudio(false),
+    mHaveSentFinishVideo(false),
+    mStream(aStream),
+    mMainThreadListener(new DecodedStreamMainThreadListener(aDecoder)),
+    mHaveBlockedForPlayState(false)
+{
+  mStream->AddMainThreadListener(mMainThreadListener);
+}
+
+nsBuiltinDecoder::DecodedStreamData::~DecodedStreamData()
+{
+  mStream->RemoveMainThreadListener(mMainThreadListener);
+  mStream->Destroy();
+}
+
+void nsBuiltinDecoder::DestroyDecodedStream()
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+  mReentrantMonitor.AssertCurrentThreadIn();
+
+  // All streams are having their SourceMediaStream disconnected, so they
+  // need to be explicitly blocked again.
+  for (uint32_t i = 0; i < mOutputStreams.Length(); ++i) {
+    OutputStreamData& os = mOutputStreams[i];
+    // During cycle collection, nsDOMMediaStream can be destroyed and send
+    // its Destroy message before this decoder is destroyed. So we have to
+    // be careful not to send any messages after the Destroy().
+    if (!os.mStream->IsDestroyed()) {
+      os.mStream->ChangeExplicitBlockerCount(1);
+    }
+    // Explicitly remove all existing ports. This is not strictly necessary but it's
+    // good form.
+    os.mPort->Destroy();
+    os.mPort = nullptr;
+  }
+
+  mDecodedStream = nullptr;
+}
+
+void nsBuiltinDecoder::RecreateDecodedStream(int64_t aStartTimeUSecs)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+  mReentrantMonitor.AssertCurrentThreadIn();
+  LOG(PR_LOG_DEBUG, ("nsBuiltinDecoder::RecreateDecodedStream this=%p aStartTimeUSecs=%lld!",
+                     this, (long long)aStartTimeUSecs));
+
+  DestroyDecodedStream();
+
+  mDecodedStream = new DecodedStreamData(this, aStartTimeUSecs,
+    MediaStreamGraph::GetInstance()->CreateInputStream(nullptr));
+
+  // Note that the delay between removing ports in DestroyDecodedStream
+  // and adding new ones won't cause a glitch since all graph operations
+  // between main-thread stable states take effect atomically.
+  for (uint32_t i = 0; i < mOutputStreams.Length(); ++i) {
+    ConnectDecodedStreamToOutputStream(&mOutputStreams[i]);
+  }
+
+  mDecodedStream->mHaveBlockedForPlayState = mPlayState != PLAY_STATE_PLAYING;
+  if (mDecodedStream->mHaveBlockedForPlayState) {
+    mDecodedStream->mStream->ChangeExplicitBlockerCount(1);
+  }
+}
+
+void nsBuiltinDecoder::NotifyDecodedStreamMainThreadStateChanged()
+{
+  if (mTriggerPlaybackEndedWhenSourceStreamFinishes && mDecodedStream &&
+      mDecodedStream->mStream->IsFinished()) {
+    mTriggerPlaybackEndedWhenSourceStreamFinishes = false;
+    if (GetState() == PLAY_STATE_PLAYING) {
+      nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethod(this, &nsBuiltinDecoder::PlaybackEnded);
+      NS_DispatchToCurrentThread(event);
+    }
+  }
+}
+
+void nsBuiltinDecoder::AddOutputStream(ProcessedMediaStream* aStream,
+                                       bool aFinishWhenEnded)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+  LOG(PR_LOG_DEBUG, ("nsBuiltinDecoder::AddOutputStream this=%p aStream=%p!",
+                     this, aStream));
 
   {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
-    OutputMediaStream* ms = mOutputStreams.AppendElement();
-    ms->Init(int64_t(mCurrentTime*USECS_PER_S), aStream, aFinishWhenEnded);
+    if (!mDecodedStream) {
+      RecreateDecodedStream(mDecoderStateMachine ?
+          int64_t(mDecoderStateMachine->GetCurrentTime()*USECS_PER_S) : 0);
+    }
+    OutputStreamData* os = mOutputStreams.AppendElement();
+    os->Init(aStream, aFinishWhenEnded);
+    ConnectDecodedStreamToOutputStream(os);
+    if (aFinishWhenEnded) {
+      // Ensure that aStream finishes the moment mDecodedStream does.
+      aStream->SetAutofinish(true);
+    }
   }
 
   // This can be called before Load(), in which case our mDecoderStateMachine
   // won't have been created yet and we can rely on Load() to schedule it
   // once it is created.
   if (mDecoderStateMachine) {
     // Make sure the state machine thread runs so that any buffered data
     // is fed into our stream.
@@ -111,17 +223,18 @@ nsBuiltinDecoder::nsBuiltinDecoder() :
   mRequestedSeekTime(-1.0),
   mDuration(-1),
   mSeekable(true),
   mReentrantMonitor("media.decoder"),
   mPlayState(PLAY_STATE_PAUSED),
   mNextState(PLAY_STATE_PAUSED),
   mResourceLoaded(false),
   mIgnoreProgressData(false),
-  mInfiniteStream(false)
+  mInfiniteStream(false),
+  mTriggerPlaybackEndedWhenSourceStreamFinishes(false)
 {
   MOZ_COUNT_CTOR(nsBuiltinDecoder);
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
 #ifdef PR_LOGGING
   if (!gBuiltinDecoderLog) {
     gBuiltinDecoderLog = PR_NewLogModule("nsBuiltinDecoder");
   }
 #endif
@@ -141,16 +254,21 @@ void nsBuiltinDecoder::Shutdown()
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
   
   if (mShuttingDown)
     return;
 
   mShuttingDown = true;
 
+  {
+    ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
+    DestroyDecodedStream();
+  }
+
   // This changes the decoder state to SHUTDOWN and does other things
   // necessary to unblock the state machine thread if it's blocked, so
   // the asynchronous shutdown in nsDestroyStateMachine won't deadlock.
   if (mDecoderStateMachine) {
     mDecoderStateMachine->Shutdown();
   }
 
   // Force any outstanding seek and byterange requests to complete
@@ -538,20 +656,46 @@ bool nsBuiltinDecoder::IsSeeking() const
 bool nsBuiltinDecoder::IsEnded() const
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
   return mPlayState == PLAY_STATE_ENDED || mPlayState == PLAY_STATE_SHUTDOWN;
 }
 
 void nsBuiltinDecoder::PlaybackEnded()
 {
+  NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+
   if (mShuttingDown || mPlayState == nsBuiltinDecoder::PLAY_STATE_SEEKING)
     return;
 
-  printf("nsBuiltinDecoder::PlaybackEnded mPlayState=%d\n", mPlayState);
+  {
+    ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+
+    if (mDecodedStream && !mDecodedStream->mStream->IsFinished()) {
+      // Wait for it to finish before firing PlaybackEnded()
+      mTriggerPlaybackEndedWhenSourceStreamFinishes = true;
+      return;
+    }
+
+    for (int32_t i = mOutputStreams.Length() - 1; i >= 0; --i) {
+      OutputStreamData& os = mOutputStreams[i];
+      if (os.mFinishWhenEnded) {
+        // Shouldn't really be needed since mDecodedStream should already have
+        // finished, but doesn't hurt.
+        os.mStream->Finish();
+        os.mPort->Destroy();
+        os.mPort = nullptr;
+        // Not really needed but it keeps the invariant that a stream not
+        // connected to mDecodedStream is explicity blocked.
+        os.mStream->ChangeExplicitBlockerCount(1);
+        mOutputStreams.RemoveElementAt(i);
+      }
+    }
+  }
+
   PlaybackPositionChanged();
   ChangeState(PLAY_STATE_ENDED);
 
   if (mElement)  {
     UpdateReadyStateForData();
     mElement->PlaybackEnded();
   }
 
@@ -760,17 +904,16 @@ void nsBuiltinDecoder::SeekingStopped()
 
     // An additional seek was requested while the current seek was
     // in operation.
     if (mRequestedSeekTime >= 0.0) {
       ChangeState(PLAY_STATE_SEEKING);
       seekWasAborted = true;
     } else {
       UnpinForSeek();
-      printf("nsBuiltinDecoder::SeekingStopped, next state=%d\n", mNextState);
       ChangeState(mNextState);
     }
   }
 
   if (mElement) {
     UpdateReadyStateForData();
     if (!seekWasAborted) {
       mElement->SeekCompleted();
@@ -837,16 +980,23 @@ void nsBuiltinDecoder::ChangeState(PlayS
     mNextState = PLAY_STATE_PAUSED;
   }
 
   if (mPlayState == PLAY_STATE_SHUTDOWN) {
     mReentrantMonitor.NotifyAll();
     return;
   }
 
+  if (mDecodedStream) {
+    bool blockForPlayState = aState != PLAY_STATE_PLAYING;
+    if (mDecodedStream->mHaveBlockedForPlayState != blockForPlayState) {
+      mDecodedStream->mStream->ChangeExplicitBlockerCount(blockForPlayState ? 1 : -1);
+      mDecodedStream->mHaveBlockedForPlayState = blockForPlayState;
+    }
+  }
   mPlayState = aState;
   if (mDecoderStateMachine) {
     switch (aState) {
     case PLAY_STATE_PLAYING:
       mDecoderStateMachine->Play();
       break;
     case PLAY_STATE_SEEKING:
       mDecoderStateMachine->Seek(mRequestedSeekTime);
@@ -1043,51 +1193,8 @@ bool nsBuiltinDecoder::OnStateMachineThr
 void nsBuiltinDecoder::NotifyAudioAvailableListener()
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
   if (mDecoderStateMachine) {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
     mDecoderStateMachine->NotifyAudioAvailableListener();
   }
 }
-
-nsBuiltinDecoder::OutputMediaStream::OutputMediaStream()
-{
-
-}
-
-nsBuiltinDecoder::OutputMediaStream::~OutputMediaStream()
-{
-
-}
-    
-
-void nsBuiltinDecoder::OutputMediaStream::Init(int64_t aInitialTime, SourceMediaStream* aStream, bool aFinishWhenEnded)
-{
-  mLastAudioPacketTime = -1;
-  mLastAudioPacketEndTime = -1;
-  mAudioFramesWrittenBaseTime = aInitialTime;
-  mAudioFramesWritten = 0;
-  mNextVideoTime = aInitialTime;
-  mStream = aStream;
-  mStreamInitialized = false;
-  mFinishWhenEnded = aFinishWhenEnded;
-  mHaveSentFinish = false;
-  mHaveSentFinishAudio = false;
-  mHaveSentFinishVideo = false;
-}
-
-nsBuiltinDecoder::OutputMediaStream::OutputMediaStream(const OutputMediaStream& rhs)
-{
-  mLastAudioPacketTime = rhs.mLastAudioPacketTime;
-  mLastAudioPacketEndTime = rhs.mLastAudioPacketEndTime;
-  mAudioFramesWritten = rhs.mAudioFramesWritten;
-  mAudioFramesWrittenBaseTime = rhs.mAudioFramesWrittenBaseTime;
-  mNextVideoTime = rhs.mNextVideoTime;
-  mLastVideoImage = rhs.mLastVideoImage;
-  mStream = rhs.mStream;
-  mLastVideoImageDisplaySize = rhs.mLastVideoImageDisplaySize;
-  mStreamInitialized = rhs.mStreamInitialized;
-  mFinishWhenEnded = rhs.mFinishWhenEnded;
-  mHaveSentFinish = rhs.mHaveSentFinish;
-  mHaveSentFinishAudio = rhs.mHaveSentFinishAudio;
-  mHaveSentFinishVideo = rhs.mHaveSentFinishVideo;
-}
--- a/content/media/nsBuiltinDecoder.h
+++ b/content/media/nsBuiltinDecoder.h
@@ -327,16 +327,17 @@ public:
   // element. Called on the main thread.
   virtual void NotifyAudioAvailableListener() = 0;
 };
 
 class nsBuiltinDecoder : public nsMediaDecoder
 {
 public:
   typedef mozilla::MediaChannelStatistics MediaChannelStatistics;
+  class DecodedStreamMainThreadListener;
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSIOBSERVER
 
   // Enumeration for the valid play states (see mPlayState)
   enum PlayState {
     PLAY_STATE_START,
     PLAY_STATE_LOADING,
@@ -372,53 +373,116 @@ public:
   virtual nsresult Seek(double aTime);
 
   virtual nsresult PlaybackRateChanged();
 
   virtual void Pause();
   virtual void SetVolume(double aVolume);
   virtual void SetAudioCaptured(bool aCaptured);
 
-  virtual void AddOutputStream(SourceMediaStream* aStream, bool aFinishWhenEnded);
-  // Protected by mReentrantMonitor. All decoder output is copied to these streams.
-  struct OutputMediaStream {
-    OutputMediaStream();
-    ~OutputMediaStream();
-    OutputMediaStream(const OutputMediaStream& rhs);
+  // All MediaStream-related data is protected by mReentrantMonitor.
+  // We have at most one DecodedStreamData per nsBuiltinDecoder. Its stream
+  // is used as the input for each ProcessedMediaStream created by calls to
+  // captureStream(UntilEnded). Seeking creates a new source stream, as does
+  // replaying after the input as ended. In the latter case, the new source is
+  // not connected to streams created by captureStreamUntilEnded.
 
-    void Init(int64_t aInitialTime, SourceMediaStream* aStream, bool aFinishWhenEnded);
-    
+  struct DecodedStreamData {
+    DecodedStreamData(nsBuiltinDecoder* aDecoder,
+                      int64_t aInitialTime, SourceMediaStream* aStream);
+    ~DecodedStreamData();
+
+    // The following group of fields are protected by the decoder's monitor
+    // and can be read or written on any thread.    
     int64_t mLastAudioPacketTime; // microseconds
     int64_t mLastAudioPacketEndTime; // microseconds
     // Count of audio frames written to the stream
     int64_t mAudioFramesWritten;
-    // Timestamp of the first audio packet whose frames we wrote.
-    int64_t mAudioFramesWrittenBaseTime; // microseconds
+    // Saved value of aInitialTime. Timestamp of the first audio and/or
+    // video packet written.
+    int64_t mInitialTime; // microseconds
     // mNextVideoTime is the end timestamp for the last packet sent to the stream.
     // Therefore video packets starting at or after this time need to be copied
     // to the output stream.
     int64_t mNextVideoTime; // microseconds
     // The last video image sent to the stream. Useful if we need to replicate
     // the image.
     nsRefPtr<Image> mLastVideoImage;
-    nsRefPtr<SourceMediaStream> mStream;
     gfxIntSize mLastVideoImageDisplaySize;
     // This is set to true when the stream is initialized (audio and
     // video tracks added).
     bool mStreamInitialized;
-    bool mFinishWhenEnded;
     bool mHaveSentFinish;
     bool mHaveSentFinishAudio;
     bool mHaveSentFinishVideo;
+
+    // The decoder is responsible for calling Destroy() on this stream.
+    // Can be read from any thread.
+    const nsRefPtr<SourceMediaStream> mStream;
+    // A listener object that receives notifications when mStream's
+    // main-thread-visible state changes. Used on the main thread only.
+    const nsRefPtr<DecodedStreamMainThreadListener> mMainThreadListener;
+    // True when we've explicitly blocked this stream because we're
+    // not in PLAY_STATE_PLAYING. Used on the main thread only.
+    bool mHaveBlockedForPlayState;
   };
-  nsTArray<OutputMediaStream>& OutputStreams()
+  struct OutputStreamData {
+    void Init(ProcessedMediaStream* aStream, bool aFinishWhenEnded)
+    {
+      mStream = aStream;
+      mFinishWhenEnded = aFinishWhenEnded;
+    }
+    nsRefPtr<ProcessedMediaStream> mStream;
+    // mPort connects mDecodedStream->mStream to our mStream.
+    nsRefPtr<MediaInputPort> mPort;
+    bool mFinishWhenEnded;
+  };
+  /**
+   * Connects mDecodedStream->mStream to aStream->mStream.
+   */
+  void ConnectDecodedStreamToOutputStream(OutputStreamData* aStream);
+  /**
+   * Disconnects mDecodedStream->mStream from all outputs and clears
+   * mDecodedStream.
+   */
+  void DestroyDecodedStream();
+  /**
+   * Recreates mDecodedStream. Call this to create mDecodedStream at first,
+   * and when seeking, to ensure a new stream is set up with fresh buffers.
+   * aStartTimeUSecs is relative to the state machine's mStartTime.
+   */
+  void RecreateDecodedStream(int64_t aStartTimeUSecs);
+  /**
+   * Called when the state of mDecodedStream as visible on the main thread
+   * has changed. In particular we want to know when the stream has finished
+   * so we can call PlaybackEnded.
+   */
+  void NotifyDecodedStreamMainThreadStateChanged();
+  nsTArray<OutputStreamData>& OutputStreams()
   {
     GetReentrantMonitor().AssertCurrentThreadIn();
     return mOutputStreams;
   }
+  DecodedStreamData* GetDecodedStream()
+  {
+    GetReentrantMonitor().AssertCurrentThreadIn();
+    return mDecodedStream;
+  }
+  class DecodedStreamMainThreadListener : public MainThreadMediaStreamListener {
+  public:
+    DecodedStreamMainThreadListener(nsBuiltinDecoder* aDecoder)
+      : mDecoder(aDecoder) {}
+    virtual void NotifyMainThreadStateChanged()
+    {
+      mDecoder->NotifyDecodedStreamMainThreadStateChanged();
+    }
+    nsBuiltinDecoder* mDecoder;
+  };
+
+  virtual void AddOutputStream(ProcessedMediaStream* aStream, bool aFinishWhenEnded);
 
   virtual double GetDuration();
 
   virtual void SetInfinite(bool aInfinite);
   virtual bool IsInfinite();
 
   virtual MediaResource* GetResource() { return mResource; }
   virtual already_AddRefed<nsIPrincipal> GetCurrentPrincipal();
@@ -705,17 +769,23 @@ public:
   nsAutoPtr<MediaResource> mResource;
 
   // ReentrantMonitor for detecting when the video play state changes. A call
   // to Wait on this monitor will block the thread until the next
   // state change.
   ReentrantMonitor mReentrantMonitor;
 
   // Data about MediaStreams that are being fed by this decoder.
-  nsTArray<OutputMediaStream> mOutputStreams;
+  nsTArray<OutputStreamData> mOutputStreams;
+  // The SourceMediaStream we are using to feed the mOutputStreams. This stream
+  // is never exposed outside the decoder.
+  // Only written on the main thread while holding the monitor. Therefore it
+  // can be read on any thread while holding the monitor, or on the main thread
+  // without holding the monitor.
+  nsAutoPtr<DecodedStreamData> mDecodedStream;
 
   // Set to one of the valid play states.
   // This can only be changed on the main thread while holding the decoder
   // monitor. Thus, it can be safely read while holding the decoder monitor
   // OR on the main thread.
   // Any change to the state on the main thread must call NotifyAll on the
   // monitor so the decode thread can wake up.
   PlayState mPlayState;
@@ -737,11 +807,15 @@ public:
   // such a manner that progress event data is inaccurate. This is set
   // during seek and duration operations to prevent the progress indicator
   // from jumping around. Read/Write from any thread. Must have decode monitor
   // locked before accessing.
   bool mIgnoreProgressData;
 
   // True if the stream is infinite (e.g. a webradio).
   bool mInfiniteStream;
+
+  // True if NotifyDecodedStreamMainThreadStateChanged should retrigger
+  // PlaybackEnded when mDecodedStream->mStream finishes.
+  bool mTriggerPlaybackEndedWhenSourceStreamFinishes;
 };
 
 #endif
--- a/content/media/nsBuiltinDecoderStateMachine.cpp
+++ b/content/media/nsBuiltinDecoderStateMachine.cpp
@@ -423,16 +423,18 @@ nsBuiltinDecoderStateMachine::nsBuiltinD
   mBufferingWait = mRealTime ? 0 : BUFFERING_WAIT_S;
   mLowDataThresholdUsecs = mRealTime ? 0 : LOW_DATA_THRESHOLD_USECS;
 }
 
 nsBuiltinDecoderStateMachine::~nsBuiltinDecoderStateMachine()
 {
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
   MOZ_COUNT_DTOR(nsBuiltinDecoderStateMachine);
+  NS_ASSERTION(!mPendingWakeDecoder.get(),
+               "WakeDecoder should have been revoked already");
   NS_ASSERTION(!StateMachineTracker::Instance().IsQueued(this),
     "Should not have a pending request for a new decode thread");
   NS_ASSERTION(!mRequestedNewDecodeThread,
     "Should not have (or flagged) a pending request for a new decode thread");
   if (mTimer)
     mTimer->Cancel();
   mTimer = nullptr;
   mReader = nullptr;
@@ -491,36 +493,37 @@ void nsBuiltinDecoderStateMachine::Decod
       DecodeSeek();
     }
   }
 
   mDecodeThreadIdle = true;
   LOG(PR_LOG_DEBUG, ("%p Decode thread finished", mDecoder.get()));
 }
 
-void nsBuiltinDecoderStateMachine::SendOutputStreamAudio(AudioData* aAudio,
-                                                         OutputMediaStream* aStream,
-                                                         AudioSegment* aOutput)
+void nsBuiltinDecoderStateMachine::SendStreamAudio(AudioData* aAudio,
+                                                   DecodedStreamData* aStream,
+                                                   AudioSegment* aOutput)
 {
+  NS_ASSERTION(OnDecodeThread(), "Should be on decode thread.");
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
 
   if (aAudio->mTime <= aStream->mLastAudioPacketTime) {
     // ignore packet that we've already processed
     return;
   }
   aStream->mLastAudioPacketTime = aAudio->mTime;
   aStream->mLastAudioPacketEndTime = aAudio->GetEnd();
 
   NS_ASSERTION(aOutput->GetChannels() == int32_t(aAudio->mChannels),
                "Wrong number of channels");
 
   // This logic has to mimic AudioLoop closely to make sure we write
   // the exact same silences
   CheckedInt64 audioWrittenOffset = UsecsToFrames(mInfo.mAudioRate,
-      aStream->mAudioFramesWrittenBaseTime + mStartTime) + aStream->mAudioFramesWritten;
+      aStream->mInitialTime + mStartTime) + aStream->mAudioFramesWritten;
   CheckedInt64 frameOffset = UsecsToFrames(mInfo.mAudioRate, aAudio->mTime);
   if (!audioWrittenOffset.isValid() || !frameOffset.isValid())
     return;
   if (audioWrittenOffset.value() < frameOffset.value()) {
     // Write silence to catch up
     LOG(PR_LOG_DEBUG, ("%p Decoder writing %d frames of silence to MediaStream",
                        mDecoder.get(), int32_t(frameOffset.value() - audioWrittenOffset.value())));
     AudioSegment silence;
@@ -560,121 +563,122 @@ static void WriteVideoToMediaStream(mozi
   nsRefPtr<mozilla::layers::Image> image = aImage;
   aOutput->AppendFrame(image.forget(), aDuration, aIntrinsicSize);
 }
 
 static const TrackID TRACK_AUDIO = 1;
 static const TrackID TRACK_VIDEO = 2;
 static const TrackRate RATE_VIDEO = USECS_PER_S;
 
-void nsBuiltinDecoderStateMachine::SendOutputStreamData()
+void nsBuiltinDecoderStateMachine::SendStreamData()
 {
+  NS_ASSERTION(OnDecodeThread(), "Should be on decode thread.");
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
 
+  DecodedStreamData* stream = mDecoder->GetDecodedStream();
+  if (!stream)
+    return;
+
   if (mState == DECODER_STATE_DECODING_METADATA)
     return;
 
-  nsTArray<OutputMediaStream>& streams = mDecoder->OutputStreams();
   int64_t minLastAudioPacketTime = PR_INT64_MAX;
+  SourceMediaStream* mediaStream = stream->mStream;
+  StreamTime endPosition = 0;
+
+  if (!stream->mStreamInitialized) {
+    if (mInfo.mHasAudio) {
+      AudioSegment* audio = new AudioSegment();
+      audio->Init(mInfo.mAudioChannels);
+      mediaStream->AddTrack(TRACK_AUDIO, mInfo.mAudioRate, 0, audio);
+    }
+    if (mInfo.mHasVideo) {
+      VideoSegment* video = new VideoSegment();
+      mediaStream->AddTrack(TRACK_VIDEO, RATE_VIDEO, 0, video);
+    }
+    stream->mStreamInitialized = true;
+  }
+
+  if (mInfo.mHasAudio) {
+    nsAutoTArray<AudioData*,10> audio;
+    // It's OK to hold references to the AudioData because while audio
+    // is captured, only the decoder thread pops from the queue (see below).
+    mReader->mAudioQueue.GetElementsAfter(stream->mLastAudioPacketTime, &audio);
+    AudioSegment output;
+    output.Init(mInfo.mAudioChannels);
+    for (uint32_t i = 0; i < audio.Length(); ++i) {
+      SendStreamAudio(audio[i], stream, &output);
+    }
+    if (output.GetDuration() > 0) {
+      mediaStream->AppendToTrack(TRACK_AUDIO, &output);
+    }
+    if (mReader->mAudioQueue.IsFinished() && !stream->mHaveSentFinishAudio) {
+      mediaStream->EndTrack(TRACK_AUDIO);
+      stream->mHaveSentFinishAudio = true;
+    }
+    minLastAudioPacketTime = NS_MIN(minLastAudioPacketTime, stream->mLastAudioPacketTime);
+    endPosition = NS_MAX(endPosition,
+        TicksToTimeRoundDown(mInfo.mAudioRate, stream->mAudioFramesWritten));
+  }
+
+  if (mInfo.mHasVideo) {
+    nsAutoTArray<VideoData*,10> video;
+    // It's OK to hold references to the VideoData only the decoder thread
+    // pops from the queue.
+    mReader->mVideoQueue.GetElementsAfter(stream->mNextVideoTime + mStartTime, &video);
+    VideoSegment output;
+    for (uint32_t i = 0; i < video.Length(); ++i) {
+      VideoData* v = video[i];
+      if (stream->mNextVideoTime + mStartTime < v->mTime) {
+        LOG(PR_LOG_DEBUG, ("%p Decoder writing last video to MediaStream %p for %lld ms",
+                           mDecoder.get(), mediaStream,
+                           v->mTime - (stream->mNextVideoTime + mStartTime)));
+        // Write last video frame to catch up. mLastVideoImage can be null here
+        // which is fine, it just means there's no video.
+        WriteVideoToMediaStream(stream->mLastVideoImage,
+          v->mTime - (stream->mNextVideoTime + mStartTime), stream->mLastVideoImageDisplaySize,
+            &output);
+        stream->mNextVideoTime = v->mTime - mStartTime;
+      }
+      if (stream->mNextVideoTime + mStartTime < v->mEndTime) {
+        LOG(PR_LOG_DEBUG, ("%p Decoder writing video frame %lld to MediaStream %p for %lld ms",
+                           mDecoder.get(), v->mTime, mediaStream,
+                           v->mEndTime - (stream->mNextVideoTime + mStartTime)));
+        WriteVideoToMediaStream(v->mImage,
+            v->mEndTime - (stream->mNextVideoTime + mStartTime), v->mDisplay,
+            &output);
+        stream->mNextVideoTime = v->mEndTime - mStartTime;
+        stream->mLastVideoImage = v->mImage;
+        stream->mLastVideoImageDisplaySize = v->mDisplay;
+      } else {
+        LOG(PR_LOG_DEBUG, ("%p Decoder skipping writing video frame %lld to MediaStream",
+                           mDecoder.get(), v->mTime));
+      }
+    }
+    if (output.GetDuration() > 0) {
+      mediaStream->AppendToTrack(TRACK_VIDEO, &output);
+    }
+    if (mReader->mVideoQueue.IsFinished() && !stream->mHaveSentFinishVideo) {
+      mediaStream->EndTrack(TRACK_VIDEO);
+      stream->mHaveSentFinishVideo = true;
+    }
+    endPosition = NS_MAX(endPosition,
+        TicksToTimeRoundDown(RATE_VIDEO, stream->mNextVideoTime - stream->mInitialTime));
+  }
+
+  if (!stream->mHaveSentFinish) {
+    stream->mStream->AdvanceKnownTracksTime(endPosition);
+  }
 
   bool finished =
       (!mInfo.mHasAudio || mReader->mAudioQueue.IsFinished()) &&
       (!mInfo.mHasVideo || mReader->mVideoQueue.IsFinished());
-
-  for (uint32_t i = 0; i < streams.Length(); ++i) {
-    OutputMediaStream* stream = &streams[i];
-    SourceMediaStream* mediaStream = stream->mStream;
-    StreamTime endPosition = 0;
-
-    if (!stream->mStreamInitialized) {
-      if (mInfo.mHasAudio) {
-        AudioSegment* audio = new AudioSegment();
-        audio->Init(mInfo.mAudioChannels);
-        mediaStream->AddTrack(TRACK_AUDIO, mInfo.mAudioRate, 0, audio);
-      }
-      if (mInfo.mHasVideo) {
-        VideoSegment* video = new VideoSegment();
-        mediaStream->AddTrack(TRACK_VIDEO, RATE_VIDEO, 0, video);
-      }
-      stream->mStreamInitialized = true;
-    }
-
-    if (mInfo.mHasAudio) {
-      nsAutoTArray<AudioData*,10> audio;
-      // It's OK to hold references to the AudioData because while audio
-      // is captured, only the decoder thread pops from the queue (see below).
-      mReader->mAudioQueue.GetElementsAfter(stream->mLastAudioPacketTime, &audio);
-      AudioSegment output;
-      output.Init(mInfo.mAudioChannels);
-      for (uint32_t i = 0; i < audio.Length(); ++i) {
-        SendOutputStreamAudio(audio[i], stream, &output);
-      }
-      if (output.GetDuration() > 0) {
-        mediaStream->AppendToTrack(TRACK_AUDIO, &output);
-      }
-      if (mReader->mAudioQueue.IsFinished() && !stream->mHaveSentFinishAudio) {
-        mediaStream->EndTrack(TRACK_AUDIO);
-        stream->mHaveSentFinishAudio = true;
-      }
-      minLastAudioPacketTime = NS_MIN(minLastAudioPacketTime, stream->mLastAudioPacketTime);
-      endPosition = NS_MAX(endPosition,
-          TicksToTimeRoundDown(mInfo.mAudioRate, stream->mAudioFramesWritten));
-    }
-
-    if (mInfo.mHasVideo) {
-      nsAutoTArray<VideoData*,10> video;
-      // It's OK to hold references to the VideoData only the decoder thread
-      // pops from the queue.
-      mReader->mVideoQueue.GetElementsAfter(stream->mNextVideoTime + mStartTime, &video);
-      VideoSegment output;
-      for (uint32_t i = 0; i < video.Length(); ++i) {
-        VideoData* v = video[i];
-        if (stream->mNextVideoTime + mStartTime < v->mTime) {
-          LOG(PR_LOG_DEBUG, ("%p Decoder writing last video to MediaStream for %lld ms",
-                             mDecoder.get(), v->mTime - (stream->mNextVideoTime + mStartTime)));
-          // Write last video frame to catch up. mLastVideoImage can be null here
-          // which is fine, it just means there's no video.
-          WriteVideoToMediaStream(stream->mLastVideoImage,
-              v->mTime - (stream->mNextVideoTime + mStartTime), stream->mLastVideoImageDisplaySize,
-              &output);
-          stream->mNextVideoTime = v->mTime - mStartTime;
-        }
-        if (stream->mNextVideoTime + mStartTime < v->mEndTime) {
-          LOG(PR_LOG_DEBUG, ("%p Decoder writing video frame %lld to MediaStream",
-                             mDecoder.get(), v->mTime));
-          WriteVideoToMediaStream(v->mImage,
-              v->mEndTime - (stream->mNextVideoTime + mStartTime), v->mDisplay,
-              &output);
-          stream->mNextVideoTime = v->mEndTime - mStartTime;
-          stream->mLastVideoImage = v->mImage;
-          stream->mLastVideoImageDisplaySize = v->mDisplay;
-        } else {
-          LOG(PR_LOG_DEBUG, ("%p Decoder skipping writing video frame %lld to MediaStream",
-                             mDecoder.get(), v->mTime));
-        }
-      }
-      if (output.GetDuration() > 0) {
-        mediaStream->AppendToTrack(TRACK_VIDEO, &output);
-      }
-      if (mReader->mVideoQueue.IsFinished() && !stream->mHaveSentFinishVideo) {
-        mediaStream->EndTrack(TRACK_VIDEO);
-        stream->mHaveSentFinishVideo = true;
-      }
-      endPosition = NS_MAX(endPosition,
-          TicksToTimeRoundDown(RATE_VIDEO, stream->mNextVideoTime));
-    }
-
-    if (!stream->mHaveSentFinish) {
-      stream->mStream->AdvanceKnownTracksTime(endPosition);
-    }
-
-    if (finished && !stream->mHaveSentFinish) {
-      stream->mHaveSentFinish = true;
-      stream->mStream->Finish();
-    }
+  if (finished && !stream->mHaveSentFinish) {
+    stream->mHaveSentFinish = true;
+    stream->mStream->Finish();
   }
 
   if (mAudioCaptured) {
     // Discard audio packets that are no longer needed.
     int64_t audioPacketTimeToDiscard =
         NS_MIN(minLastAudioPacketTime, mStartTime + mCurrentFrameTime);
     while (true) {
       nsAutoPtr<AudioData> a(mReader->mAudioQueue.PopFront());
@@ -694,106 +698,68 @@ void nsBuiltinDecoderStateMachine::SendO
 
     if (finished) {
       mAudioCompleted = true;
       UpdateReadyState();
     }
   }
 }
 
-void nsBuiltinDecoderStateMachine::FinishOutputStreams()
+nsBuiltinDecoderStateMachine::WakeDecoderRunnable*
+nsBuiltinDecoderStateMachine::GetWakeDecoderRunnable()
 {
-  // Tell all our output streams that all tracks have ended and we've
-  // finished.
-  nsTArray<OutputMediaStream>& streams = mDecoder->OutputStreams();
-  for (uint32_t i = 0; i < streams.Length(); ++i) {
-    OutputMediaStream* stream = &streams[i];
-    if (!stream->mStreamInitialized) {
-      continue;
-    }
-    SourceMediaStream* mediaStream = stream->mStream;
-    if (mInfo.mHasAudio && !stream->mHaveSentFinishAudio) {
-      mediaStream->EndTrack(TRACK_AUDIO);
-      stream->mHaveSentFinishAudio = true;
-    }
-    if (mInfo.mHasVideo && !stream->mHaveSentFinishVideo) {
-      mediaStream->EndTrack(TRACK_VIDEO);
-      stream->mHaveSentFinishVideo = true;
-    }
-    // XXX ignoring mFinishWhenEnded for now. Immediate goal is to not crash.
-    if (!stream->mHaveSentFinish) {
-      mediaStream->Finish();
-      stream->mHaveSentFinish = true;
-    }
+  mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
+
+  if (!mPendingWakeDecoder.get()) {
+    mPendingWakeDecoder = new WakeDecoderRunnable(this);
   }
+  return mPendingWakeDecoder.get();
 }
 
 bool nsBuiltinDecoderStateMachine::HaveEnoughDecodedAudio(int64_t aAmpleAudioUSecs)
 {
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
 
   if (mReader->mAudioQueue.GetSize() == 0 ||
       GetDecodedAudioDuration() < aAmpleAudioUSecs) {
     return false;
   }
   if (!mAudioCaptured) {
     return true;
   }
 
-  nsTArray<OutputMediaStream>& streams = mDecoder->OutputStreams();
-  for (uint32_t i = 0; i < streams.Length(); ++i) {
-    OutputMediaStream* stream = &streams[i];
-    if (stream->mStreamInitialized && !stream->mHaveSentFinishAudio &&
-        !stream->mStream->HaveEnoughBuffered(TRACK_AUDIO)) {
+  DecodedStreamData* stream = mDecoder->GetDecodedStream();
+  if (stream && stream->mStreamInitialized && !stream->mHaveSentFinishAudio) {
+    if (!stream->mStream->HaveEnoughBuffered(TRACK_AUDIO)) {
       return false;
     }
+    stream->mStream->DispatchWhenNotEnoughBuffered(TRACK_AUDIO,
+        GetStateMachineThread(), GetWakeDecoderRunnable());
   }
 
-  nsIThread* thread = GetStateMachineThread();
-  nsCOMPtr<nsIRunnable> callback = NS_NewRunnableMethod(this,
-      &nsBuiltinDecoderStateMachine::ScheduleStateMachineWithLockAndWakeDecoder);
-  for (uint32_t i = 0; i < streams.Length(); ++i) {
-    OutputMediaStream* stream = &streams[i];
-    if (stream->mStreamInitialized && !stream->mHaveSentFinishAudio) {
-      stream->mStream->DispatchWhenNotEnoughBuffered(TRACK_AUDIO, thread, callback);
-    }
-  }
   return true;
 }
 
 bool nsBuiltinDecoderStateMachine::HaveEnoughDecodedVideo()
 {
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
 
   if (static_cast<uint32_t>(mReader->mVideoQueue.GetSize()) < AMPLE_VIDEO_FRAMES) {
     return false;
   }
 
-  nsTArray<OutputMediaStream>& streams = mDecoder->OutputStreams();
-  if (streams.IsEmpty()) {
-    return true;
-  }
-
-  for (uint32_t i = 0; i < streams.Length(); ++i) {
-    OutputMediaStream* stream = &streams[i];
-    if (stream->mStreamInitialized && !stream->mHaveSentFinishVideo &&
-        !stream->mStream->HaveEnoughBuffered(TRACK_VIDEO)) {
+  DecodedStreamData* stream = mDecoder->GetDecodedStream();
+  if (stream && stream->mStreamInitialized && !stream->mHaveSentFinishVideo) {
+    if (!stream->mStream->HaveEnoughBuffered(TRACK_VIDEO)) {
       return false;
     }
+    stream->mStream->DispatchWhenNotEnoughBuffered(TRACK_VIDEO,
+        GetStateMachineThread(), GetWakeDecoderRunnable());
   }
 
-  nsIThread* thread = GetStateMachineThread();
-  nsCOMPtr<nsIRunnable> callback = NS_NewRunnableMethod(this,
-      &nsBuiltinDecoderStateMachine::ScheduleStateMachineWithLockAndWakeDecoder);
-  for (uint32_t i = 0; i < streams.Length(); ++i) {
-    OutputMediaStream* stream = &streams[i];
-    if (stream->mStreamInitialized && !stream->mHaveSentFinishVideo) {
-      stream->mStream->DispatchWhenNotEnoughBuffered(TRACK_VIDEO, thread, callback);
-    }
-  }
   return true;
 }
 
 void nsBuiltinDecoderStateMachine::DecodeLoop()
 {
   LOG(PR_LOG_DEBUG, ("%p Start DecodeLoop()", mDecoder.get()));
 
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
@@ -910,17 +876,17 @@ void nsBuiltinDecoderStateMachine::Decod
       audioPump = true;
     }
     mDidThrottleAudioDecoding = throttleAudioDecoding;
     if (!mDidThrottleAudioDecoding) {
       ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor());
       audioPlaying = mReader->DecodeAudioData();
     }
 
-    SendOutputStreamData();
+    SendStreamData();
 
     // Notify to ensure that the AudioLoop() is not waiting, in case it was
     // waiting for more audio to be decoded.
     mDecoder->GetReentrantMonitor().NotifyAll();
 
     // The ready state can change when we've decoded data, so update the
     // ready state, so that DOM events can fire.
     UpdateReadyState();
@@ -1520,16 +1486,17 @@ void nsBuiltinDecoderStateMachine::Seek(
 
   // Bound the seek time to be inside the media range.
   NS_ASSERTION(mStartTime != -1, "Should know start time by now");
   NS_ASSERTION(mEndTime != -1, "Should know end time by now");
   mSeekTime = NS_MIN(mSeekTime, mEndTime);
   mSeekTime = NS_MAX(mStartTime, mSeekTime);
   LOG(PR_LOG_DEBUG, ("%p Changed state to SEEKING (to %f)", mDecoder.get(), aTime));
   mState = DECODER_STATE_SEEKING;
+  mDecoder->RecreateDecodedStream(mSeekTime - mStartTime);
   ScheduleStateMachine();
 }
 
 void nsBuiltinDecoderStateMachine::StopDecodeThread()
 {
   NS_ASSERTION(OnStateMachineThread(), "Should be on state machine thread.");
   mDecoder->GetReentrantMonitor().AssertCurrentThreadIn();
   if (mRequestedNewDecodeThread) {
@@ -1996,22 +1963,21 @@ nsresult nsBuiltinDecoderStateMachine::R
 
   switch (mState) {
     case DECODER_STATE_SHUTDOWN: {
       if (IsPlaying()) {
         StopPlayback();
       }
       StopAudioThread();
       StopDecodeThread();
+      // Now that those threads are stopped, there's no possibility of
+      // mPendingWakeDecoder being needed again. Revoke it.
+      mPendingWakeDecoder = nullptr;
       NS_ASSERTION(mState == DECODER_STATE_SHUTDOWN,
                    "How did we escape from the shutdown state?");
-      // Need to call this before dispatching nsDispatchDisposeEvent below, to
-      // ensure that any notifications dispatched by the stream graph
-      // will run before nsDispatchDisposeEvent below.
-      FinishOutputStreams();
       // We must daisy-chain these events to destroy the decoder. We must
       // destroy the decoder on the main thread, but we can't destroy the
       // decoder while this thread holds the decoder monitor. We can't
       // dispatch an event to the main thread to destroy the decoder from
       // here, as the event may run before the dispatch returns, and we
       // hold the decoder monitor here. We also want to guarantee that the
       // state machine is destroyed on the main thread, and so the
       // event runner running this function (which holds a reference to the
--- a/content/media/nsBuiltinDecoderStateMachine.h
+++ b/content/media/nsBuiltinDecoderStateMachine.h
@@ -101,17 +101,17 @@ hardware (via nsAudioStream and libsydne
 */
 class nsBuiltinDecoderStateMachine : public nsDecoderStateMachine
 {
 public:
   typedef mozilla::ReentrantMonitor ReentrantMonitor;
   typedef mozilla::TimeStamp TimeStamp;
   typedef mozilla::TimeDuration TimeDuration;
   typedef mozilla::VideoFrameContainer VideoFrameContainer;
-  typedef nsBuiltinDecoder::OutputMediaStream OutputMediaStream;
+  typedef nsBuiltinDecoder::DecodedStreamData DecodedStreamData;
   typedef mozilla::SourceMediaStream SourceMediaStream;
   typedef mozilla::AudioSegment AudioSegment;
   typedef mozilla::VideoSegment VideoSegment;
 
   nsBuiltinDecoderStateMachine(nsBuiltinDecoder* aDecoder, nsBuiltinDecoderReader* aReader, bool aRealTime = false);
   ~nsBuiltinDecoderStateMachine();
 
   // nsDecoderStateMachine interface
@@ -255,22 +255,56 @@ public:
   void ReleaseDecoder() { mDecoder = nullptr; }
 
    // Called when a "MozAudioAvailable" event listener is added to the media
    // element. Called on the main thread.
    void NotifyAudioAvailableListener();
 
   // Copy queued audio/video data in the reader to any output MediaStreams that
   // need it.
-  void SendOutputStreamData();
-  void FinishOutputStreams();
+  void SendStreamData();
+  void FinishStreamData();
   bool HaveEnoughDecodedAudio(int64_t aAmpleAudioUSecs);
   bool HaveEnoughDecodedVideo();
 
 protected:
+  class WakeDecoderRunnable : public nsRunnable {
+  public:
+    WakeDecoderRunnable(nsBuiltinDecoderStateMachine* aSM)
+      : mMutex("WakeDecoderRunnable"), mStateMachine(aSM) {}
+    NS_IMETHOD Run()
+    {
+      nsRefPtr<nsBuiltinDecoderStateMachine> stateMachine;
+      {
+        // Don't let Run() (called by media stream graph thread) race with
+        // Revoke() (called by decoder state machine thread)
+        MutexAutoLock lock(mMutex);
+        if (!mStateMachine)
+          return NS_OK;
+        stateMachine = mStateMachine;
+      }
+      stateMachine->ScheduleStateMachineWithLockAndWakeDecoder();
+      return NS_OK;
+    }
+    void Revoke()
+    {
+      MutexAutoLock lock(mMutex);
+      mStateMachine = nullptr;
+    }
+
+    Mutex mMutex;
+    // Protected by mMutex.
+    // We don't use an owning pointer here, because keeping mStateMachine alive
+    // would mean in some cases we'd have to destroy mStateMachine from this
+    // object, which would be problematic since nsBuiltinDecoderStateMachine can
+    // only be destroyed on the main thread whereas this object can be destroyed
+    // on the media stream graph thread.
+    nsBuiltinDecoderStateMachine* mStateMachine;
+  };
+  WakeDecoderRunnable* GetWakeDecoderRunnable();
 
   // Returns true if we've got less than aAudioUsecs microseconds of decoded
   // and playable data. The decoder monitor must be held.
   bool HasLowDecodedData(int64_t aAudioUsecs) const;
 
   // Returns true if we're running low on data which is not yet decoded.
   // The decoder monitor must be held.
   bool HasLowUndecodedData() const;
@@ -425,18 +459,18 @@ protected:
   void DecodeLoop();
 
   // Decode thread run function. Determines which of the Decode*() functions
   // to call.
   void DecodeThreadRun();
 
   // Copy audio from an AudioData packet to aOutput. This may require
   // inserting silence depending on the timing of the audio packet.
-  void SendOutputStreamAudio(AudioData* aAudio, OutputMediaStream* aStream,
-                             AudioSegment* aOutput);
+  void SendStreamAudio(AudioData* aAudio, DecodedStreamData* aStream,
+                       AudioSegment* aOutput);
 
   // State machine thread run function. Defers to RunStateMachine().
   nsresult CallRunStateMachine();
 
   // Performs one "cycle" of the state machine. Polls the state, and may send
   // a video frame to be displayed, and generally manages the decode. Called
   // periodically via timer to ensure the video stays in sync.
   nsresult RunStateMachine();
@@ -525,16 +559,23 @@ protected:
   // first acquire the decoder monitor and check that it is non-null.
   nsRefPtr<nsAudioStream> mAudioStream;
 
   // The reader, don't call its methods with the decoder monitor held.
   // This is created in the play state machine's constructor, and destroyed
   // in the play state machine's destructor.
   nsAutoPtr<nsBuiltinDecoderReader> mReader;
 
+  // Accessed only on the state machine thread.
+  // Not an nsRevocableEventPtr since we must Revoke() it well before
+  // this object is destroyed, anyway.
+  // Protected by decoder monitor except during the SHUTDOWN state after the
+  // decoder thread has been stopped.
+  nsRevocableEventPtr<WakeDecoderRunnable> mPendingWakeDecoder;
+
   // The time of the current frame in microseconds. This is referenced from
   // 0 which is the initial playback position. Set by the state machine
   // thread, and read-only from the main thread to get the current
   // time value. Synchronised via decoder monitor.
   int64_t mCurrentFrameTime;
 
   // The presentation time of the first audio frame that was played in
   // microseconds. We can add this to the audio stream position to determine
--- a/content/media/nsDOMMediaStream.cpp
+++ b/content/media/nsDOMMediaStream.cpp
@@ -46,13 +46,22 @@ already_AddRefed<nsDOMMediaStream>
 nsDOMMediaStream::CreateInputStream()
 {
   nsRefPtr<nsDOMMediaStream> stream = new nsDOMMediaStream();
   MediaStreamGraph* gm = MediaStreamGraph::GetInstance();
   stream->mStream = gm->CreateInputStream(stream);
   return stream.forget();
 }
 
+already_AddRefed<nsDOMMediaStream>
+nsDOMMediaStream::CreateTrackUnionStream()
+{
+  nsRefPtr<nsDOMMediaStream> stream = new nsDOMMediaStream();
+  MediaStreamGraph* gm = MediaStreamGraph::GetInstance();
+  stream->mStream = gm->CreateTrackUnionStream(stream);
+  return stream.forget();
+}
+
 bool
 nsDOMMediaStream::CombineWithPrincipal(nsIPrincipal* aPrincipal)
 {
   return nsContentUtils::CombineResourcePrincipals(&mPrincipal, aPrincipal);
 }
--- a/content/media/nsDOMMediaStream.h
+++ b/content/media/nsDOMMediaStream.h
@@ -50,16 +50,21 @@ public:
    */
   bool CombineWithPrincipal(nsIPrincipal* aPrincipal);
 
   /**
    * Create an nsDOMMediaStream whose underlying stream is a SourceMediaStream.
    */
   static already_AddRefed<nsDOMMediaStream> CreateInputStream();
 
+  /**
+   * Create an nsDOMMediaStream whose underlying stream is a TrackUnionStream.
+   */
+  static already_AddRefed<nsDOMMediaStream> CreateTrackUnionStream();
+
 protected:
   // MediaStream is owned by the graph, but we tell it when to die, and it won't
   // die until we let it.
   MediaStream* mStream;
   // Principal identifying who may access the contents of this stream.
   // If null, this stream can be used by anyone because it has no content yet.
   nsCOMPtr<nsIPrincipal> mPrincipal;
 };
--- a/content/media/nsMediaDecoder.h
+++ b/content/media/nsMediaDecoder.h
@@ -36,16 +36,19 @@ static const uint32_t FRAMEBUFFER_LENGTH
 // with the exception of GetVideoFrameContainer and GetStatistics,
 // which can be called from any thread.
 class nsMediaDecoder : public nsIObserver
 {
 public:
   typedef mozilla::MediaResource MediaResource;
   typedef mozilla::ReentrantMonitor ReentrantMonitor;
   typedef mozilla::SourceMediaStream SourceMediaStream;
+  typedef mozilla::ProcessedMediaStream ProcessedMediaStream;
+  typedef mozilla::MediaInputPort MediaInputPort;
+  typedef mozilla::MainThreadMediaStreamListener MainThreadMediaStreamListener;
   typedef mozilla::TimeStamp TimeStamp;
   typedef mozilla::TimeDuration TimeDuration;
   typedef mozilla::VideoFrameContainer VideoFrameContainer;
 
   nsMediaDecoder();
   virtual ~nsMediaDecoder();
 
   // Create a new decoder of the same type as this one.
@@ -97,17 +100,20 @@ public:
   // Set the audio volume. It should be a value from 0 to 1.0.
   virtual void SetVolume(double aVolume) = 0;
 
   // Sets whether audio is being captured. If it is, we won't play any
   // of our audio.
   virtual void SetAudioCaptured(bool aCaptured) = 0;
 
   // Add an output stream. All decoder output will be sent to the stream.
-  virtual void AddOutputStream(SourceMediaStream* aStream, bool aFinishWhenEnded) = 0;
+  // The stream is initially blocked. The decoder is responsible for unblocking
+  // it while it is playing back.
+  virtual void AddOutputStream(ProcessedMediaStream* aStream,
+                               bool aFinishWhenEnded) = 0;
 
   // Start playback of a video. 'Load' must have previously been
   // called.
   virtual nsresult Play() = 0;
 
   // Start downloading the media. Decode the downloaded data up to the
   // point of the first frame of data.
   // aResource is the media stream to use. Ownership of aResource passes to