Bug 973522 MediaRecorder causes large leak. r=roc, jsmith

This commit is contained in:
Randy Lin 2014-03-26 01:11:58 +08:00
parent 926fd8dee8
commit 05878d4b12
4 changed files with 131 additions and 48 deletions

View File

@ -22,12 +22,19 @@
#include "mozilla/dom/AudioStreamTrack.h" #include "mozilla/dom/AudioStreamTrack.h"
#include "mozilla/dom/VideoStreamTrack.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 mozilla {
namespace dom { namespace dom {
NS_IMPL_CYCLE_COLLECTION_INHERITED_2(MediaRecorder, nsDOMEventTargetHelper, NS_IMPL_CYCLE_COLLECTION_INHERITED_1(MediaRecorder, nsDOMEventTargetHelper,
mStream, mSession) mStream)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaRecorder) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaRecorder)
NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper) NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper)
@ -81,9 +88,14 @@ class MediaRecorder::Session: public nsIObserver
NS_IMETHODIMP Run() NS_IMETHODIMP Run()
{ {
LOG(PR_LOG_DEBUG, ("Session.PushBlobRunnable s=(%p)", mSession.get()));
MOZ_ASSERT(NS_IsMainThread()); 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()) { if (mSession->IsEncoderError()) {
recorder->NotifyError(NS_ERROR_UNEXPECTED); recorder->NotifyError(NS_ERROR_UNEXPECTED);
} }
@ -91,6 +103,7 @@ class MediaRecorder::Session: public nsIObserver
if (NS_FAILED(rv)) { if (NS_FAILED(rv)) {
recorder->NotifyError(rv); recorder->NotifyError(rv);
} }
return NS_OK; return NS_OK;
} }
@ -111,6 +124,7 @@ class MediaRecorder::Session: public nsIObserver
MOZ_ASSERT(NS_GetCurrentThread() == mSession->mReadThread); MOZ_ASSERT(NS_GetCurrentThread() == mSession->mReadThread);
mSession->Extract(); mSession->Extract();
LOG(PR_LOG_DEBUG, ("Session.ExtractRunnable shutdown = %d", mSession->mEncoder->IsShutdown()));
if (!mSession->mEncoder->IsShutdown()) { if (!mSession->mEncoder->IsShutdown()) {
NS_DispatchToCurrentThread(new ExtractRunnable(mSession)); NS_DispatchToCurrentThread(new ExtractRunnable(mSession));
} else { } else {
@ -123,7 +137,7 @@ class MediaRecorder::Session: public nsIObserver
} }
private: private:
Session* mSession; nsRefPtr<Session> mSession;
}; };
// For Ensure recorder has tracks to record. // For Ensure recorder has tracks to record.
@ -149,12 +163,12 @@ class MediaRecorder::Session: public nsIObserver
trackType |= DOMMediaStream::HINT_CONTENTS_AUDIO; trackType |= DOMMediaStream::HINT_CONTENTS_AUDIO;
} }
} }
LOG(PR_LOG_DEBUG, ("Session.NotifyTracksAvailable track type = (%d)", trackType));
mSession->AfterTracksAdded(trackType); mSession->AfterTracksAdded(trackType);
} }
private: private:
nsRefPtr<Session> mSession; nsRefPtr<Session> mSession;
}; };
// Main thread task. // Main thread task.
// To delete RecordingSession object. // To delete RecordingSession object.
class DestroyRunnable : public nsRunnable class DestroyRunnable : public nsRunnable
@ -165,9 +179,13 @@ class MediaRecorder::Session: public nsIObserver
NS_IMETHODIMP Run() 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()); 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. // SourceMediaStream is ended, and send out TRACK_EVENT_END notification.
// Read Thread will be terminate soon. // Read Thread will be terminate soon.
// We need to switch MediaRecorder to "Stop" state first to make sure // We need to switch MediaRecorder to "Stop" state first to make sure
@ -176,16 +194,18 @@ class MediaRecorder::Session: public nsIObserver
// Also avoid to run if this session already call stop before // Also avoid to run if this session already call stop before
if (!mSession->mStopIssued) { if (!mSession->mStopIssued) {
ErrorResult result; ErrorResult result;
mSession->mStopIssued = true;
recorder->Stop(result); recorder->Stop(result);
NS_DispatchToMainThread(new DestroyRunnable(mSession.forget())); NS_DispatchToMainThread(new DestroyRunnable(mSession.forget()));
return NS_OK; return NS_OK;
} }
// Dispatch stop event and clear MIME type. // Dispatch stop event and clear MIME type.
recorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop"));
mSession->mMimeType = NS_LITERAL_STRING(""); mSession->mMimeType = NS_LITERAL_STRING("");
recorder->SetMimeType(mSession->mMimeType); recorder->SetMimeType(mSession->mMimeType);
recorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop"));
recorder->RemoveSession(mSession);
mSession->mRecorder = nullptr;
return NS_OK; return NS_OK;
} }
@ -215,11 +235,13 @@ public:
// Only DestroyRunnable is allowed to delete Session object. // Only DestroyRunnable is allowed to delete Session object.
virtual ~Session() virtual ~Session()
{ {
LOG(PR_LOG_DEBUG, ("Session.~Session (%p)", this));
CleanupStreams(); CleanupStreams();
} }
void Start() void Start()
{ {
LOG(PR_LOG_DEBUG, ("Session.Start %p", this));
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
SetupStreams(); SetupStreams();
@ -227,8 +249,8 @@ public:
void Stop() void Stop()
{ {
LOG(PR_LOG_DEBUG, ("Session.Stop %p", this));
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
mStopIssued = true; mStopIssued = true;
CleanupStreams(); CleanupStreams();
nsContentUtils::UnregisterShutdownObserver(this); nsContentUtils::UnregisterShutdownObserver(this);
@ -236,6 +258,7 @@ public:
nsresult Pause() nsresult Pause()
{ {
LOG(PR_LOG_DEBUG, ("Session.Pause"));
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE); NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE);
@ -246,6 +269,7 @@ public:
nsresult Resume() nsresult Resume()
{ {
LOG(PR_LOG_DEBUG, ("Session.Resume"));
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE); NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE);
@ -256,6 +280,7 @@ public:
already_AddRefed<nsIDOMBlob> GetEncodedData() already_AddRefed<nsIDOMBlob> GetEncodedData()
{ {
MOZ_ASSERT(NS_IsMainThread());
return mEncodedBufferCache->ExtractBlob(mMimeType); return mEncodedBufferCache->ExtractBlob(mMimeType);
} }
@ -266,6 +291,11 @@ public:
} }
return false; return false;
} }
void ForgetMediaRecorder()
{
LOG(PR_LOG_DEBUG, ("Session.ForgetMediaRecorder (%p)", mRecorder));
mRecorder = nullptr;
}
private: private:
// Pull encoded meida data from MediaEncoder and put into EncodedBufferCache. // Pull encoded meida data from MediaEncoder and put into EncodedBufferCache.
@ -273,14 +303,13 @@ private:
void Extract() void Extract()
{ {
MOZ_ASSERT(NS_GetCurrentThread() == mReadThread); MOZ_ASSERT(NS_GetCurrentThread() == mReadThread);
LOG(PR_LOG_DEBUG, ("Session.Extract %p", this));
// Whether push encoded data back to onDataAvailable automatically. // Whether push encoded data back to onDataAvailable automatically.
const bool pushBlob = (mTimeSlice > 0) ? true : false; const bool pushBlob = (mTimeSlice > 0) ? true : false;
// Pull encoded media data from MediaEncoder // Pull encoded media data from MediaEncoder
nsTArray<nsTArray<uint8_t> > encodedBuf; nsTArray<nsTArray<uint8_t> > encodedBuf;
mEncoder->GetEncodedData(&encodedBuf, mMimeType); mEncoder->GetEncodedData(&encodedBuf, mMimeType);
mRecorder->SetMimeType(mMimeType);
// Append pulled data into cache buffer. // Append pulled data into cache buffer.
for (uint32_t i = 0; i < encodedBuf.Length(); i++) { for (uint32_t i = 0; i < encodedBuf.Length(); i++) {
@ -311,12 +340,13 @@ private:
mInputPort = mTrackUnionStream->AllocateInputPort(mRecorder->mStream->GetStream(), MediaInputPort::FLAG_BLOCK_OUTPUT); mInputPort = mTrackUnionStream->AllocateInputPort(mRecorder->mStream->GetStream(), MediaInputPort::FLAG_BLOCK_OUTPUT);
// Allocate encoder and bind with the Track Union Stream. // 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); mRecorder->mStream->OnTracksAvailable(tracksAvailableCallback);
} }
void AfterTracksAdded(uint8_t aTrackTypes) void AfterTracksAdded(uint8_t aTrackTypes)
{ {
LOG(PR_LOG_DEBUG, ("Session.AfterTracksAdded %p", this));
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
// Allocate encoder and bind with union stream. // Allocate encoder and bind with union stream.
@ -367,10 +397,10 @@ private:
void DoSessionEndTask(nsresult rv) void DoSessionEndTask(nsresult rv)
{ {
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
if (NS_FAILED(rv)) { if (NS_FAILED(rv)) {
mRecorder->NotifyError(rv); mRecorder->NotifyError(rv);
} }
CleanupStreams(); CleanupStreams();
// Destroy this session object in main thread. // Destroy this session object in main thread.
NS_DispatchToMainThread(new PushBlobRunnable(this)); NS_DispatchToMainThread(new PushBlobRunnable(this));
@ -392,9 +422,14 @@ private:
NS_IMETHODIMP Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) NS_IMETHODIMP Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData)
{ {
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
LOG(PR_LOG_DEBUG, ("Session.Observe XPCOM_SHUTDOWN %p", this));
if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
// Force stop Session to terminate Read Thread. // Force stop Session to terminate Read Thread.
mEncoder->Cancel();
if (mRecorder) {
mRecorder->RemoveSession(this);
mRecorder = nullptr;
}
Stop(); Stop();
} }
@ -402,9 +437,8 @@ private:
} }
private: private:
// Hold a reference to MediaRecoder to make sure MediaRecoder be // Hold weak a reference to MediaRecoder and can be accessed ONLY on main thread.
// destroyed after all session object dead. MediaRecorder* mRecorder;
nsRefPtr<MediaRecorder> mRecorder;
// Receive track data from source and dispatch to Encoder. // Receive track data from source and dispatch to Encoder.
// Pause/ Resume controller. // Pause/ Resume controller.
@ -434,18 +468,28 @@ NS_IMPL_ISUPPORTS1(MediaRecorder::Session, nsIObserver)
MediaRecorder::~MediaRecorder() 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) MediaRecorder::MediaRecorder(DOMMediaStream& aStream, nsPIDOMWindow* aOwnerWindow)
: nsDOMEventTargetHelper(aOwnerWindow), : nsDOMEventTargetHelper(aOwnerWindow),
mState(RecordingState::Inactive), mState(RecordingState::Inactive),
mSession(nullptr),
mMutex("Session.Data.Mutex") mMutex("Session.Data.Mutex")
{ {
MOZ_ASSERT(aOwnerWindow); MOZ_ASSERT(aOwnerWindow);
MOZ_ASSERT(aOwnerWindow->IsInnerWindow()); MOZ_ASSERT(aOwnerWindow->IsInnerWindow());
mStream = &aStream; mStream = &aStream;
#ifdef PR_LOGGING
if (!gMediaRecorderLog) {
gMediaRecorderLog = PR_NewLogModule("MediaRecorder");
}
#endif
} }
void void
@ -465,6 +509,7 @@ MediaRecorder::GetMimeType(nsString &aMimeType)
void void
MediaRecorder::Start(const Optional<int32_t>& aTimeSlice, ErrorResult& aResult) MediaRecorder::Start(const Optional<int32_t>& aTimeSlice, ErrorResult& aResult)
{ {
LOG(PR_LOG_DEBUG, ("MediaRecorder.Start %p", this));
if (mState != RecordingState::Inactive) { if (mState != RecordingState::Inactive) {
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return; return;
@ -497,59 +542,60 @@ MediaRecorder::Start(const Optional<int32_t>& aTimeSlice, ErrorResult& aResult)
mState = RecordingState::Recording; mState = RecordingState::Recording;
// Start a session // Start a session
mSession = new Session(this, timeSlice);
mSession->Start(); mSessions.AppendElement();
mSessions.LastElement() = new Session(this, timeSlice);
mSessions.LastElement()->Start();
} }
void void
MediaRecorder::Stop(ErrorResult& aResult) MediaRecorder::Stop(ErrorResult& aResult)
{ {
LOG(PR_LOG_DEBUG, ("MediaRecorder.Stop %p", this));
if (mState == RecordingState::Inactive) { if (mState == RecordingState::Inactive) {
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return; return;
} }
mState = RecordingState::Inactive; mState = RecordingState::Inactive;
if (mSessions.Length() > 0) {
mSession->Stop(); mSessions.LastElement()->Stop();
mSession = nullptr; }
} }
void void
MediaRecorder::Pause(ErrorResult& aResult) MediaRecorder::Pause(ErrorResult& aResult)
{ {
LOG(PR_LOG_DEBUG, ("MediaRecorder.Pause"));
if (mState != RecordingState::Recording) { if (mState != RecordingState::Recording) {
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return; return;
} }
MOZ_ASSERT(mSession != nullptr); MOZ_ASSERT(mSessions.Length() > 0);
if (mSession) { nsresult rv = mSessions.LastElement()->Pause();
nsresult rv = mSession->Pause(); if (NS_FAILED(rv)) {
if (NS_FAILED(rv)) { NotifyError(rv);
NotifyError(rv); return;
return;
}
mState = RecordingState::Paused;
} }
mState = RecordingState::Paused;
} }
void void
MediaRecorder::Resume(ErrorResult& aResult) MediaRecorder::Resume(ErrorResult& aResult)
{ {
LOG(PR_LOG_DEBUG, ("MediaRecorder.Resume"));
if (mState != RecordingState::Paused) { if (mState != RecordingState::Paused) {
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return; return;
} }
MOZ_ASSERT(mSession != nullptr); MOZ_ASSERT(mSessions.Length() > 0);
if (mSession) { nsresult rv = mSessions.LastElement()->Resume();
nsresult rv = mSession->Resume(); if (NS_FAILED(rv)) {
if (NS_FAILED(rv)) { NotifyError(rv);
NotifyError(rv); return;
return;
}
mState = RecordingState::Recording;
} }
mState = RecordingState::Recording;
} }
class CreateAndDispatchBlobEventRunnable : public nsRunnable { class CreateAndDispatchBlobEventRunnable : public nsRunnable {
@ -580,8 +626,9 @@ MediaRecorder::RequestData(ErrorResult& aResult)
} }
NS_DispatchToMainThread( NS_DispatchToMainThread(
new CreateAndDispatchBlobEventRunnable(mSession->GetEncodedData(), this), new CreateAndDispatchBlobEventRunnable(mSessions.LastElement()->GetEncodedData(),
NS_DISPATCH_NORMAL); this),
NS_DISPATCH_NORMAL);
} }
JSObject* JSObject*
@ -617,12 +664,11 @@ nsresult
MediaRecorder::CreateAndDispatchBlobEvent(already_AddRefed<nsIDOMBlob>&& aBlob) MediaRecorder::CreateAndDispatchBlobEvent(already_AddRefed<nsIDOMBlob>&& aBlob)
{ {
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread"); NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
if (!CheckPrincipal()) { if (!CheckPrincipal()) {
// Media is not same-origin, don't allow the data out. // Media is not same-origin, don't allow the data out.
nsRefPtr<nsIDOMBlob> blob = aBlob;
return NS_ERROR_DOM_SECURITY_ERR; return NS_ERROR_DOM_SECURITY_ERR;
} }
BlobEventInit init; BlobEventInit init;
init.mBubbles = false; init.mBubbles = false;
init.mCancelable = false; init.mCancelable = false;
@ -704,6 +750,10 @@ MediaRecorder::NotifyError(nsresult aRv)
bool MediaRecorder::CheckPrincipal() bool MediaRecorder::CheckPrincipal()
{ {
NS_ABORT_IF_FALSE(NS_IsMainThread(), "Not running on main thread");
if (!mStream) {
return false;
}
nsCOMPtr<nsIPrincipal> principal = mStream->GetPrincipal(); nsCOMPtr<nsIPrincipal> principal = mStream->GetPrincipal();
if (!GetOwner()) if (!GetOwner())
return false; return false;
@ -718,5 +768,12 @@ bool MediaRecorder::CheckPrincipal()
return subsumes; return subsumes;
} }
void
MediaRecorder::RemoveSession(Session* aSession)
{
LOG(PR_LOG_DEBUG, ("MediaRecorder.RemoveSession (%p)", aSession));
mSessions.RemoveElement(aSession);
}
} }
} }

