Bug 1208371 - Check principal per track instead of per stream in MediaRecorder. r?roc draft
authorAndreas Pehrson <pehrsons@gmail.com>
Mon, 01 Feb 2016 22:47:17 +0800
changeset 342147 4b5d7c4f875af6691046a85de13c6c193322d0a9
parent 342146 bc6ae2dd320a2ec79a91876d20799033d1a7a856
child 342148 b0c78c1bc07d80c73e30896c25e8ed3b624f3aee
push id13352
push userpehrsons@gmail.com
push dateFri, 18 Mar 2016 13:49:47 +0000
reviewersroc
bugs1208371
milestone47.0a1
Bug 1208371 - Check principal per track instead of per stream in MediaRecorder. r?roc MozReview-Commit-ID: ItwZ4tzhsIC
dom/locales/en-US/chrome/dom/dom.properties
dom/media/MediaRecorder.cpp
dom/media/MediaRecorder.h
--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -102,16 +102,18 @@ MediaLoadInvalidURI=Invalid URI. Load of
 # LOCALIZATION NOTE: %1$S is the media resource's format/codec type (basically equivalent to the file type, e.g. MP4,AVI,WMV,MOV etc), %2$S is the URL of the media resource which failed to load.
 MediaLoadUnsupportedTypeAttribute=Specified "type" attribute of "%1$S" is not supported. Load of media resource %2$S failed.
 # LOCALIZATION NOTE: %1$S is the "media" attribute value of the <source> element. It is a media query. %2$S is the URL of the media resource which failed to load.
 MediaLoadSourceMediaNotMatched=Specified "media" attribute of "%1$S" does not match the environment. Load of media resource %2$S failed.
 # LOCALIZATION NOTE: %1$S is the MIME type HTTP header being sent by the web server, %2$S is the URL of the media resource which failed to load.
 MediaLoadUnsupportedMimeType=HTTP "Content-Type" of "%1$S" is not supported. Load of media resource %2$S failed.
 # LOCALIZATION NOTE: %S is the URL of the media resource which failed to load because of error in decoding.
 MediaLoadDecodeError=Media resource %S could not be decoded.
+# LOCALIZATION NOTE: Do not translate "MediaRecorder".
+MediaRecorderMultiTracksNotSupported=MediaRecorder does not support recording multiple tracks of the same type at this time.
 # LOCALIZATION NOTE: %S is the ID of the MediaStreamTrack passed to MediaStream.addTrack(). Do not translate "MediaStreamTrack" and "AudioChannel".
 MediaStreamAddTrackDifferentAudioChannel=MediaStreamTrack %S could not be added since it belongs to a different AudioChannel.
 # LOCALIZATION NOTE: Do not translate "MediaStream", "stop()" and "MediaStreamTrack"
 MediaStreamStopDeprecatedWarning=MediaStream.stop() is deprecated and will soon be removed. Use MediaStreamTrack.stop() instead.
 # LOCALIZATION NOTE: Do not translate "DOMException", "code" and "name"
 DOMExceptionCodeWarning=Use of DOMException's code attribute is deprecated. Use name instead.
 # LOCALIZATION NOTE: Do not translate "__exposedProps__"
 NoExposedPropsWarning=Exposing chrome JS objects to content without __exposedProps__ is insecure and deprecated. See https://developer.mozilla.org/en/XPConnect_wrappers for more information.
--- a/dom/media/MediaRecorder.cpp
+++ b/dom/media/MediaRecorder.cpp
@@ -18,16 +18,17 @@
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/RecordErrorEvent.h"
 #include "mozilla/dom/VideoStreamTrack.h"
 #include "nsContentUtils.h"
 #include "nsError.h"
 #include "nsIDocument.h"
 #include "nsIPermissionManager.h"
 #include "nsIPrincipal.h"
+#include "nsIScriptError.h"
 #include "nsMimeTypes.h"
 #include "nsProxyRelease.h"
 #include "nsTArray.h"
 #include "GeckoProfiler.h"
 
 #ifdef LOG
 #undef LOG
 #endif
