Bug 973522 - MediaRecorder causes large leak. r=roc, r=jsmith, a=1.4+
authorRandy Lin <rlin@mozilla.com>
Wed, 26 Mar 2014 01:11:58 +0800
changeset 226611 d17672d2b6d0cf6cc237e556011c185ba8043d94
parent 226610 45fa4e7d40e6d02f9f8f1d913df72a51b03e5f86
child 226612 b96db54cfaa845a473ba758abf1bef88600b4f63
push id6
push userryanvm@gmail.com
push dateMon, 12 Jan 2015 22:04:06 +0000
treeherdermozilla-b2g37_v2_2@895c8fc7b734 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc, jsmith, 1.4
bugs973522
milestone30.0a2
Bug 973522 - MediaRecorder causes large leak. r=roc, r=jsmith, a=1.4+
content/media/MediaRecorder.cpp
content/media/MediaRecorder.h
content/media/test/mochitest.ini
content/media/test/test_mediarecorder_record_audiocontext_mlk.html
--- a/content/media/MediaRecorder.cpp
+++ b/content/media/MediaRecorder.cpp
@@ -16,22 +16,29 @@
 #include "EncodedBufferCache.h"
 #include "nsIDOMFile.h"
 #include "mozilla/dom/BlobEvent.h"
 
 
 #include "mozilla/dom/AudioStreamTrack.h"
 #include "mozilla/dom/VideoStreamTrack.h"
 