View File

@ -100,13 +100,14 @@ protected:
void SetMimeType(const nsString &aMimeType); void SetMimeType(const nsString &aMimeType);
MediaRecorder(const MediaRecorder& x) MOZ_DELETE; // prevent bad usage MediaRecorder(const MediaRecorder& x) MOZ_DELETE; // prevent bad usage
// Remove session pointer.
void RemoveSession(Session* aSession);
// MediaStream passed from js context // MediaStream passed from js context
nsRefPtr<DOMMediaStream> mStream; nsRefPtr<DOMMediaStream> mStream;
// The current state of the MediaRecorder object. // The current state of the MediaRecorder object.
RecordingState mState; RecordingState mState;
// Current recording session. // Hold the sessions pointer in media recorder and clean in the destructor of recorder.
nsRefPtr<Session> mSession; nsTArray<Session*> mSessions;
// Thread safe for mMimeType. // Thread safe for mMimeType.
Mutex mMutex; Mutex mMutex;
// It specifies the container format as well as the audio and video capture formats. // It specifies the container format as well as the audio and video capture formats.

View File

@ -403,6 +403,7 @@ skip-if = (buildapp == 'b2g' && (toolkit != 'gonk' || debug))
[test_mediarecorder_avoid_recursion.html] [test_mediarecorder_avoid_recursion.html]
[test_mediarecorder_record_timeslice.html] [test_mediarecorder_record_timeslice.html]
[test_mediarecorder_record_audiocontext.html] [test_mediarecorder_record_audiocontext.html]
[test_mediarecorder_record_audiocontext_mlk.html]
[test_mediarecorder_record_4ch_audiocontext.html] [test_mediarecorder_record_4ch_audiocontext.html]
skip-if = (toolkit == 'gonk' && !debug) skip-if = (toolkit == 'gonk' && !debug)
[test_mediarecorder_record_stopms.html] [test_mediarecorder_record_stopms.html]

View File

@ -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>