@@ -149,17 +150,19 @@ NS_IMPL_RELEASE_INHERITED(MediaRecorder,
  *    Therefore, the reference dependency in gecko is:
  *    ShutdownObserver -> Session <-> MediaRecorder, note that there is a cycle
  *    reference between Session and MediaRecorder.
  * 2) A Session is destroyed in DestroyRunnable after MediaRecorder::Stop being called
  *    _and_ all encoded media data been passed to OnDataAvailable handler.
  * 3) MediaRecorder::Stop is called by user or the document is going to
  *    inactive or invisible.
  */
-class MediaRecorder::Session: public nsIObserver
+class MediaRecorder::Session: public nsIObserver,
+                              public PrincipalChangeObserver<MediaStreamTrack>,
+                              public DOMMediaStream::TrackListener
 {
   NS_DECL_THREADSAFE_ISUPPORTS
 
   // Main thread task.
   // Create a blob event and send back to client.
   class PushBlobRunnable : public nsRunnable
   {
   public:
@@ -287,27 +290,61 @@ class MediaRecorder::Session: public nsI
   // For Ensure recorder has tracks to record.
   class TracksAvailableCallback : public OnTracksAvailableCallback
   {
   public:
     explicit TracksAvailableCallback(Session *aSession)
      : mSession(aSession) {}
     virtual void NotifyTracksAvailable(DOMMediaStream* aStream)
     {
+      if (mSession->mStopIssued) {
+        return;
+      }
+
+      MOZ_RELEASE_ASSERT(aStream);
+      mSession->MediaStreamReady(*aStream);
+
       uint8_t trackTypes = 0;
       nsTArray<RefPtr<mozilla::dom::AudioStreamTrack>> audioTracks;
       aStream->GetAudioTracks(audioTracks);
       if (!audioTracks.IsEmpty()) {
         trackTypes |= ContainerWriter::CREATE_AUDIO_TRACK;
+        mSession->ConnectMediaStreamTrack(*audioTracks[0]);
       }
 
       nsTArray<RefPtr<mozilla::dom::VideoStreamTrack>> videoTracks;
       aStream->GetVideoTracks(videoTracks);
       if (!videoTracks.IsEmpty()) {
         trackTypes |= ContainerWriter::CREATE_VIDEO_TRACK;
+        mSession->ConnectMediaStreamTrack(*videoTracks[0]);
+      }
+
+      if (audioTracks.Length() > 1 ||
+          videoTracks.Length() > 1) {
+        // When MediaRecorder supports multiple tracks, we should set up a single
+        // MediaInputPort from the input stream, and let main thread check
+        // track principals async later.
+        nsPIDOMWindowInner* window = mSession->mRecorder->GetParentObject();
+        nsIDocument* document = window ? window->GetExtantDoc() : nullptr;
+        nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
+                                        NS_LITERAL_CSTRING("Media"),
+                                        document,
+                                        nsContentUtils::eDOM_PROPERTIES,
+                                        "MediaRecorderMultiTracksNotSupported");
+        mSession->DoSessionEndTask(NS_ERROR_ABORT);
+        return;
+      }
+
+      NS_ASSERTION(trackTypes != 0, "TracksAvailableCallback without any tracks available");
+
+      // Check that we may access the tracks' content.
+      if (!mSession->MediaStreamTracksPrincipalSubsumes()) {
+        LOG(LogLevel::Warning, ("Session.NotifyTracksAvailable MediaStreamTracks principal check failed"));
+        mSession->DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
+        return;
       }
 
       LOG(LogLevel::Debug, ("Session.NotifyTracksAvailable track type = (%d)", trackTypes));
       mSession->InitEncoder(trackTypes);
     }
   private:
     RefPtr<Session> mSession;
   };
@@ -368,44 +405,94 @@ public:
     : mRecorder(aRecorder)
     , mTimeSlice(aTimeSlice)
     , mStopIssued(false)
     , mIsStartEventFired(false)
     , mIsRegisterProfiler(false)
     , mNeedSessionEndTask(true)
   {
     MOZ_ASSERT(NS_IsMainThread());
+    MOZ_COUNT_CTOR(MediaRecorder::Session);
 
     uint32_t maxMem = Preferences::GetUint("media.recorder.max_memory",
                                            MAX_ALLOW_MEMORY_BUFFER);
     mEncodedBufferCache = new EncodedBufferCache(maxMem);
     mLastBlobTimeStamp = TimeStamp::Now();
   }
 
+  void PrincipalChanged(MediaStreamTrack* aTrack) override
+  {
+    NS_ASSERTION(mMediaStreamTracks.Contains(aTrack),
+                 "Principal changed for unrecorded track");
+    if (!MediaStreamTracksPrincipalSubsumes()) {
+      DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
+    }
+  }
+
+  void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override
+  {
+    LOG(LogLevel::Warning, ("Session.NotifyTrackAdded %p Raising error due to track set change", this));
+    DoSessionEndTask(NS_ERROR_ABORT);
+  }
+
+  void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override
+  {
+    RefPtr<MediaInputPort> foundInputPort;
+    for (RefPtr<MediaInputPort> inputPort : mInputPorts) {
+      if (aTrack->IsForwardedThrough(inputPort)) {
+        foundInputPort = inputPort;
+        break;
+      }
+    }
+
+    if (foundInputPort) {
+      // A recorded track was removed or ended. End it in the recording.
+      // Don't raise an error.
+      foundInputPort->Destroy();
+      DebugOnly<bool> removed = mInputPorts.RemoveElement(foundInputPort);
+      MOZ_ASSERT(removed);
+      return;
+    }
+
+    LOG(LogLevel::Warning, ("Session.NotifyTrackRemoved %p Raising error due to track set change", this));
+    DoSessionEndTask(NS_ERROR_ABORT);
+  }
+
   void Start()
   {
     LOG(LogLevel::Debug, ("Session.Start %p", this));
     MOZ_ASSERT(NS_IsMainThread());
 
     // Create a Track Union Stream
     MediaStreamGraph* gm = mRecorder->GetSourceMediaStream()->Graph();
     mTrackUnionStream = gm->CreateTrackUnionStream(nullptr);
     MOZ_ASSERT(mTrackUnionStream, "CreateTrackUnionStream failed");
 
     mTrackUnionStream->SetAutofinish(true);
 
-    // Bind this Track Union Stream with Source Media.
-    mInputPort = mTrackUnionStream->AllocateInputPort(mRecorder->GetSourceMediaStream());
-
     DOMMediaStream* domStream = mRecorder->Stream();
     if (domStream) {
-      // Get the track type hint from DOM media stream.
+      // Get the available tracks from the DOMMediaStream.
+      // The callback will report back tracks that we have to connect to
+      // mTrackUnionStream and listen to principal changes on.
       TracksAvailableCallback* tracksAvailableCallback = new TracksAvailableCallback(this);
       domStream->OnTracksAvailable(tracksAvailableCallback);
     } else {
+      // Check that we may access the audio node's content.
+      if (!AudioNodePrincipalSubsumes()) {
+        LOG(LogLevel::Warning, ("Session.Start AudioNode principal check failed"));
+        DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
+        return;
+      }
+      // Bind this Track Union Stream with Source Media.
+      RefPtr<MediaInputPort> inputPort =
+        mTrackUnionStream->AllocateInputPort(mRecorder->GetSourceMediaStream());
+      mInputPorts.AppendElement(inputPort.forget());
+      MOZ_ASSERT(mInputPorts[mInputPorts.Length()-1]);
+
       // Web Audio node has only audio.
       InitEncoder(ContainerWriter::CREATE_AUDIO_TRACK);
     }
   }
 
   void Stop()
   {
     LOG(LogLevel::Debug, ("Session.Stop %p", this));
@@ -477,16 +564,17 @@ public:
     return (mEncoder ?  mEncoder->SizeOfExcludingThis(aMallocSizeOf) : 0);
   }
 
 
 private:
   // Only DestroyRunnable is allowed to delete Session object.
   virtual ~Session()
   {
+    MOZ_COUNT_DTOR(MediaRecorder::Session);
     LOG(LogLevel::Debug, ("Session.~Session (%p)", this));
     CleanupStreams();
   }
   // Pull encoded media data from MediaEncoder and put into EncodedBufferCache.
   // Destroy this session object in the end of this function.
   // If the bool aForceFlush is true, we will force to dispatch a
   // PushBlobRunnable to main thread.
   void Extract(bool aForceFlush)
@@ -540,16 +628,68 @@ private:
       if (NS_FAILED(NS_DispatchToMainThread(new PushBlobRunnable(this)))) {
         MOZ_ASSERT(false, "NS_DispatchToMainThread PushBlobRunnable failed");
       } else {
         mLastBlobTimeStamp = TimeStamp::Now();
       }
     }
   }
 