+#ifdef PR_LOGGING
+PRLogModuleInfo* gMediaRecorderLog;
+#define LOG(type, msg) PR_LOG(gMediaRecorderLog, type, msg)
+#else
+#define LOG(type, msg)
+#endif
+
 namespace mozilla {
 
 namespace dom {
 
-NS_IMPL_CYCLE_COLLECTION_INHERITED_2(MediaRecorder, nsDOMEventTargetHelper,
-                                     mStream, mSession)
+NS_IMPL_CYCLE_COLLECTION_INHERITED_1(MediaRecorder, nsDOMEventTargetHelper,
+                                     mStream)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaRecorder)
 NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper)
 
 NS_IMPL_ADDREF_INHERITED(MediaRecorder, nsDOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(MediaRecorder, nsDOMEventTargetHelper)
 
 /**
@@ -75,26 +82,32 @@ class MediaRecorder::Session: public nsI
   {
   public:
     PushBlobRunnable(Session* aSession)
       : mSession(aSession)
     { }
 
     NS_IMETHODIMP Run()
     {
+      LOG(PR_LOG_DEBUG, ("Session.PushBlobRunnable s=(%p)", mSession.get()));
       MOZ_ASSERT(NS_IsMainThread());
 
-      MediaRecorder *recorder = mSession->mRecorder;
+      nsRefPtr<MediaRecorder> recorder = mSession->mRecorder;
+      if (!recorder) {
+	 return NS_OK;
+      }
+      recorder->SetMimeType(mSession->mMimeType);
       if (mSession->IsEncoderError()) {
         recorder->NotifyError(NS_ERROR_UNEXPECTED);
       }
       nsresult rv = recorder->CreateAndDispatchBlobEvent(mSession->GetEncodedData());
       if (NS_FAILED(rv)) {
         recorder->NotifyError(rv);
       }
+
       return NS_OK;
     }
 
   private:
     nsRefPtr<Session> mSession;
   };
 
   // Record thread task and it run in Media Encoder thread.
@@ -105,29 +118,30 @@ class MediaRecorder::Session: public nsI
     ExtractRunnable(Session *aSession)
       : mSession(aSession) {}
 
     NS_IMETHODIMP Run()
     {
       MOZ_ASSERT(NS_GetCurrentThread() == mSession->mReadThread);
 
       mSession->Extract();
+      LOG(PR_LOG_DEBUG, ("Session.ExtractRunnable shutdown = %d", mSession->mEncoder->IsShutdown()));
       if (!mSession->mEncoder->IsShutdown()) {
         NS_DispatchToCurrentThread(new ExtractRunnable(mSession));
       } else {
         // Flush out remainding encoded data.
         NS_DispatchToMainThread(new PushBlobRunnable(mSession));
         // Destroy this Session object in main thread.
         NS_DispatchToMainThread(new DestroyRunnable(already_AddRefed<Session>(mSession)));
       }
       return NS_OK;
     }
 
   private:
-    Session* mSession;
+    nsRefPtr<Session> mSession;
   };
 
   // For Ensure recorder has tracks to record.
   class TracksAvailableCallback : public DOMMediaStream::OnTracksAvailableCallback
   {
   public:
     TracksAvailableCallback(Session *aSession)
      : mSession(aSession) {}
@@ -143,53 +157,59 @@ class MediaRecorder::Session: public nsI
         // What is inside the track
         if (videoTracks.Length() > 0) {
           trackType |= DOMMediaStream::HINT_CONTENTS_VIDEO;
         }
         if (audioTracks.Length() > 0) {
           trackType |= DOMMediaStream::HINT_CONTENTS_AUDIO;
         }
       }
+      LOG(PR_LOG_DEBUG, ("Session.NotifyTracksAvailable track type = (%d)", trackType));
       mSession->AfterTracksAdded(trackType);
     }
   private:
     nsRefPtr<Session> mSession;
   };
-
   // Main thread task.
   // To delete RecordingSession object.
   class DestroyRunnable : public nsRunnable
   {
   public:
     DestroyRunnable(already_AddRefed<Session>&& aSession)
       : mSession(aSession) {}
 
     NS_IMETHODIMP Run()
     {
+      LOG(PR_LOG_DEBUG, ("Session.DestroyRunnable session refcnt = (%d) stopIssued %d s=(%p)",
+                         (int)mSession->mRefCnt, mSession->mStopIssued, mSession.get()));
       MOZ_ASSERT(NS_IsMainThread() && mSession.get());
-      MediaRecorder *recorder = mSession->mRecorder;
-
+      nsRefPtr<MediaRecorder> recorder = mSession->mRecorder;
+      if (!recorder) {
+        return NS_OK;
+      }
       // SourceMediaStream is ended, and send out TRACK_EVENT_END notification.
       // Read Thread will be terminate soon.
       // We need to switch MediaRecorder to "Stop" state first to make sure
       // MediaRecorder is not associated with this Session anymore, then, it's
       // safe to delete this Session.
       // Also avoid to run if this session already call stop before
       if (!mSession->mStopIssued) {
         ErrorResult result;
+        mSession->mStopIssued = true;
         recorder->Stop(result);
         NS_DispatchToMainThread(new DestroyRunnable(mSession.forget()));
-
         return NS_OK;
       }
 
       // Dispatch stop event and clear MIME type.
-      recorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop"));
       mSession->mMimeType = NS_LITERAL_STRING("");
       recorder->SetMimeType(mSession->mMimeType);
+      recorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop"));
+      recorder->RemoveSession(mSession);
+      mSession->mRecorder = nullptr;
       return NS_OK;
     }
 
   private:
     // Call mSession::Release automatically while DestroyRunnable be destroy.
     nsRefPtr<Session> mSession;
   };
 
@@ -209,82 +229,91 @@ public:
     AddRef();
     mEncodedBufferCache = new EncodedBufferCache(MAX_ALLOW_MEMORY_BUFFER);
     mLastBlobTimeStamp = TimeStamp::Now();
   }
 
   // Only DestroyRunnable is allowed to delete Session object.
   virtual ~Session()
   {
+    LOG(PR_LOG_DEBUG, ("Session.~Session (%p)", this));
     CleanupStreams();
   }
 
   void Start()
   {
+    LOG(PR_LOG_DEBUG, ("Session.Start %p", this));
     MOZ_ASSERT(NS_IsMainThread());
 
     SetupStreams();
   }
 
   void Stop()
   {
+    LOG(PR_LOG_DEBUG, ("Session.Stop %p", this));
     MOZ_ASSERT(NS_IsMainThread());
-
     mStopIssued = true;
     CleanupStreams();
     nsContentUtils::UnregisterShutdownObserver(this);
   }
 
   nsresult Pause()
   {
+    LOG(PR_LOG_DEBUG, ("Session.Pause"));
     MOZ_ASSERT(NS_IsMainThread());
 
     NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE);
     mTrackUnionStream->ChangeExplicitBlockerCount(-1);
 
     return NS_OK;
   }
 
   nsresult Resume()
   {
+    LOG(PR_LOG_DEBUG, ("Session.Resume"));
     MOZ_ASSERT(NS_IsMainThread());
 
     NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE);
     mTrackUnionStream->ChangeExplicitBlockerCount(1);
 
     return NS_OK;
   }
 
   already_AddRefed<nsIDOMBlob> GetEncodedData()
   {
+    MOZ_ASSERT(NS_IsMainThread());
     return mEncodedBufferCache->ExtractBlob(mMimeType);
   }
 
   bool IsEncoderError()
   {
     if (mEncoder && mEncoder->HasError()) {
       return true;
     }
     return false;
   }
+  void ForgetMediaRecorder()
+  {
+    LOG(PR_LOG_DEBUG, ("Session.ForgetMediaRecorder (%p)", mRecorder));
+    mRecorder = nullptr;
+  }
 private:
 
   // Pull encoded meida data from MediaEncoder and put into EncodedBufferCache.
   // Destroy this session object in the end of this function.
   void Extract()
   {
     MOZ_ASSERT(NS_GetCurrentThread() == mReadThread);
-
+    LOG(PR_LOG_DEBUG, ("Session.Extract %p", this));
     // Whether push encoded data back to onDataAvailable automatically.
     const bool pushBlob = (mTimeSlice > 0) ? true : false;
 
     // Pull encoded media data from MediaEncoder
     nsTArray<nsTArray<uint8_t> > encodedBuf;
     mEncoder->GetEncodedData(&encodedBuf, mMimeType);
-    mRecorder->SetMimeType(mMimeType);
 
     // Append pulled data into cache buffer.
     for (uint32_t i = 0; i < encodedBuf.Length(); i++) {
       mEncodedBufferCache->AppendBuffer(encodedBuf[i]);
     }
 
     if (pushBlob) {
       if ((TimeStamp::Now() - mLastBlobTimeStamp).ToMilliseconds() > mTimeSlice) {
@@ -305,22 +334,23 @@ private:
     MOZ_ASSERT(mTrackUnionStream, "CreateTrackUnionStream failed");
 
     mTrackUnionStream->SetAutofinish(true);
 
     // Bind this Track Union Stream with Source Media
     mInputPort = mTrackUnionStream->AllocateInputPort(mRecorder->mStream->GetStream(), MediaInputPort::FLAG_BLOCK_OUTPUT);
 
     // Allocate encoder and bind with the Track Union Stream.
-    TracksAvailableCallback* tracksAvailableCallback = new TracksAvailableCallback(mRecorder->mSession);
+    TracksAvailableCallback* tracksAvailableCallback = new TracksAvailableCallback(mRecorder->mSessions.LastElement());
     mRecorder->mStream->OnTracksAvailable(tracksAvailableCallback);
   }
 
   void AfterTracksAdded(uint8_t aTrackTypes)
   {
+    LOG(PR_LOG_DEBUG, ("Session.AfterTracksAdded %p", this));
     MOZ_ASSERT(NS_IsMainThread());
 
     // Allocate encoder and bind with union stream.
     // At this stage, the API doesn't allow UA to choose the output mimeType format.
     mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(""), aTrackTypes);
 
     if (!mEncoder) {
       DoSessionEndTask(NS_ERROR_ABORT);
@@ -349,20 +379,20 @@ private:
     nsContentUtils::RegisterShutdownObserver(this);
 
     mReadThread->Dispatch(new ExtractRunnable(this), NS_DISPATCH_NORMAL);
   }
   // application should get blob and onstop event
   void DoSessionEndTask(nsresult rv)
   {
     MOZ_ASSERT(NS_IsMainThread());
-
     if (NS_FAILED(rv)) {
       mRecorder->NotifyError(rv);
     }
+
     CleanupStreams();
     // Destroy this session object in main thread.
     NS_DispatchToMainThread(new PushBlobRunnable(this));
     NS_DispatchToMainThread(new DestroyRunnable(already_AddRefed<Session>(this)));
   }
   void CleanupStreams()
   {
     if (mInputPort.get()) {
@@ -374,29 +404,33 @@ private:
       mTrackUnionStream->Destroy();
       mTrackUnionStream = nullptr;
     }
   }
 
   NS_IMETHODIMP Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData)
   {
     MOZ_ASSERT(NS_IsMainThread());
-
+    LOG(PR_LOG_DEBUG, ("Session.Observe XPCOM_SHUTDOWN %p", this));
     if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
       // Force stop Session to terminate Read Thread.
+      mEncoder->Cancel();
+      if (mRecorder) {
+        mRecorder->RemoveSession(this);
+        mRecorder = nullptr;
+      }
       Stop();
     }
 
     return NS_OK;
   }
 
 private:
-  // Hold a reference to MediaRecoder to make sure MediaRecoder be
-  // destroyed after all session object dead.
-  nsRefPtr<MediaRecorder> mRecorder;
+  // Hold weak a reference to MediaRecoder and can be accessed ONLY on main thread.
+  MediaRecorder* mRecorder;
 
   // Receive track data from source and dispatch to Encoder.
   // Pause/ Resume controller.
   nsRefPtr<ProcessedMediaStream> mTrackUnionStream;
   nsRefPtr<MediaInputPort> mInputPort;
 
   // Runnable thread for read data from MediaEncode.
   nsCOMPtr<nsIThread> mReadThread;
@@ -416,28 +450,38 @@ private:
   // Indicate this session's stop has been called.
   bool mStopIssued;
 };
 
 NS_IMPL_ISUPPORTS1(MediaRecorder::Session, nsIObserver)
 
 MediaRecorder::~MediaRecorder()
 {
-  MOZ_ASSERT(mSession == nullptr);
+  LOG(PR_LOG_DEBUG, ("~MediaRecorder (%p)", this));
+  for (uint32_t i = 0; i < mSessions.Length(); i ++) {
+    if (mSessions[i]) {
+      mSessions[i]->ForgetMediaRecorder();
+      mSessions[i]->Stop();
+    }
+  }
 }
 
 MediaRecorder::MediaRecorder(DOMMediaStream& aStream, nsPIDOMWindow* aOwnerWindow)
   : nsDOMEventTargetHelper(aOwnerWindow),
     mState(RecordingState::Inactive),
-    mSession(nullptr),
     mMutex("Session.Data.Mutex")
 {
   MOZ_ASSERT(aOwnerWindow);
   MOZ_ASSERT(aOwnerWindow->IsInnerWindow());
   mStream = &aStream;
+#ifdef PR_LOGGING
+  if (!gMediaRecorderLog) {
+    gMediaRecorderLog = PR_NewLogModule("MediaRecorder");
+  }
+#endif
 }
 
 void
 MediaRecorder::SetMimeType(const nsString &aMimeType)
 {
   MutexAutoLock lock(mMutex);
   mMimeType = aMimeType;
 }
@@ -447,16 +491,17 @@ MediaRecorder::GetMimeType(nsString &aMi
 {
   MutexAutoLock lock(mMutex);
   aMimeType = mMimeType;
 }
 
 void
 MediaRecorder::Start(const Optional<int32_t>& aTimeSlice, ErrorResult& aResult)
 {
+  LOG(PR_LOG_DEBUG, ("MediaRecorder.Start %p", this));
   if (mState != RecordingState::Inactive) {
     aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
   if (mStream->GetStream()->IsFinished() || mStream->GetStream()->IsDestroyed()) {
     aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
@@ -479,69 +524,70 @@ MediaRecorder::Start(const Optional<int3
       return;
     }
 
     timeSlice = aTimeSlice.Value();
   }
 
   mState = RecordingState::Recording;
   // Start a session
-  mSession = new Session(this, timeSlice);
-  mSession->Start();
+
+  mSessions.AppendElement();
+  mSessions.LastElement() = new Session(this, timeSlice);
+  mSessions.LastElement()->Start();
 }
 
 void
 MediaRecorder::Stop(ErrorResult& aResult)
 {
+  LOG(PR_LOG_DEBUG, ("MediaRecorder.Stop %p", this));
   if (mState == RecordingState::Inactive) {
     aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
   mState = RecordingState::Inactive;
-
-  mSession->Stop();
-  mSession = nullptr;
+  if (mSessions.Length() > 0) {
+    mSessions.LastElement()->Stop();
+  }
 }
 
 void
 MediaRecorder::Pause(ErrorResult& aResult)
 {
+  LOG(PR_LOG_DEBUG, ("MediaRecorder.Pause"));
   if (mState != RecordingState::Recording) {
     aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
-  MOZ_ASSERT(mSession != nullptr);
-  if (mSession) {
-    nsresult rv = mSession->Pause();
-    if (NS_FAILED(rv)) {
-      NotifyError(rv);
-      return;
-    }
-    mState = RecordingState::Paused;
+  MOZ_ASSERT(mSessions.Length() > 0);
+  nsresult rv = mSessions.LastElement()->Pause();
+  if (NS_FAILED(rv)) {
+    NotifyError(rv);
+    return;
   }
+  mState = RecordingState::Paused;
 }
 
 void
 MediaRecorder::Resume(ErrorResult& aResult)
 {
+  LOG(PR_LOG_DEBUG, ("MediaRecorder.Resume"));
   if (mState != RecordingState::Paused) {
     aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
-  MOZ_ASSERT(mSession != nullptr);
-  if (mSession) {
-    nsresult rv = mSession->Resume();
-    if (NS_FAILED(rv)) {
-      NotifyError(rv);
-      return;
-    }
-    mState = RecordingState::Recording;
+  MOZ_ASSERT(mSessions.Length() > 0);
+  nsresult rv = mSessions.LastElement()->Resume();
+  if (NS_FAILED(rv)) {
+    NotifyError(rv);
+    return;
   }
+  mState = RecordingState::Recording;
 }
 
 class CreateAndDispatchBlobEventRunnable : public nsRunnable {
   nsCOMPtr<nsIDOMBlob> mBlob;
   nsRefPtr<MediaRecorder> mRecorder;
 public:
   CreateAndDispatchBlobEventRunnable(already_AddRefed<nsIDOMBlob>&& aBlob,
                                      MediaRecorder* aRecorder)
@@ -562,18 +608,19 @@ void
 MediaRecorder::RequestData(ErrorResult& aResult)
 {
   if (mState != RecordingState::Recording) {
     aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
   NS_DispatchToMainThread(
-    new CreateAndDispatchBlobEventRunnable(mSession->GetEncodedData(), this),
-    NS_DISPATCH_NORMAL);
+    new CreateAndDispatchBlobEventRunnable(mSessions.LastElement()->GetEncodedData(),
+                                           this),
+                                           NS_DISPATCH_NORMAL);
 }
 
 JSObject*
 MediaRecorder::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
 {
   return MediaRecorderBinding::Wrap(aCx, aScope, this);
 }
 
@@ -596,22 +643,21 @@ MediaRecorder::Constructor(const GlobalO
   nsRefPtr<MediaRecorder> object = new MediaRecorder(aStream, ownerWindow);
   return object.forget();
 }
 
 nsresult
 MediaRecorder::CreateAndDispatchBlobEvent(already_AddRefed<nsIDOMBlob>&& aBlob)
 {
   NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
-
   if (!CheckPrincipal()) {
     // Media is not same-origin, don't allow the data out.
+    nsRefPtr<nsIDOMBlob> blob = aBlob;
     return NS_ERROR_DOM_SECURITY_ERR;
   }
-
   BlobEventInit init;
   init.mBubbles = false;
   init.mCancelable = false;
   init.mData = aBlob;
   nsRefPtr<BlobEvent> event =
     BlobEvent::Constructor(this,
                            NS_LITERAL_STRING("dataavailable"),
                            init);
@@ -683,24 +729,35 @@ MediaRecorder::NotifyError(nsresult aRv)
     NS_ERROR("Failed to dispatch the error event!!!");
     return;
   }
   return;
 }
 
 bool MediaRecorder::CheckPrincipal()
 {
+  NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
+  if (!mStream) {
+    return false;
+  }
   nsCOMPtr<nsIPrincipal> principal = mStream->GetPrincipal();
   if (!GetOwner())
     return false;
   nsCOMPtr<nsIDocument> doc = GetOwner()->GetExtantDoc();
   if (!doc || !principal)
     return false;
 
   bool subsumes;
   if (NS_FAILED(doc->NodePrincipal()->Subsumes(principal, &subsumes)))
     return false;
 
   return subsumes;
 }
 
+void
+MediaRecorder::RemoveSession(Session* aSession)
+{
+  LOG(PR_LOG_DEBUG, ("MediaRecorder.RemoveSession (%p)", aSession));
+  mSessions.RemoveElement(aSession);
+}
+
 }
 }
--- a/content/media/MediaRecorder.h
+++ b/content/media/MediaRecorder.h
@@ -92,23 +92,24 @@ protected:
   // 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);
 
   MediaRecorder(const MediaRecorder& x) MOZ_DELETE; // prevent bad usage
-
+  // Remove session pointer.
+  void RemoveSession(Session* aSession);
   // MediaStream passed from js context
   nsRefPtr<DOMMediaStream> mStream;
   // The current state of the MediaRecorder object.
   RecordingState mState;
-  // Current recording session.
-  nsRefPtr<Session> mSession;
+  // Hold the sessions pointer in media recorder and clean in the destructor of recorder.
+  nsTArray<Session*> mSessions;
   // Thread safe for mMimeType.
   Mutex mMutex;
   // It specifies the container format as well as the audio and video capture formats.
   nsString mMimeType;
 };
 
 }
 }
--- a/content/media/test/mochitest.ini
+++ b/content/media/test/mochitest.ini
@@ -397,16 +397,17 @@ skip-if = (buildapp == 'b2g' && (toolkit
 [test_volume.html]
 [test_video_to_canvas.html]
 [test_audiowrite.html]
 [test_mediarecorder_creation.html]
 [test_mediarecorder_creation_fail.html]
 [test_mediarecorder_avoid_recursion.html]
 [test_mediarecorder_record_timeslice.html]
 [test_mediarecorder_record_audiocontext.html]
+[test_mediarecorder_record_audiocontext_mlk.html]
 [test_mediarecorder_record_4ch_audiocontext.html]
 skip-if = (toolkit == 'gonk' && !debug)
 [test_mediarecorder_record_stopms.html]
 [test_mediarecorder_record_nosrc.html]
 [test_mozHasAudio.html]
 skip-if = (buildapp == 'b2g' && (toolkit != 'gonk' || debug))
 [test_source_media.html]
 skip-if = (buildapp == 'b2g' && (toolkit != 'gonk' || debug))
new file mode 100644
--- /dev/null
+++ b/content/media/test/test_mediarecorder_record_audiocontext_mlk.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>capture for possible memory leak when record AudioContext</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=973765">Mozill
+a Bug 973765</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+   // This test case want to capture the memory leak if exit the browser after running those script.
+   var ac = new window.AudioContext();
+   var destStream = ac.createMediaStreamDestination().stream;
+   var recorder = new MediaRecorder(destStream);
+   recorder.start(1000);
+   is(recorder.state, 'recording', 'Media recorder should be recording');
+   is(recorder.stream, destStream,
+      'Media recorder stream = element stream at the start of recording');
+</script>
+</pre>
+</body>
+</html>