Bug 1544650 - Always pre-create MediaStreamTracks for DecodedStream in MediaDecoder. r=padenot
authorAndreas Pehrson <apehrson@mozilla.com>
Tue, 23 Apr 2019 16:46:30 +0000
changeset 529452 eab0e21e5347199b24d3eaf17669c2a8efb38781
parent 529451 284e4c610279c7cc676e80ff6c500315aacc15e3
child 529453 29103bace2abbd31d1e27d738483f6db26d07808
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspadenot
bugs1544650
milestone68.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 1544650 - Always pre-create MediaStreamTracks for DecodedStream in MediaDecoder. r=padenot This moves the responsibility for creating MediaStreamTracks from DecodedStream::Start to MediaDecoder. This let's MediaDecoder create them as soon as metadata is known. This gives the application guarantees on when tracks can be expected to exist, and aligns with the spec that says they should be created when metadata is known. Differential Revision: https://phabricator.services.mozilla.com/D28473
dom/media/MediaDecoder.cpp
dom/media/MediaDecoderStateMachine.cpp
dom/media/MediaDecoderStateMachine.h
dom/media/mediasink/DecodedStream.cpp
dom/media/mediasink/OutputStreamManager.cpp
dom/media/mediasink/OutputStreamManager.h
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -243,17 +243,20 @@ void MediaDecoder::SetOutputStreamCORSMo
   mDecoderStateMachine->SetOutputStreamCORSMode(aCORSMode);
 }
 
 void MediaDecoder::AddOutputStream(DOMMediaStream* aStream) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
   AbstractThread::AutoEnter context(AbstractMainThread());
   mDecoderStateMachine->EnsureOutputStreamManager(
-      aStream->GetInputStream()->Graph(), ToMaybe(mInfo.get()));
+      aStream->GetInputStream()->Graph());
+  if (mInfo) {
+    mDecoderStateMachine->EnsureOutputStreamManagerHasTracks(*mInfo);
+  }
   mDecoderStateMachine->AddOutputStream(aStream);
 }
 
 void MediaDecoder::RemoveOutputStream(DOMMediaStream* aStream) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
   AbstractThread::AutoEnter context(AbstractMainThread());
   mDecoderStateMachine->RemoveOutputStream(aStream);
@@ -651,16 +654,17 @@ void MediaDecoder::MetadataLoaded(
       aInfo->mAudio.mChannels, aInfo->mAudio.mRate, aInfo->HasAudio(),
       aInfo->HasVideo());
 
   mMediaSeekable = aInfo->mMediaSeekable;
   mMediaSeekableOnlyInBufferedRanges =
       aInfo->mMediaSeekableOnlyInBufferedRanges;
   mInfo = aInfo.release();
   GetOwner()->ConstructMediaTracks(mInfo);