+  void MediaStreamReady(DOMMediaStream& aStream) {
+    mMediaStream = &aStream;
+    aStream.RegisterTrackListener(this);
+  }
+
+  void ConnectMediaStreamTrack(MediaStreamTrack& aTrack)
+  {
+    mMediaStreamTracks.AppendElement(&aTrack);
+    aTrack.AddPrincipalChangeObserver(this);
+    RefPtr<MediaInputPort> inputPort =
+      aTrack.ForwardTrackContentsTo(mTrackUnionStream);
+    MOZ_ASSERT(inputPort);
+    mInputPorts.AppendElement(inputPort.forget());
+    MOZ_ASSERT(mInputPorts[mInputPorts.Length()-1]);
+  }
+
+  bool PrincipalSubsumes(nsIPrincipal* aPrincipal)
+  {
+    if (!mRecorder->GetOwner())
+      return false;
+    nsCOMPtr<nsIDocument> doc = mRecorder->GetOwner()->GetExtantDoc();
+    if (!doc) {
+      return false;
+    }
+    if (!aPrincipal) {
+      return false;
+    }
+    bool subsumes;
+    if (NS_FAILED(doc->NodePrincipal()->Subsumes(aPrincipal, &subsumes))) {
+      return false;
+    }
+    return subsumes;
+  }
+
+  bool MediaStreamTracksPrincipalSubsumes()
+  {
+    MOZ_ASSERT(mRecorder->mDOMStream);
+    nsCOMPtr<nsIPrincipal> principal = nullptr;
+    for (RefPtr<MediaStreamTrack>& track : mMediaStreamTracks) {
+      nsContentUtils::CombineResourcePrincipals(&principal, track->GetPrincipal());
+    }
+    return PrincipalSubsumes(principal);
+  }
+
+  bool AudioNodePrincipalSubsumes()
+  {
+    MOZ_ASSERT(mRecorder->mAudioNode != nullptr);
+    nsIDocument* doc = mRecorder->mAudioNode->GetOwner()->GetExtantDoc();
+    nsCOMPtr<nsIPrincipal> principal = doc ? doc->NodePrincipal() : nullptr;
+    return PrincipalSubsumes(principal);
+  }
+
   bool CheckPermission(const char* aType)
   {
     nsCOMPtr<nsIDocument> doc = mRecorder->GetOwner()->GetExtantDoc();
     if (!doc) {
       return false;
     }
 
     uint16_t appStatus = nsIPrincipal::APP_STATUS_NOT_INSTALLED;
@@ -657,35 +797,49 @@ private:
       nsCOMPtr<nsIRunnable> runnable =
         NS_NewRunnableMethodWithArg<nsresult>(mRecorder,
                                               &MediaRecorder::NotifyError, rv);
       NS_DispatchToMainThread(runnable);
     }
     if (NS_FAILED(NS_DispatchToMainThread(new EncoderErrorNotifierRunnable(this)))) {
       MOZ_ASSERT(false, "NS_DispatchToMainThread EncoderErrorNotifierRunnable failed");
     }
-    if (NS_FAILED(NS_DispatchToMainThread(new PushBlobRunnable(this)))) {
-      MOZ_ASSERT(false, "NS_DispatchToMainThread PushBlobRunnable failed");
+    if (rv != NS_ERROR_DOM_SECURITY_ERR) {
+      // Don't push a blob if there was a security error.
+      if (NS_FAILED(NS_DispatchToMainThread(new PushBlobRunnable(this)))) {
+        MOZ_ASSERT(false, "NS_DispatchToMainThread PushBlobRunnable failed");
+      }
     }
     if (NS_FAILED(NS_DispatchToMainThread(new DestroyRunnable(this)))) {
       MOZ_ASSERT(false, "NS_DispatchToMainThread DestroyRunnable failed");
     }
     mNeedSessionEndTask = false;
   }
   void CleanupStreams()
   {
-    if (mInputPort.get()) {
-      mInputPort->Destroy();
-      mInputPort = nullptr;
+    for (RefPtr<MediaInputPort>& inputPort : mInputPorts) {
+      MOZ_ASSERT(inputPort);
+      inputPort->Destroy();
     }
+    mInputPorts.Clear();
 
     if (mTrackUnionStream.get()) {
       mTrackUnionStream->Destroy();
       mTrackUnionStream = nullptr;
     }
+
+    if (mMediaStream) {
+      mMediaStream->UnregisterTrackListener(this);
+      mMediaStream = nullptr;
+    }
+
+    for (RefPtr<MediaStreamTrack>& track : mMediaStreamTracks) {
+      track->RemovePrincipalChangeObserver(this);
+    }
+    mMediaStreamTracks.Clear();
   }
 
   NS_IMETHODIMP Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) override
   {
     MOZ_ASSERT(NS_IsMainThread());
     LOG(LogLevel::Debug, ("Session.Observe XPCOM_SHUTDOWN %p", this));
     if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
       // Force stop Session to terminate Read Thread.
@@ -714,17 +868,24 @@ private:
 private:
   // Hold reference to MediaRecoder that ensure MediaRecorder is alive
   // if there is an active session. Access ONLY on main thread.
   RefPtr<MediaRecorder> mRecorder;
 
   // Receive track data from source and dispatch to Encoder.
   // Pause/ Resume controller.
   RefPtr<ProcessedMediaStream> mTrackUnionStream;
-  RefPtr<MediaInputPort> mInputPort;
+  nsTArray<RefPtr<MediaInputPort>> mInputPorts;
+
+  // Stream currently recorded.
+  RefPtr<DOMMediaStream> mMediaStream;
+
+  // Tracks currently recorded. This should be a subset of mMediaStream's track
+  // set.
+  nsTArray<RefPtr<MediaStreamTrack>> mMediaStreamTracks;
 
   // Runnable thread for read data from MediaEncode.
   nsCOMPtr<nsIThread> mReadThread;
   // MediaEncoder pipeline.
   RefPtr<MediaEncoder> mEncoder;
   // A buffer to cache encoded meda data.
   nsAutoPtr<EncodedBufferCache> mEncodedBufferCache;
   // Current session mimeType
@@ -850,27 +1011,16 @@ MediaRecorder::Start(const Optional<int3
     return;
   }
 
   if (GetSourceMediaStream()->IsFinished() || GetSourceMediaStream()->IsDestroyed()) {
     aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
-  // Check if source media stream is valid. See bug 919051.
-  if (mDOMStream && !mDOMStream->GetPrincipal()) {
-    aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
-    return;
-  }
-
-  if (!CheckPrincipal()) {
-    aResult.Throw(NS_ERROR_DOM_SECURITY_ERR);
-    return;
-  }
-
   int32_t timeSlice = 0;
   if (aTimeSlice.WasPassed()) {
     if (aTimeSlice.Value() < 0) {
       aResult.Throw(NS_ERROR_INVALID_ARG);
       return;
     }
 
     timeSlice = aTimeSlice.Value();
@@ -1028,21 +1178,17 @@ MediaRecorder::SetOptions(const MediaRec
     mVideoBitsPerSecond = mBitsPerSecond;
   }
 }
 
 nsresult
 MediaRecorder::CreateAndDispatchBlobEvent(already_AddRefed<nsIDOMBlob>&& aBlob)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
-  if (!CheckPrincipal()) {
-    // Media is not same-origin, don't allow the data out.
-    RefPtr<nsIDOMBlob> blob = aBlob;
-    return NS_ERROR_DOM_SECURITY_ERR;
-  }
+
   BlobEventInit init;
   init.mBubbles = false;
   init.mCancelable = false;
 
   nsCOMPtr<nsIDOMBlob> blob = aBlob;
   init.mData = static_cast<Blob*>(blob.get());
 
   RefPtr<BlobEvent> event =
@@ -1105,39 +1251,16 @@ MediaRecorder::NotifyError(nsresult aRv)
   rv = DispatchDOMEvent(nullptr, event, nullptr, nullptr);
   if (NS_FAILED(rv)) {
     NS_ERROR("Failed to dispatch the error event!!!");
     return;
   }
   return;
 }
 
-bool MediaRecorder::CheckPrincipal()
-{
-  MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
-  if (!mDOMStream && !mAudioNode) {
-    return false;
-  }
-  if (!GetOwner())
-    return false;
-  nsCOMPtr<nsIDocument> doc = GetOwner()->GetExtantDoc();
-  if (!doc) {
-    return false;
-  }
-  nsIPrincipal* srcPrincipal = GetSourcePrincipal();
-  if (!srcPrincipal) {
-    return false;
-  }
-  bool subsumes;
-  if (NS_FAILED(doc->NodePrincipal()->Subsumes(srcPrincipal, &subsumes))) {
-    return false;
-  }
-  return subsumes;
-}
-
 void
 MediaRecorder::RemoveSession(Session* aSession)
 {
   LOG(LogLevel::Debug, ("MediaRecorder.RemoveSession (%p)", aSession));
   mSessions.RemoveElement(aSession);
 }
 
 void
@@ -1163,27 +1286,16 @@ MediaRecorder::GetSourceMediaStream()
 {
   if (mDOMStream != nullptr) {
     return mDOMStream->GetPlaybackStream();
   }
   MOZ_ASSERT(mAudioNode != nullptr);
   return mPipeStream ? mPipeStream.get() : mAudioNode->GetStream();
 }
 
-nsIPrincipal*
-MediaRecorder::GetSourcePrincipal()
-{
-  if (mDOMStream != nullptr) {
-    return mDOMStream->GetPrincipal();
-  }
-  MOZ_ASSERT(mAudioNode != nullptr);
-  nsIDocument* doc = mAudioNode->GetOwner()->GetExtantDoc();
-  return doc ? doc->NodePrincipal() : nullptr;
-}
-
 size_t
 MediaRecorder::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
 {
   size_t amount = 42;
   for (size_t i = 0; i < mSessions.Length(); ++i) {
     amount += mSessions[i]->SizeOfExcludingThis(aMallocSizeOf);
   }
   return amount;
--- a/dom/media/MediaRecorder.h
+++ b/dom/media/MediaRecorder.h
@@ -114,28 +114,25 @@ protected:
 
   MediaRecorder& operator = (const MediaRecorder& x) = delete;
   // Create dataavailable event with Blob data and it runs in main thread
   nsresult CreateAndDispatchBlobEvent(already_AddRefed<nsIDOMBlob>&& aBlob);
   // Creating a simple event to notify UA simple event.
   void DispatchSimpleEvent(const nsAString & aStr);
   // Creating a error event with message.
   void NotifyError(nsresult aRv);
-  // Check if the recorder's principal is the subsume of mediaStream
-  bool CheckPrincipal();
   // Set encoded MIME type.
   void SetMimeType(const nsString &aMimeType);
   void SetOptions(const MediaRecorderOptions& aInitDict);
 
   MediaRecorder(const MediaRecorder& x) = delete; // prevent bad usage
   // Remove session pointer.
   void RemoveSession(Session* aSession);
   // Functions for Session to query input source info.
   MediaStream* GetSourceMediaStream();
-  nsIPrincipal* GetSourcePrincipal();
   // DOM wrapper for source media stream. Will be null when input is audio node.
   RefPtr<DOMMediaStream> mDOMStream;
   // Source audio node. Will be null when input is a media stream.
   RefPtr<AudioNode> mAudioNode;
   // Pipe stream connecting non-destination source node and session track union
   // stream of recorder. Will be null when input is media stream or destination
   // node.
   RefPtr<AudioNodeStream> mPipeStream;