+  mDecoderStateMachine->EnsureOutputStreamManagerHasTracks(*mInfo);
 
   // Make sure the element and the frame (if any) are told about
   // our new size.
   if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
     mFiredMetadataLoaded = true;
     GetOwner()->MetadataLoaded(mInfo, std::move(aTags));
   }
   // Invalidate() will end up calling GetOwner()->UpdateMediaSize with the last
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -3333,18 +3333,18 @@ void MediaDecoderStateMachine::FinishDec
   mReader->ReadUpdatedMetadata(mInfo.ptr());
 
   EnqueueFirstFrameLoadedEvent();
 }
 
 RefPtr<ShutdownPromise> MediaDecoderStateMachine::BeginShutdown() {
   MOZ_ASSERT(NS_IsMainThread());
   if (mOutputStreamManager) {
+    mOutputStreamManager->Disconnect();
     mNextOutputStreamTrackID = mOutputStreamManager->NextTrackID();
-    mOutputStreamManager->Disconnect();
   }
   return InvokeAsync(OwnerThread(), this, __func__,
                      &MediaDecoderStateMachine::Shutdown);
 }
 
 RefPtr<ShutdownPromise> MediaDecoderStateMachine::FinishShutdown() {
   MOZ_ASSERT(OnTaskQueue());
   LOG("Shutting down state machine task queue");
@@ -3785,38 +3785,49 @@ void MediaDecoderStateMachine::RemoveOut
         });
     nsresult rv = OwnerThread()->Dispatch(r.forget());
     MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
     Unused << rv;
   }
 }
 
 void MediaDecoderStateMachine::EnsureOutputStreamManager(
-    MediaStreamGraph* aGraph, const Maybe<MediaInfo>& aLoadedInfo) {
+    MediaStreamGraph* aGraph) {
   MOZ_ASSERT(NS_IsMainThread());
   if (mOutputStreamManager) {
     return;
   }
-  LOG("Starting output track allocations at id %d", mNextOutputStreamTrackID);
   mOutputStreamManager = new OutputStreamManager(
       aGraph->CreateSourceStream(), mNextOutputStreamTrackID,
       mOutputStreamPrincipal, mOutputStreamCORSMode, mAbstractMainThread);
-  if (!aLoadedInfo) {
+}
+
+void MediaDecoderStateMachine::EnsureOutputStreamManagerHasTracks(
+    const MediaInfo& aLoadedInfo) {
+  MOZ_ASSERT(NS_IsMainThread());
+  if (!mOutputStreamManager) {
     return;
   }
-  TrackID mirroredTrackIDAllocation = mNextOutputStreamTrackID;
-  if (aLoadedInfo->HasAudio()) {
-    mOutputStreamManager->AddTrack(mirroredTrackIDAllocation++,
-                                   MediaSegment::AUDIO);
-    LOG("Pre-created audio track with id %d", mirroredTrackIDAllocation - 1);
+  if ((!aLoadedInfo.HasAudio() ||
+       mOutputStreamManager->HasTrackType(MediaSegment::AUDIO)) &&
+      (!aLoadedInfo.HasVideo() ||
+       mOutputStreamManager->HasTrackType(MediaSegment::VIDEO))) {
+    return;
   }
-  if (aLoadedInfo->HasVideo()) {
-    mOutputStreamManager->AddTrack(mirroredTrackIDAllocation++,
-                                   MediaSegment::VIDEO);
-    LOG("Pre-created video track with id %d", mirroredTrackIDAllocation - 1);
+  if (aLoadedInfo.HasAudio()) {
+    MOZ_ASSERT(!mOutputStreamManager->HasTrackType(MediaSegment::AUDIO));
+    mOutputStreamManager->AddTrack(MediaSegment::AUDIO);
+    LOG("Pre-created audio track with id %d",
+        mOutputStreamManager->GetLiveTrackIDFor(MediaSegment::AUDIO));
+  }
+  if (aLoadedInfo.HasVideo()) {
+    MOZ_ASSERT(!mOutputStreamManager->HasTrackType(MediaSegment::VIDEO));
+    mOutputStreamManager->AddTrack(MediaSegment::VIDEO);
+    LOG("Pre-created video track with id %d",
+        mOutputStreamManager->GetLiveTrackIDFor(MediaSegment::VIDEO));
   }
 }
 
 void MediaDecoderStateMachine::SetNextOutputStreamTrackID(
     TrackID aNextTrackID) {
   MOZ_ASSERT(NS_IsMainThread());
   LOG("SetNextOutputStreamTrackID aNextTrackID=%d", aNextTrackID);
   mNextOutputStreamTrackID = aNextTrackID;
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -181,21 +181,21 @@ class MediaDecoderStateMachine
 
   // Returns the state machine task queue.
   TaskQueue* OwnerThread() const { return mTaskQueue; }
 
   RefPtr<MediaDecoder::DebugInfoPromise> RequestDebugInfo();
 
   void SetOutputStreamPrincipal(const nsCOMPtr<nsIPrincipal>& aPrincipal);
   void SetOutputStreamCORSMode(CORSMode aCORSMode);
-  // If an OutputStreamManager does not exist, one will be created and tracks
-  // matching aLoadedInfo will be created ahead of being created by the
-  // DecodedStream sink.
-  void EnsureOutputStreamManager(MediaStreamGraph* aGraph,
-                                 const Maybe<MediaInfo>& aLoadedInfo);
+  // If an OutputStreamManager does not exist, one will be created.
+  void EnsureOutputStreamManager(MediaStreamGraph* aGraph);
+  // If an OutputStreamManager exists, tracks matching aLoadedInfo will be
+  // created unless they already exist in the manager.
+  void EnsureOutputStreamManagerHasTracks(const MediaInfo& aLoadedInfo);
   // Add an output stream to the output stream manager. The manager must have
   // been created through EnsureOutputStreamManager() before this.
   void AddOutputStream(DOMMediaStream* aStream);
   // Remove an output stream added with AddOutputStream. If the last output
   // stream was removed, we will also tear down the OutputStreamManager.
   void RemoveOutputStream(DOMMediaStream* aStream);
   // Set the TrackID to be used as the initial id by the next DecodedStream
   // sink.
--- a/dom/media/mediasink/DecodedStream.cpp
+++ b/dom/media/mediasink/DecodedStream.cpp
@@ -312,42 +312,24 @@ DecodedStreamData::DecodedStreamData(
       mStream(aOutputStreamManager->mSourceStream),
       // DecodedStreamGraphListener will resolve these promises.
       mListener(MakeRefPtr<DecodedStreamGraphListener>(
           mStream, aInit.mAudioTrackID, std::move(aAudioEndedPromise),
           aInit.mVideoTrackID, std::move(aVideoEndedPromise), aMainThread)),
       mOutputStreamManager(aOutputStreamManager),
       mAbstractMainThread(aMainThread) {
   MOZ_ASSERT(NS_IsMainThread());
-  // Initialize tracks on main thread and in the MediaStreamGraph.
-  // Tracks on main thread may have been created early in OutputStreamManager
-  // by the state machine, since creating them here is async from the js call.
-  // If they were pre-created in OutputStreamManager and the MediaInfo has
-  // changed since then, we end them and create new tracks.
-  if (!mOutputStreamManager->HasTracks(aInit.mAudioTrackID,
-                                       aInit.mVideoTrackID)) {
-    // Because these tracks were pre-allocated, we also have to increment the
-    // internal track allocator by the same number of tracks, so we don't risk
-    // a TrackID collision.
-    for (size_t i = 0; i < mOutputStreamManager->NumberOfTracks(); ++i) {
-      Unused << mOutputStreamManager->AllocateNextTrackID();
-    }
-    mOutputStreamManager->RemoveTracks();
-  }
+  MOZ_DIAGNOSTIC_ASSERT(
+      mOutputStreamManager->HasTracks(aInit.mAudioTrackID, aInit.mVideoTrackID),
+      "Tracks must be pre-created on main thread");
   if (IsTrackIDExplicit(aInit.mAudioTrackID)) {
-    if (!mOutputStreamManager->HasTrack(aInit.mAudioTrackID)) {
-      mOutputStreamManager->AddTrack(aInit.mAudioTrackID, MediaSegment::AUDIO);
-    }
     mStream->AddAudioTrack(aInit.mAudioTrackID, aInit.mInfo.mAudio.mRate,
                            new AudioSegment());
   }
   if (IsTrackIDExplicit(aInit.mVideoTrackID)) {
-    if (!mOutputStreamManager->HasTrack(aInit.mVideoTrackID)) {
-      mOutputStreamManager->AddTrack(aInit.mVideoTrackID, MediaSegment::VIDEO);
-    }
     mStream->AddTrack(aInit.mVideoTrackID, new VideoSegment());
   }
 }
 
 DecodedStreamData::~DecodedStreamData() { MOZ_ASSERT(NS_IsMainThread()); }
 
 MediaEventSource<int64_t>& DecodedStreamData::OnOutput() {
   return mListener->OnOutput();
@@ -452,22 +434,28 @@ nsresult DecodedStream::Start(const Time
       // This happens when RemoveOutput() is called immediately after
       // StartPlayback().
       if (mOutputStreamManager->IsEmpty()) {
         // Resolve the promise to indicate the end of playback.
         mAudioEndedPromise.Resolve(true, __func__);
         mVideoEndedPromise.Resolve(true, __func__);
         return NS_OK;
       }
-      mInit.mAudioTrackID = mInit.mInfo.HasAudio()
-                                ? mOutputStreamManager->AllocateNextTrackID()
-                                : TRACK_NONE;
-      mInit.mVideoTrackID = mInit.mInfo.HasVideo()
-                                ? mOutputStreamManager->AllocateNextTrackID()
-                                : TRACK_NONE;
+      if (mInit.mInfo.HasAudio() &&
+          !mOutputStreamManager->HasTrackType(MediaSegment::AUDIO)) {
+        mOutputStreamManager->AddTrack(MediaSegment::AUDIO);
+      }
+      if (mInit.mInfo.HasVideo() &&
+          !mOutputStreamManager->HasTrackType(MediaSegment::VIDEO)) {
+        mOutputStreamManager->AddTrack(MediaSegment::VIDEO);
+      }
+      mInit.mAudioTrackID =
+          mOutputStreamManager->GetLiveTrackIDFor(MediaSegment::AUDIO);
+      mInit.mVideoTrackID =
+          mOutputStreamManager->GetLiveTrackIDFor(MediaSegment::VIDEO);
       mData = MakeUnique<DecodedStreamData>(
           mOutputStreamManager, std::move(mInit), std::move(mAudioEndedPromise),
           std::move(mVideoEndedPromise), mAbstractMainThread);
       return NS_OK;
     }
     UniquePtr<DecodedStreamData> ReleaseData() { return std::move(mData); }
 
    private:
@@ -537,19 +525,22 @@ void DecodedStream::DestroyData(UniquePt
   AssertOwnerThread();
 
   if (!aData) {
     return;
   }
 
   mOutputListener.Disconnect();
 
-  NS_DispatchToMainThread(
-      NS_NewRunnableFunction("DecodedStream::DestroyData",
-                             [data = std::move(aData)]() { data->Forget(); }));
+  NS_DispatchToMainThread(NS_NewRunnableFunction(
+      "DecodedStream::DestroyData",
+      [data = std::move(aData), manager = mOutputStreamManager]() {
+        data->Forget();
+        manager->RemoveTracks();
+      }));
 }
 
 void DecodedStream::SetPlaying(bool aPlaying) {
   AssertOwnerThread();
 
   // Resume/pause matters only when playback started.
   if (mStartTime.isNothing()) {
     return;
--- a/dom/media/mediasink/OutputStreamManager.cpp
+++ b/dom/media/mediasink/OutputStreamManager.cpp
@@ -196,90 +196,87 @@ void OutputStreamManager::Remove(DOMMedi
           aData->RemoveTrack(pair.first());
         }
       },
       []() { MOZ_ASSERT_UNREACHABLE("Didn't exist"); });
   DebugOnly<bool> rv = mStreams.RemoveElement(aDOMStream, StreamComparator());
   MOZ_ASSERT(rv);
 }
 
-bool OutputStreamManager::HasTrack(TrackID aTrackID) {
+bool OutputStreamManager::HasTrackType(MediaSegment::Type aType) {
   MOZ_ASSERT(NS_IsMainThread());
 
-  return mLiveTracks.Contains(aTrackID, TrackIDComparator());
+  return mLiveTracks.Contains(aType, TrackTypeComparator());
 }
 
 bool OutputStreamManager::HasTracks(TrackID aAudioTrack, TrackID aVideoTrack) {
   MOZ_ASSERT(NS_IsMainThread());
 
   size_t nrExpectedTracks = 0;
   bool asExpected = true;
   if (IsTrackIDExplicit(aAudioTrack)) {
     Unused << ++nrExpectedTracks;
     asExpected = asExpected && mLiveTracks.Contains(
                                    MakePair(aAudioTrack, MediaSegment::AUDIO),
-                                   TrackTypeComparator());
+                                   TrackComparator());
   }
   if (IsTrackIDExplicit(aVideoTrack)) {
     Unused << ++nrExpectedTracks;
     asExpected = asExpected && mLiveTracks.Contains(
                                    MakePair(aVideoTrack, MediaSegment::VIDEO),
-                                   TrackTypeComparator());
+                                   TrackComparator());
   }
   asExpected = asExpected && mLiveTracks.Length() == nrExpectedTracks;
   return asExpected;
 }
 
 size_t OutputStreamManager::NumberOfTracks() {
   MOZ_ASSERT(NS_IsMainThread());
   return mLiveTracks.Length();
 }
 
-void OutputStreamManager::AddTrack(TrackID aTrackID, MediaSegment::Type aType) {
+void OutputStreamManager::AddTrack(MediaSegment::Type aType) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mSourceStream->IsDestroyed());
-  MOZ_ASSERT(!HasTrack(aTrackID));
+  MOZ_ASSERT(!HasTrackType(aType),
+             "Cannot have two tracks of the same type at the same time");
+
+  TrackID id = mNextTrackID++;
 
   LOG(LogLevel::Info, "Adding %s track with id %d",
-      aType == MediaSegment::AUDIO ? "audio" : "video", aTrackID);
+      aType == MediaSegment::AUDIO ? "audio" : "video", id);
 
-  mLiveTracks.AppendElement(MakePair(aTrackID, aType));
+  mLiveTracks.AppendElement(MakePair(id, aType));
   for (const auto& data : mStreams) {
-    data->AddTrack(aTrackID, aType, mPrincipal, mCORSMode, true);
+    data->AddTrack(id, aType, mPrincipal, mCORSMode, true);
   }
 }
 
 void OutputStreamManager::RemoveTrack(TrackID aTrackID) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mSourceStream->IsDestroyed());
   LOG(LogLevel::Info, "Removing track with id %d", aTrackID);
   DebugOnly<bool> rv = mLiveTracks.RemoveElement(aTrackID, TrackIDComparator());
   MOZ_ASSERT(rv);
   for (const auto& data : mStreams) {
     data->RemoveTrack(aTrackID);
   }
 }
 
 void OutputStreamManager::RemoveTracks() {
   MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(!mSourceStream->IsDestroyed());
-  for (const Pair<TrackID, MediaSegment::Type>& pair : mLiveTracks) {
-    for (const auto& data : mStreams) {
-      data->RemoveTrack(pair.first());
-    }
+  nsTArray<Pair<TrackID, MediaSegment::Type>> liveTracks(mLiveTracks);
+  for (const auto& pair : liveTracks) {
+    RemoveTrack(pair.first());
   }
-  mLiveTracks.Clear();
 }
 
 void OutputStreamManager::Disconnect() {
   MOZ_ASSERT(NS_IsMainThread());
-  nsTArray<Pair<TrackID, MediaSegment::Type>> liveTracks(mLiveTracks);
-  for (const auto& pair : liveTracks) {
-    RemoveTrack(pair.first());
-  }
+  RemoveTracks();
   MOZ_ASSERT(mLiveTracks.IsEmpty());
   nsTArray<RefPtr<DOMMediaStream>> domStreams(mStreams.Length());
   for (const auto& data : mStreams) {
     domStreams.AppendElement(data->mDOMStream);
   }
   for (auto& domStream : domStreams) {
     Remove(domStream);
   }
@@ -306,20 +303,24 @@ void OutputStreamManager::SetPrincipal(n
   }
 }
 
 TrackID OutputStreamManager::NextTrackID() const {
   MOZ_ASSERT(NS_IsMainThread());
   return mNextTrackID;
 }
 
-TrackID OutputStreamManager::AllocateNextTrackID() {
+TrackID OutputStreamManager::GetLiveTrackIDFor(MediaSegment::Type aType) const {
   MOZ_ASSERT(NS_IsMainThread());
-  MOZ_RELEASE_ASSERT(IsTrackIDExplicit(mNextTrackID));
-  return mNextTrackID++;
+  for (const auto& pair : mLiveTracks) {
+    if (pair.second() == aType) {
+      return pair.first();
+    }
+  }
+  return TRACK_NONE;
 }
 
 void OutputStreamManager::SetPlaying(bool aPlaying) {
   MOZ_ASSERT(NS_IsMainThread());
   if (mPlaying == aPlaying) {
     return;
   }
 
--- a/dom/media/mediasink/OutputStreamManager.h
+++ b/dom/media/mediasink/OutputStreamManager.h
@@ -71,45 +71,43 @@ class OutputStreamManager {
   explicit OutputStreamManager(SourceMediaStream* aSourceStream,
                                TrackID aNextTrackID, nsIPrincipal* aPrincipal,
                                CORSMode aCORSMode,
                                AbstractThread* aAbstractMainThread);
   // Add the output stream to the collection.
   void Add(DOMMediaStream* aDOMStream);
   // Remove the output stream from the collection.
   void Remove(DOMMediaStream* aDOMStream);
-  // Returns true if aTrackID has been added to all output streams.
-  bool HasTrack(TrackID aTrackID);
+  // Returns true if there's a live track of the given type.
+  bool HasTrackType(MediaSegment::Type aType);
   // Returns true if the given tracks and no others are currently live.
   // Use a non-explicit TrackID to make it ignored for that type.
   bool HasTracks(TrackID aAudioTrack, TrackID aVideoTrack);
   // Returns the number of live tracks.
   size_t NumberOfTracks();
-  // Add aTrackID to all output streams.
-  void AddTrack(TrackID aTrackID, MediaSegment::Type aType);
-  // Remove aTrackID from all output streams.
-  void RemoveTrack(TrackID aTrackID);
-  // Remove all added tracks from all output streams.
+  // Add a track to all output streams.
+  void AddTrack(MediaSegment::Type aType);
+  // Remove all currently live tracks from all output streams.
   void RemoveTracks();
   // Disconnect mSourceStream from all output streams.
   void Disconnect();
   // The principal handle for the underlying decoder.
   AbstractCanonical<PrincipalHandle>* CanonicalPrincipalHandle();
   // Called when the underlying decoder's principal has changed.
   void SetPrincipal(nsIPrincipal* aPrincipal);
   // The CORSMode for the media element owning the decoder.
   AbstractCanonical<CORSMode>* CanonicalCORSMode();
   // Called when the CORSMode for the media element owning the decoder has
   // changed.
   void SetCORSMode(CORSMode aCORSMode);
-  // Returns the track id that would be used the next time a track is allocated.
+  // Returns the track id that would be used the next time a track is added.
   TrackID NextTrackID() const;
-  // Like NextTrackID() but advances internal state, so the next call returns a
-  // new unique TrackID.
-  TrackID AllocateNextTrackID();
+  // Returns the TrackID for the currently live track of the given type, or
+  // TRACK_NONE otherwise.
+  TrackID GetLiveTrackIDFor(MediaSegment::Type aType) const;
   // Called by DecodedStream when its playing state changes. While not playing
   // we suspend mSourceStream.
   void SetPlaying(bool aPlaying);
   // Return true if the collection of output streams is empty.
   bool IsEmpty() const {
     MOZ_ASSERT(NS_IsMainThread());
     return mStreams.IsEmpty();
   }
@@ -130,21 +128,31 @@ class OutputStreamManager {
   struct TrackIDComparator {
     static bool Equals(const Pair<TrackID, MediaSegment::Type>& aLiveTrack,
                        TrackID aTrackID) {
       return aLiveTrack.first() == aTrackID;
     }
   };
   struct TrackTypeComparator {
     static bool Equals(const Pair<TrackID, MediaSegment::Type>& aLiveTrack,
+                       MediaSegment::Type aType) {
+      return aLiveTrack.second() == aType;
+    }
+  };
+  struct TrackComparator {
+    static bool Equals(const Pair<TrackID, MediaSegment::Type>& aLiveTrack,
                        const Pair<TrackID, MediaSegment::Type>& aOther) {
       return aLiveTrack.first() == aOther.first() &&
              aLiveTrack.second() == aOther.second();
     }
   };
+
+  // Remove aTrackID from all output streams.
+  void RemoveTrack(TrackID aTrackID);
+
   nsTArray<UniquePtr<OutputStreamData>> mStreams;
   nsTArray<Pair<TrackID, MediaSegment::Type>> mLiveTracks;
   Canonical<PrincipalHandle> mPrincipalHandle;
   nsCOMPtr<nsIPrincipal> mPrincipal;
   const CORSMode mCORSMode;
   TrackID mNextTrackID;
   bool mPlaying;
 };