gecko-dev/dom/media/MediaRecorder.cpp
Andreas Pehrson 154c803c91 Bug 1423253 - Route MediaEncoder::Suspend/Resume through the graph thread for less drift. r=padenot
Because we sync video frames to audio time before encoding, we suspend and
resume on the audio thread because that's the only place we have acccess to
both clocks at the same time;
- A dispatch to the audio encoder puts the event in the right place in the
  audio encoder buffer.
- TimeStamp::Now() is the current video time, and since we capture it on the
  audio thread these are in sync. This timestamp needs drift compensation too -
  this happens in a later patch on this bug.

Differential Revision: https://phabricator.services.mozilla.com/D22907

--HG--
extra : moz-landing-system : lando
2019-03-22 11:42:55 +00:00

1734 lines
57 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "MediaRecorder.h"
#include "AudioNodeEngine.h"
#include "AudioNodeStream.h"
#include "DOMMediaStream.h"
#include "GeckoProfiler.h"
#include "MediaDecoder.h"
#include "MediaEncoder.h"
#include "MediaStreamGraphImpl.h"
#include "VideoUtils.h"
#include "mozilla/DOMEventTargetHelper.h"
#include "mozilla/dom/AudioStreamTrack.h"
#include "mozilla/dom/BlobEvent.h"
#include "mozilla/dom/File.h"
#include "mozilla/dom/MediaRecorderErrorEvent.h"
#include "mozilla/dom/MutableBlobStorage.h"
#include "mozilla/dom/VideoStreamTrack.h"
#include "mozilla/media/MediaUtils.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/TaskQueue.h"
#include "nsAutoPtr.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsContentTypeParser.h"
#include "nsContentUtils.h"
#include "nsDocShell.h"
#include "nsError.h"
#include "mozilla/dom/Document.h"
#include "nsIPermissionManager.h"
#include "nsIPrincipal.h"
#include "nsIScriptError.h"
#include "nsMimeTypes.h"
#include "nsProxyRelease.h"
#include "nsTArray.h"
#ifdef LOG
# undef LOG
#endif
mozilla::LazyLogModule gMediaRecorderLog("MediaRecorder");
#define LOG(type, msg) MOZ_LOG(gMediaRecorderLog, type, msg)
namespace mozilla {
namespace dom {
using namespace mozilla::media;
/* static */ StaticRefPtr<nsIAsyncShutdownBlocker>
gMediaRecorderShutdownBlocker;
static nsTHashtable<nsRefPtrHashKey<MediaRecorder::Session>> gSessions;
/**
* MediaRecorderReporter measures memory being used by the Media Recorder.
*
* It is a singleton reporter and the single class object lives as long as at
* least one Recorder is registered. In MediaRecorder, the reporter is
* unregistered when it is destroyed.
*/
class MediaRecorderReporter final : public nsIMemoryReporter {
public:
static void AddMediaRecorder(MediaRecorder* aRecorder) {
if (!sUniqueInstance) {
sUniqueInstance = MakeAndAddRef<MediaRecorderReporter>();
RegisterWeakAsyncMemoryReporter(sUniqueInstance);
}
sUniqueInstance->mRecorders.AppendElement(aRecorder);
}
static void RemoveMediaRecorder(MediaRecorder* aRecorder) {
if (!sUniqueInstance) {
return;
}
sUniqueInstance->mRecorders.RemoveElement(aRecorder);
if (sUniqueInstance->mRecorders.IsEmpty()) {
UnregisterWeakMemoryReporter(sUniqueInstance);
sUniqueInstance = nullptr;
}
}
NS_DECL_THREADSAFE_ISUPPORTS
MediaRecorderReporter() = default;
NS_IMETHOD
CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData,
bool aAnonymize) override {
nsTArray<RefPtr<MediaRecorder::SizeOfPromise>> promises;
for (const RefPtr<MediaRecorder>& recorder : mRecorders) {
promises.AppendElement(recorder->SizeOfExcludingThis(MallocSizeOf));
}
nsCOMPtr<nsIHandleReportCallback> handleReport = aHandleReport;
nsCOMPtr<nsISupports> data = aData;
MediaRecorder::SizeOfPromise::All(GetCurrentThreadSerialEventTarget(),
promises)
->Then(
GetCurrentThreadSerialEventTarget(), __func__,
[handleReport, data](const nsTArray<size_t>& sizes) {
nsCOMPtr<nsIMemoryReporterManager> manager =
do_GetService("@mozilla.org/memory-reporter-manager;1");
if (!manager) {
return;
}
size_t sum = 0;
for (const size_t& size : sizes) {
sum += size;
}
handleReport->Callback(
EmptyCString(), NS_LITERAL_CSTRING("explicit/media/recorder"),
KIND_HEAP, UNITS_BYTES, sum,
NS_LITERAL_CSTRING("Memory used by media recorder."), data);
manager->EndReport();
},
[](size_t) { MOZ_CRASH("Unexpected reject"); });
return NS_OK;
}
private:
MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
virtual ~MediaRecorderReporter() {
MOZ_ASSERT(mRecorders.IsEmpty(), "All recorders must have been removed");
}
static StaticRefPtr<MediaRecorderReporter> sUniqueInstance;
nsTArray<RefPtr<MediaRecorder>> mRecorders;
};
NS_IMPL_ISUPPORTS(MediaRecorderReporter, nsIMemoryReporter);
NS_IMPL_CYCLE_COLLECTION_CLASS(MediaRecorder)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaRecorder,
DOMEventTargetHelper)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMStream)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioNode)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSecurityDomException)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mUnknownDomException)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaRecorder,
DOMEventTargetHelper)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMStream)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioNode)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSecurityDomException)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mUnknownDomException)
tmp->UnRegisterActivityObserver();
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaRecorder)
NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity)
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
NS_IMPL_ADDREF_INHERITED(MediaRecorder, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(MediaRecorder, DOMEventTargetHelper)
/**
* Session is an object to represent a single recording event.
* In original design, all recording context is stored in MediaRecorder, which
* causes a problem if someone calls MediaRecorder::Stop and
* MediaRecorder::Start quickly. To prevent blocking main thread, media encoding
* is executed in a second thread, named as Read Thread. For the same reason, we
* do not wait Read Thread shutdown in MediaRecorder::Stop. If someone call
* MediaRecorder::Start before Read Thread shutdown, the same recording context
* in MediaRecorder might be access by two Reading Threads, which cause a
* problem. In the new design, we put recording context into Session object,
* including Read Thread. Each Session has its own recording context and Read
* Thread, problem is been resolved.
*
* Life cycle of a Session object.
* 1) Initialization Stage (in main thread)
* Setup media streams in MSG, and bind MediaEncoder with Source Stream when
* mStream is available. Resource allocation, such as encoded data cache buffer
* and MediaEncoder. Create read thread. Automatically switch to Extract stage
* in the end of this stage. 2) Extract Stage (in Read Thread) Pull encoded A/V
* frames from MediaEncoder, dispatch to OnDataAvailable handler. Unless a
* client calls Session::Stop, Session object keeps stay in this stage. 3)
* Destroy Stage (in main thread) Switch from Extract stage to Destroy stage by
* calling Session::Stop. Release session resource and remove associated streams
* from MSG.
*
* Lifetime of MediaRecorder and Session objects.
* 1) MediaRecorder creates a Session in MediaRecorder::Start function and holds
* a reference to Session. Then the Session registers itself to a
* ShutdownBlocker and also holds a reference to MediaRecorder.
* Therefore, the reference dependency in gecko is:
* ShutdownBlocker -> 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 PrincipalChangeObserver<MediaStreamTrack>,
public DOMMediaStream::TrackListener {
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Session)
// Main thread task.
// Create a blob event and send back to client.
class PushBlobRunnable : public Runnable, public MutableBlobStorageCallback {
public:
// We need to always declare refcounting because
// MutableBlobStorageCallback has pure-virtual refcounting.
NS_DECL_ISUPPORTS_INHERITED
// aDestroyRunnable can be null. If it's not, it will be dispatched after
// the PushBlobRunnable::Run().
PushBlobRunnable(Session* aSession, Runnable* aDestroyRunnable)
: Runnable("dom::MediaRecorder::Session::PushBlobRunnable"),
mSession(aSession),
mDestroyRunnable(aDestroyRunnable) {}
NS_IMETHOD Run() override {
LOG(LogLevel::Debug, ("Session.PushBlobRunnable s=(%p)", mSession.get()));
MOZ_ASSERT(NS_IsMainThread());
mSession->GetBlobWhenReady(this);
return NS_OK;
}
void BlobStoreCompleted(MutableBlobStorage* aBlobStorage, Blob* aBlob,
nsresult aRv) override {
RefPtr<MediaRecorder> recorder = mSession->mRecorder;
if (!recorder) {
return;
}
if (NS_FAILED(aRv)) {
mSession->DoSessionEndTask(aRv);
return;
}
nsresult rv = recorder->CreateAndDispatchBlobEvent(aBlob);
if (NS_FAILED(rv)) {
mSession->DoSessionEndTask(aRv);
}
if (mDestroyRunnable &&
NS_FAILED(NS_DispatchToMainThread(mDestroyRunnable.forget()))) {
MOZ_ASSERT(false, "NS_DispatchToMainThread failed");
}
}
private:
~PushBlobRunnable() = default;
RefPtr<Session> mSession;
// The generation of the blob is async. In order to avoid dispatching the
// DestroyRunnable before pushing the blob event, we store the runnable
// here.
RefPtr<Runnable> mDestroyRunnable;
};
class StoreEncodedBufferRunnable final : public Runnable {
RefPtr<Session> mSession;
nsTArray<nsTArray<uint8_t>> mBuffer;
public:
StoreEncodedBufferRunnable(Session* aSession,
nsTArray<nsTArray<uint8_t>>&& aBuffer)
: Runnable("StoreEncodedBufferRunnable"),
mSession(aSession),
mBuffer(std::move(aBuffer)) {}
NS_IMETHOD
Run() override {
MOZ_ASSERT(NS_IsMainThread());
mSession->MaybeCreateMutableBlobStorage();
for (uint32_t i = 0; i < mBuffer.Length(); i++) {
if (mBuffer[i].IsEmpty()) {
continue;
}
nsresult rv = mSession->mMutableBlobStorage->Append(
mBuffer[i].Elements(), mBuffer[i].Length());
if (NS_WARN_IF(NS_FAILED(rv))) {
mSession->DoSessionEndTask(rv);
break;
}
}
return NS_OK;
}
};
// Notify encoder error, run in main thread task. (Bug 1095381)
class EncoderErrorNotifierRunnable : public Runnable {
public:
explicit EncoderErrorNotifierRunnable(Session* aSession)
: Runnable("dom::MediaRecorder::Session::EncoderErrorNotifierRunnable"),
mSession(aSession) {}
NS_IMETHOD Run() override {
LOG(LogLevel::Debug,
("Session.ErrorNotifyRunnable s=(%p)", mSession.get()));
MOZ_ASSERT(NS_IsMainThread());
RefPtr<MediaRecorder> recorder = mSession->mRecorder;
if (!recorder) {
return NS_OK;
}
recorder->NotifyError(NS_ERROR_UNEXPECTED);
return NS_OK;
}
private:
RefPtr<Session> mSession;
};
// Fire a named event, run in main thread task.
class DispatchEventRunnable : public Runnable {
public:
explicit DispatchEventRunnable(Session* aSession,
const nsAString& aEventName)
: Runnable("dom::MediaRecorder::Session::DispatchEventRunnable"),
mSession(aSession),
mEventName(aEventName) {}
NS_IMETHOD Run() override {
LOG(LogLevel::Debug,
("Session.DispatchEventRunnable s=(%p) e=(%s)", mSession.get(),
NS_ConvertUTF16toUTF8(mEventName).get()));
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_TRUE(mSession->mRecorder, NS_OK);
mSession->mRecorder->DispatchSimpleEvent(mEventName);
return NS_OK;
}
private:
RefPtr<Session> mSession;
nsString mEventName;
};
// Main thread task.
// To delete RecordingSession object.
class DestroyRunnable : public Runnable {
public:
explicit DestroyRunnable(Session* aSession)
: Runnable("dom::MediaRecorder::Session::DestroyRunnable"),
mSession(aSession) {}
explicit DestroyRunnable(already_AddRefed<Session> aSession)
: Runnable("dom::MediaRecorder::Session::DestroyRunnable"),
mSession(aSession) {}
NS_IMETHOD Run() override {
LOG(LogLevel::Debug,
("Session.DestroyRunnable session refcnt = (%d) s=(%p)",
static_cast<int>(mSession->mRefCnt), mSession.get()));
MOZ_ASSERT(NS_IsMainThread() && mSession);
RefPtr<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->mRunningState.isOk() &&
mSession->mRunningState.unwrap() != RunningState::Stopping &&
mSession->mRunningState.unwrap() != RunningState::Stopped) {
recorder->StopForSessionDestruction();
if (NS_FAILED(NS_DispatchToMainThread(
new DestroyRunnable(mSession.forget())))) {
MOZ_ASSERT(false, "NS_DispatchToMainThread failed");
}
return NS_OK;
}
if (mSession->mRunningState.isOk()) {
mSession->mRunningState = RunningState::Stopped;
}
// Dispatch stop event and clear MIME type.
mSession->mMimeType = NS_LITERAL_STRING("");
recorder->SetMimeType(mSession->mMimeType);
recorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop"));
RefPtr<Session> session = mSession.forget();
session->Shutdown()->Then(
GetCurrentThreadSerialEventTarget(), __func__,
[session]() {
gSessions.RemoveEntry(session);
if (gSessions.Count() == 0 && gMediaRecorderShutdownBlocker) {
// All sessions finished before shutdown, no need to keep the
// blocker.
RefPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
barrier->RemoveBlocker(gMediaRecorderShutdownBlocker);
gMediaRecorderShutdownBlocker = nullptr;
}
},
[]() { MOZ_CRASH("Not reached"); });
return NS_OK;
}
private:
// Call mSession::Release automatically while DestroyRunnable be destroy.
RefPtr<Session> mSession;
};
class EncoderListener : public MediaEncoderListener {
public:
EncoderListener(TaskQueue* aEncoderThread, Session* aSession)
: mEncoderThread(aEncoderThread), mSession(aSession) {}
void Forget() {
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
mSession = nullptr;
}
void Initialized() override {
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
if (mSession) {
mSession->MediaEncoderInitialized();
}
}
void DataAvailable() override {
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
if (mSession) {
mSession->MediaEncoderDataAvailable();
}
}
void Error() override {
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
if (mSession) {
mSession->MediaEncoderError();
}
}
void Shutdown() override {
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
if (mSession) {
mSession->MediaEncoderShutdown();
}
}
protected:
RefPtr<TaskQueue> mEncoderThread;
RefPtr<Session> mSession;
};
friend class EncoderErrorNotifierRunnable;
friend class PushBlobRunnable;
friend class DestroyRunnable;
public:
Session(MediaRecorder* aRecorder, int32_t aTimeSlice)
: mRecorder(aRecorder),
mMediaStreamReady(false),
mTimeSlice(aTimeSlice),
mRunningState(RunningState::Idling) {
MOZ_ASSERT(NS_IsMainThread());
mMaxMemory = Preferences::GetUint("media.recorder.max_memory",
MAX_ALLOW_MEMORY_BUFFER);
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));
if (mMediaStreamReady) {
DoSessionEndTask(NS_ERROR_ABORT);
}
NS_DispatchToMainThread(
NewRunnableMethod("MediaRecorder::Session::MediaStreamReady", this,
&Session::MediaStreamReady));
return;
}
void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override {
if (!mMediaStreamReady) {
// We haven't chosen the track set to record yet.
return;
}
if (aTrack->Ended()) {
// TrackEncoder will pickup tracks that end itself.
return;
}
MOZ_ASSERT(mEncoder);
if (mEncoder) {
mEncoder->RemoveMediaStreamTrack(aTrack);
}
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());
DOMMediaStream* domStream = mRecorder->Stream();
if (domStream) {
// The callback reports back when tracks are available and can be
// attached to MediaEncoder. This allows `recorder.start()` before any
// tracks are available. We have supported this historically and have
// mochitests assuming this behavior.
mMediaStream = domStream;
mMediaStream->RegisterTrackListener(this);
nsTArray<RefPtr<MediaStreamTrack>> tracks(2);
mMediaStream->GetTracks(tracks);
for (const auto& track : tracks) {
// Notify of existing tracks, as the stream doesn't do this by itself.
NotifyTrackAdded(track);
}
return;
}
if (mRecorder->mAudioNode) {
// 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;
}
TrackRate trackRate =
mRecorder->mAudioNode->Context()->Graph()->GraphRate();
// Web Audio node has only audio.
InitEncoder(ContainerWriter::CREATE_AUDIO_TRACK, trackRate);
return;
}
MOZ_ASSERT(false, "Unknown source");
}
void Stop() {
LOG(LogLevel::Debug, ("Session.Stop %p", this));
MOZ_ASSERT(NS_IsMainThread());
if (mEncoder) {
mEncoder->Stop();
}
if (mRunningState.isOk() &&
mRunningState.unwrap() == RunningState::Idling) {
LOG(LogLevel::Debug, ("Session.Stop Explicit end task %p", this));
// End the Session directly if there is no ExtractRunnable.
DoSessionEndTask(NS_OK);
} else if (mRunningState.isOk() &&
(mRunningState.unwrap() == RunningState::Starting ||
mRunningState.unwrap() == RunningState::Running)) {
mRunningState = RunningState::Stopping;
}
}
nsresult Pause() {
LOG(LogLevel::Debug, ("Session.Pause"));
MOZ_ASSERT(NS_IsMainThread());
if (!mEncoder) {
return NS_ERROR_FAILURE;
}
mEncoder->Suspend();
NS_DispatchToMainThread(
new DispatchEventRunnable(this, NS_LITERAL_STRING("pause")));
return NS_OK;
}
nsresult Resume() {
LOG(LogLevel::Debug, ("Session.Resume"));
MOZ_ASSERT(NS_IsMainThread());
if (!mEncoder) {
return NS_ERROR_FAILURE;
}
mEncoder->Resume();
NS_DispatchToMainThread(
new DispatchEventRunnable(this, NS_LITERAL_STRING("resume")));
return NS_OK;
}
nsresult RequestData() {
LOG(LogLevel::Debug, ("Session.RequestData"));
MOZ_ASSERT(NS_IsMainThread());
if (NS_FAILED(
NS_DispatchToMainThread(new PushBlobRunnable(this, nullptr)))) {
MOZ_ASSERT(false, "RequestData NS_DispatchToMainThread failed");
return NS_ERROR_FAILURE;
}
return NS_OK;
}
void MaybeCreateMutableBlobStorage() {
if (!mMutableBlobStorage) {
mMutableBlobStorage = new MutableBlobStorage(
MutableBlobStorage::eCouldBeInTemporaryFile, nullptr, mMaxMemory);
}
}
void GetBlobWhenReady(MutableBlobStorageCallback* aCallback) {
MOZ_ASSERT(NS_IsMainThread());
MaybeCreateMutableBlobStorage();
mMutableBlobStorage->GetBlobWhenReady(mRecorder->GetParentObject(),
NS_ConvertUTF16toUTF8(mMimeType),
aCallback);
mMutableBlobStorage = nullptr;
}
RefPtr<SizeOfPromise> SizeOfExcludingThis(
mozilla::MallocSizeOf aMallocSizeOf) {
MOZ_ASSERT(NS_IsMainThread());
size_t encodedBufferSize =
mMutableBlobStorage ? mMutableBlobStorage->SizeOfCurrentMemoryBuffer()
: 0;
if (!mEncoder) {
return SizeOfPromise::CreateAndResolve(encodedBufferSize, __func__);
}
auto& encoder = mEncoder;
return InvokeAsync(
mEncoderThread, __func__,
[encoder, encodedBufferSize, aMallocSizeOf]() {
return SizeOfPromise::CreateAndResolve(
encodedBufferSize + encoder->SizeOfExcludingThis(aMallocSizeOf),
__func__);
});
}
private:
// Only DestroyRunnable is allowed to delete Session object on main thread.
virtual ~Session() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(mShutdownPromise);
LOG(LogLevel::Debug, ("Session.~Session (%p)", this));
}
// Pull encoded media data from MediaEncoder and put into MutableBlobStorage.
// 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, Runnable* aDestroyRunnable) {
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
LOG(LogLevel::Debug, ("Session.Extract %p", this));
AUTO_PROFILER_LABEL("MediaRecorder::Session::Extract", OTHER);
// Pull encoded media data from MediaEncoder
nsTArray<nsTArray<uint8_t>> encodedBuf;
nsresult rv = mEncoder->GetEncodedData(&encodedBuf);
if (NS_FAILED(rv)) {
MOZ_RELEASE_ASSERT(encodedBuf.IsEmpty());
// Even if we failed to encode more data, it might be time to push a blob
// with already encoded data.
}
// Append pulled data into cache buffer.
NS_DispatchToMainThread(
new StoreEncodedBufferRunnable(this, std::move(encodedBuf)));
// Whether push encoded data back to onDataAvailable automatically or we
// need a flush.
bool pushBlob = aForceFlush;
if (!pushBlob && mTimeSlice > 0 &&
(TimeStamp::Now() - mLastBlobTimeStamp).ToMilliseconds() > mTimeSlice) {
pushBlob = true;
}
if (pushBlob) {
if (NS_FAILED(NS_DispatchToMainThread(
new PushBlobRunnable(this, aDestroyRunnable)))) {
MOZ_ASSERT(false, "NS_DispatchToMainThread PushBlobRunnable failed");
} else {
mLastBlobTimeStamp = TimeStamp::Now();
}
} else if (aDestroyRunnable) {
if (NS_FAILED(NS_DispatchToMainThread(aDestroyRunnable))) {
MOZ_ASSERT(false, "NS_DispatchToMainThread DestroyRunnable failed");
}
}
}
void MediaStreamReady() {
if (!mMediaStream) {
// Already shut down. This can happen because MediaStreamReady is async.
return;
}
if (mMediaStreamReady) {
return;
}
if (!mRunningState.isOk() ||
mRunningState.unwrap() != RunningState::Idling) {
return;
}
nsTArray<RefPtr<mozilla::dom::MediaStreamTrack>> tracks;
mMediaStream->GetTracks(tracks);
uint8_t trackTypes = 0;
int32_t audioTracks = 0;
int32_t videoTracks = 0;
for (auto& track : tracks) {
if (track->Ended()) {
continue;
}
ConnectMediaStreamTrack(*track);
if (track->AsAudioStreamTrack()) {
++audioTracks;
trackTypes |= ContainerWriter::CREATE_AUDIO_TRACK;
} else if (track->AsVideoStreamTrack()) {
++videoTracks;
trackTypes |= ContainerWriter::CREATE_VIDEO_TRACK;
} else {
MOZ_CRASH("Unexpected track type");
}
}
if (trackTypes == 0) {
MOZ_ASSERT(audioTracks == 0);
MOZ_ASSERT(videoTracks == 0);
return;
}
mMediaStreamReady = true;
if (audioTracks > 1 || videoTracks > 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 = mRecorder->GetParentObject();
Document* document = window ? window->GetExtantDoc() : nullptr;
nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
NS_LITERAL_CSTRING("Media"), document,
nsContentUtils::eDOM_PROPERTIES,
"MediaRecorderMultiTracksNotSupported");
DoSessionEndTask(NS_ERROR_ABORT);
return;
}
// Check that we may access the tracks' content.
if (!MediaStreamTracksPrincipalSubsumes()) {
LOG(LogLevel::Warning, ("Session.MediaTracksReady MediaStreamTracks "
"principal check failed"));
DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
return;
}
LOG(LogLevel::Debug,
("Session.MediaTracksReady track type = (%d)", trackTypes));
InitEncoder(trackTypes, mMediaStream->GraphRate());
}
void ConnectMediaStreamTrack(MediaStreamTrack& aTrack) {
for (auto& track : mMediaStreamTracks) {
if (track->AsAudioStreamTrack() && aTrack.AsAudioStreamTrack()) {
// We only allow one audio track. See bug 1276928.
return;
}
if (track->AsVideoStreamTrack() && aTrack.AsVideoStreamTrack()) {
// We only allow one video track. See bug 1276928.
return;
}
}
mMediaStreamTracks.AppendElement(&aTrack);
aTrack.AddPrincipalChangeObserver(this);
}
bool PrincipalSubsumes(nsIPrincipal* aPrincipal) {
if (!mRecorder->GetOwner()) return false;
nsCOMPtr<Document> 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);
Document* doc = mRecorder->mAudioNode->GetOwner()
? mRecorder->mAudioNode->GetOwner()->GetExtantDoc()
: nullptr;
nsCOMPtr<nsIPrincipal> principal = doc ? doc->NodePrincipal() : nullptr;
return PrincipalSubsumes(principal);
}
void InitEncoder(uint8_t aTrackTypes, TrackRate aTrackRate) {
LOG(LogLevel::Debug, ("Session.InitEncoder %p", this));
MOZ_ASSERT(NS_IsMainThread());
if (!mRunningState.isOk() ||
mRunningState.unwrap() != RunningState::Idling) {
MOZ_ASSERT_UNREACHABLE("Double-init");
return;
}
// Create a TaskQueue to read encode media data from MediaEncoder.
MOZ_RELEASE_ASSERT(!mEncoderThread);
RefPtr<SharedThreadPool> pool =
GetMediaThreadPool(MediaThreadType::WEBRTC_DECODER);
if (!pool) {
LOG(LogLevel::Debug, ("Session.InitEncoder %p Failed to create "
"MediaRecorderReadThread thread pool",
this));
DoSessionEndTask(NS_ERROR_FAILURE);
return;
}
mEncoderThread =
MakeAndAddRef<TaskQueue>(pool.forget(), "MediaRecorderReadThread");
if (!gMediaRecorderShutdownBlocker) {
// Add a shutdown blocker so mEncoderThread can be shutdown async.
class Blocker : public ShutdownBlocker {
public:
Blocker()
: ShutdownBlocker(
NS_LITERAL_STRING("MediaRecorder::Session: shutdown")) {}
NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient*) override {
// Distribute the global async shutdown blocker in a ticket. If there
// are zero graphs then shutdown is unblocked when we go out of scope.
RefPtr<ShutdownTicket> ticket =
MakeAndAddRef<ShutdownTicket>(gMediaRecorderShutdownBlocker);
gMediaRecorderShutdownBlocker = nullptr;
nsTArray<RefPtr<ShutdownPromise>> promises(gSessions.Count());
for (auto iter = gSessions.Iter(); !iter.Done(); iter.Next()) {
promises.AppendElement(iter.Get()->GetKey()->Shutdown());
}
gSessions.Clear();
ShutdownPromise::All(GetCurrentThreadSerialEventTarget(), promises)
->Then(GetCurrentThreadSerialEventTarget(), __func__,
[ticket]() mutable {
MOZ_ASSERT(gSessions.Count() == 0);
// Unblock shutdown
ticket = nullptr;
},
[]() { MOZ_CRASH("Not reached"); });
return NS_OK;
}
};
gMediaRecorderShutdownBlocker = MakeAndAddRef<Blocker>();
RefPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
nsresult rv = barrier->AddBlocker(
gMediaRecorderShutdownBlocker, NS_LITERAL_STRING(__FILE__), __LINE__,
NS_LITERAL_STRING("MediaRecorder::Session: shutdown"));
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
}
gSessions.PutEntry(this);
uint32_t audioBitrate = mRecorder->GetAudioBitrate();
uint32_t videoBitrate = mRecorder->GetVideoBitrate();
uint32_t bitrate = mRecorder->GetBitrate();
if (bitrate > 0) {
// There's a total cap set. We have to make sure the type-specific limits
// are within range.
if ((aTrackTypes & ContainerWriter::CREATE_AUDIO_TRACK) &&
(aTrackTypes & ContainerWriter::CREATE_VIDEO_TRACK) &&
audioBitrate + videoBitrate > bitrate) {
LOG(LogLevel::Info, ("Session.InitEncoder Bitrates higher than total "
"cap. Recalculating."));
double factor =
bitrate / static_cast<double>(audioBitrate + videoBitrate);
audioBitrate = static_cast<uint32_t>(audioBitrate * factor);
videoBitrate = static_cast<uint32_t>(videoBitrate * factor);
} else if ((aTrackTypes & ContainerWriter::CREATE_AUDIO_TRACK) &&
!(aTrackTypes & ContainerWriter::CREATE_VIDEO_TRACK)) {
audioBitrate = std::min(audioBitrate, bitrate);
videoBitrate = 0;
} else if (!(aTrackTypes & ContainerWriter::CREATE_AUDIO_TRACK) &&
(aTrackTypes & ContainerWriter::CREATE_VIDEO_TRACK)) {
audioBitrate = 0;
videoBitrate = std::min(videoBitrate, bitrate);
}
MOZ_ASSERT(audioBitrate + videoBitrate <= bitrate);
}
// 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(
mEncoderThread, NS_LITERAL_STRING(""), audioBitrate, videoBitrate,
aTrackTypes, aTrackRate);
if (!mEncoder) {
LOG(LogLevel::Error, ("Session.InitEncoder !mEncoder %p", this));
DoSessionEndTask(NS_ERROR_ABORT);
return;
}
mEncoderListener = MakeAndAddRef<EncoderListener>(mEncoderThread, this);
nsresult rv =
mEncoderThread->Dispatch(NewRunnableMethod<RefPtr<EncoderListener>>(
"mozilla::MediaEncoder::RegisterListener", mEncoder,
&MediaEncoder::RegisterListener, mEncoderListener));
MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
Unused << rv;
if (mRecorder->mAudioNode) {
mEncoder->ConnectAudioNode(mRecorder->mAudioNode,
mRecorder->mAudioNodeOutput);
}
for (auto& track : mMediaStreamTracks) {
mEncoder->ConnectMediaStreamTrack(track);
}
// If user defines timeslice interval for video blobs we have to set
// appropriate video keyframe interval defined in milliseconds.
mEncoder->SetVideoKeyFrameInterval(mTimeSlice);
// Set mRunningState to Running so that ExtractRunnable/DestroyRunnable will
// take the responsibility to end the session.
mRunningState = RunningState::Starting;
}
// application should get blob and onstop event
void DoSessionEndTask(nsresult rv) {
MOZ_ASSERT(NS_IsMainThread());
if (mRunningState.isErr()) {
// We have already ended with an error.
return;
}
if (mRunningState.isOk() &&
mRunningState.unwrap() == RunningState::Stopped) {
// We have already ended gracefully.
return;
}
if (mRunningState.isOk() &&
(mRunningState.unwrap() == RunningState::Idling ||
mRunningState.unwrap() == RunningState::Starting)) {
NS_DispatchToMainThread(
new DispatchEventRunnable(this, NS_LITERAL_STRING("start")));
}
if (rv == NS_OK) {
mRunningState = RunningState::Stopped;
} else {
mRunningState = Err(rv);
}
if (NS_FAILED(rv)) {
mRecorder->ForceInactive();
NS_DispatchToMainThread(NewRunnableMethod<nsresult>(
"dom::MediaRecorder::NotifyError", mRecorder,
&MediaRecorder::NotifyError, rv));
}
RefPtr<Runnable> destroyRunnable = new DestroyRunnable(this);
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, destroyRunnable)))) {
MOZ_ASSERT(false, "NS_DispatchToMainThread PushBlobRunnable failed");
}
} else {
if (NS_FAILED(NS_DispatchToMainThread(destroyRunnable))) {
MOZ_ASSERT(false, "NS_DispatchToMainThread DestroyRunnable failed");
}
}
}
void MediaEncoderInitialized() {
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
// Pull encoded metadata from MediaEncoder
nsTArray<nsTArray<uint8_t>> encodedBuf;
nsString mime;
nsresult rv = mEncoder->GetEncodedMetadata(&encodedBuf, mime);
if (NS_FAILED(rv)) {
MOZ_ASSERT(false);
return;
}
// Append pulled data into cache buffer.
NS_DispatchToMainThread(
new StoreEncodedBufferRunnable(this, std::move(encodedBuf)));
RefPtr<Session> self = this;
NS_DispatchToMainThread(NewRunnableFrom([self, mime]() {
if (!self->mRecorder) {
MOZ_ASSERT_UNREACHABLE("Recorder should be live");
return NS_OK;
}
if (self->mRunningState.isOk()) {
auto state = self->mRunningState.unwrap();
if (state == RunningState::Starting ||
state == RunningState::Stopping) {
if (state == RunningState::Starting) {
// We set it to Running in the runnable since we can only assign
// mRunningState on main thread. We set it before running the start
// event runnable since that dispatches synchronously (and may cause
// js calls to methods depending on mRunningState).
self->mRunningState = RunningState::Running;
}
self->mMimeType = mime;
self->mRecorder->SetMimeType(self->mMimeType);
auto startEvent = MakeRefPtr<DispatchEventRunnable>(
self, NS_LITERAL_STRING("start"));
startEvent->Run();
}
}
return NS_OK;
}));
}
void MediaEncoderDataAvailable() {
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
Extract(false, nullptr);
}
void MediaEncoderError() {
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
NS_DispatchToMainThread(NewRunnableMethod<nsresult>(
"dom::MediaRecorder::Session::DoSessionEndTask", this,
&Session::DoSessionEndTask, NS_ERROR_FAILURE));
}
void MediaEncoderShutdown() {
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
MOZ_ASSERT(mEncoder->IsShutdown());
// For the stop event. Let's the creation of the blob to dispatch this
// runnable.
RefPtr<Runnable> destroyRunnable = new DestroyRunnable(this);
// Forces the last blob even if it's not time for it yet.
Extract(true, destroyRunnable);
// Clean up.
mEncoderListener->Forget();
DebugOnly<bool> unregistered =
mEncoder->UnregisterListener(mEncoderListener);
MOZ_ASSERT(unregistered);
}
RefPtr<ShutdownPromise> Shutdown() {
MOZ_ASSERT(NS_IsMainThread());
LOG(LogLevel::Debug, ("Session Shutdown %p", this));
if (mShutdownPromise) {
return mShutdownPromise;
}
mShutdownPromise = ShutdownPromise::CreateAndResolve(true, __func__);
RefPtr<Session> self = this;
if (mEncoder) {
auto& encoder = mEncoder;
encoder->Cancel();
MOZ_RELEASE_ASSERT(mEncoderListener);
auto& encoderListener = mEncoderListener;
mShutdownPromise = mShutdownPromise->Then(
mEncoderThread, __func__,
[encoder, encoderListener]() {
encoder->UnregisterListener(encoderListener);
encoderListener->Forget();
return ShutdownPromise::CreateAndResolve(true, __func__);
},
[]() {
MOZ_ASSERT_UNREACHABLE("Unexpected reject");
return ShutdownPromise::CreateAndReject(false, __func__);
});
}
// Remove main thread state.
if (mMediaStream) {
mMediaStream->UnregisterTrackListener(this);
mMediaStream = nullptr;
}
{
auto tracks(std::move(mMediaStreamTracks));
for (RefPtr<MediaStreamTrack>& track : tracks) {
track->RemovePrincipalChangeObserver(this);
}
}
// Break the cycle reference between Session and MediaRecorder.
if (mRecorder) {
mShutdownPromise = mShutdownPromise->Then(
GetCurrentThreadSerialEventTarget(), __func__,
[self]() {
self->mRecorder->RemoveSession(self);
self->mRecorder = nullptr;
return ShutdownPromise::CreateAndResolve(true, __func__);
},
[]() {
MOZ_ASSERT_UNREACHABLE("Unexpected reject");
return ShutdownPromise::CreateAndReject(false, __func__);
});
}
if (mEncoderThread) {
RefPtr<TaskQueue>& encoderThread = mEncoderThread;
mShutdownPromise = mShutdownPromise->Then(
GetCurrentThreadSerialEventTarget(), __func__,
[encoderThread]() { return encoderThread->BeginShutdown(); },
[]() {
MOZ_ASSERT_UNREACHABLE("Unexpected reject");
return ShutdownPromise::CreateAndReject(false, __func__);
});
}
return mShutdownPromise;
}
private:
enum class RunningState {
Idling, // Session has been created
Starting, // MediaEncoder started, waiting for data
Running, // MediaEncoder has produced data
Stopping, // Stop() has been called
Stopped, // Session has stopped without any error
};
// Hold reference to MediaRecorder that ensure MediaRecorder is alive
// if there is an active session. Access ONLY on main thread.
RefPtr<MediaRecorder> mRecorder;
// Stream currently recorded.
RefPtr<DOMMediaStream> mMediaStream;
// True after we have decided on the track set to use for the recording.
bool mMediaStreamReady;
// Tracks currently recorded. This should be a subset of mMediaStream's track
// set.
nsTArray<RefPtr<MediaStreamTrack>> mMediaStreamTracks;
// Runnable thread for reading data from MediaEncoder.
RefPtr<TaskQueue> mEncoderThread;
// MediaEncoder pipeline.
RefPtr<MediaEncoder> mEncoder;
// Listener through which MediaEncoder signals us.
RefPtr<EncoderListener> mEncoderListener;
// Set in Shutdown() and resolved when shutdown is complete.
RefPtr<ShutdownPromise> mShutdownPromise;
// A buffer to cache encoded media data.
RefPtr<MutableBlobStorage> mMutableBlobStorage;
// Max memory to use for the MutableBlobStorage.
uint64_t mMaxMemory;
// Current session mimeType
nsString mMimeType;
// Timestamp of the last fired dataavailable event.
TimeStamp mLastBlobTimeStamp;
// The interval of passing encoded data from MutableBlobStorage to
// onDataAvailable handler. "mTimeSlice < 0" means Session object does not
// push encoded data to onDataAvailable, instead, it passive wait the client
// side pull encoded data by calling requestData API.
const int32_t mTimeSlice;
// The session's current main thread state. The error type gets setwhen ending
// a recording with an error. An NS_OK error is invalid.
// Main thread only.
Result<RunningState, nsresult> mRunningState;
};
NS_IMPL_ISUPPORTS_INHERITED0(MediaRecorder::Session::PushBlobRunnable, Runnable)
MediaRecorder::~MediaRecorder() {
LOG(LogLevel::Debug, ("~MediaRecorder (%p)", this));
UnRegisterActivityObserver();
}
MediaRecorder::MediaRecorder(DOMMediaStream& aSourceMediaStream,
nsPIDOMWindowInner* aOwnerWindow)
: DOMEventTargetHelper(aOwnerWindow),
mAudioNodeOutput(0),
mState(RecordingState::Inactive),
mAudioBitsPerSecond(0),
mVideoBitsPerSecond(0),
mBitsPerSecond(0) {
MOZ_ASSERT(aOwnerWindow);
mDOMStream = &aSourceMediaStream;
RegisterActivityObserver();
}
MediaRecorder::MediaRecorder(AudioNode& aSrcAudioNode, uint32_t aSrcOutput,
nsPIDOMWindowInner* aOwnerWindow)
: DOMEventTargetHelper(aOwnerWindow),
mAudioNodeOutput(aSrcOutput),
mState(RecordingState::Inactive),
mAudioBitsPerSecond(0),
mVideoBitsPerSecond(0),
mBitsPerSecond(0) {
MOZ_ASSERT(aOwnerWindow);
mAudioNode = &aSrcAudioNode;
RegisterActivityObserver();
}
void MediaRecorder::RegisterActivityObserver() {
if (nsPIDOMWindowInner* window = GetOwner()) {
mDocument = window->GetExtantDoc();
if (mDocument) {
mDocument->RegisterActivityObserver(
NS_ISUPPORTS_CAST(nsIDocumentActivity*, this));
}
}
}
void MediaRecorder::UnRegisterActivityObserver() {
if (mDocument) {
mDocument->UnregisterActivityObserver(
NS_ISUPPORTS_CAST(nsIDocumentActivity*, this));
}
}
void MediaRecorder::SetMimeType(const nsString& aMimeType) {
mMimeType = aMimeType;
}
void MediaRecorder::GetMimeType(nsString& aMimeType) { aMimeType = mMimeType; }
void MediaRecorder::Start(const Optional<int32_t>& aTimeSlice,
ErrorResult& aResult) {
LOG(LogLevel::Debug, ("MediaRecorder.Start %p", this));
InitializeDomExceptions();
if (mState != RecordingState::Inactive) {
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
nsTArray<RefPtr<MediaStreamTrack>> tracks;
if (mDOMStream) {
mDOMStream->GetTracks(tracks);
}
if (!tracks.IsEmpty()) {
// If there are tracks already available that we're not allowed
// to record, we should throw a security error.
bool subsumes = false;
nsPIDOMWindowInner* window;
Document* doc;
if (!(window = GetOwner()) || !(doc = window->GetExtantDoc()) ||
NS_FAILED(doc->NodePrincipal()->Subsumes(mDOMStream->GetPrincipal(),
&subsumes)) ||
!subsumes) {
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();
}
MediaRecorderReporter::AddMediaRecorder(this);
mState = RecordingState::Recording;
// Start a session.
mSessions.AppendElement();
mSessions.LastElement() = new Session(this, timeSlice);
mSessions.LastElement()->Start();
mStartTime = TimeStamp::Now();
Telemetry::ScalarAdd(Telemetry::ScalarID::MEDIARECORDER_RECORDING_COUNT, 1);
}
void MediaRecorder::Stop(ErrorResult& aResult) {
LOG(LogLevel::Debug, ("MediaRecorder.Stop %p", this));
MediaRecorderReporter::RemoveMediaRecorder(this);
if (mState == RecordingState::Inactive) {
return;
}
mState = RecordingState::Inactive;
MOZ_ASSERT(mSessions.Length() > 0);
mSessions.LastElement()->Stop();
}
void MediaRecorder::Pause(ErrorResult& aResult) {
LOG(LogLevel::Debug, ("MediaRecorder.Pause %p", this));
if (mState == RecordingState::Inactive) {
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
if (mState == RecordingState::Paused) {
return;
}
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(LogLevel::Debug, ("MediaRecorder.Resume %p", this));
if (mState == RecordingState::Inactive) {
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
if (mState == RecordingState::Recording) {
return;
}
MOZ_ASSERT(mSessions.Length() > 0);
nsresult rv = mSessions.LastElement()->Resume();
if (NS_FAILED(rv)) {
NotifyError(rv);
return;
}
mState = RecordingState::Recording;
}
void MediaRecorder::RequestData(ErrorResult& aResult) {
if (mState == RecordingState::Inactive) {
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
MOZ_ASSERT(mSessions.Length() > 0);
nsresult rv = mSessions.LastElement()->RequestData();
if (NS_FAILED(rv)) {
NotifyError(rv);
}
}
JSObject* MediaRecorder::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return MediaRecorder_Binding::Wrap(aCx, this, aGivenProto);
}
/* static */
already_AddRefed<MediaRecorder> MediaRecorder::Constructor(
const GlobalObject& aGlobal, DOMMediaStream& aStream,
const MediaRecorderOptions& aInitDict, ErrorResult& aRv) {
nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
do_QueryInterface(aGlobal.GetAsSupports());
if (!ownerWindow) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
if (!IsTypeSupported(aInitDict.mMimeType)) {
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return nullptr;
}
RefPtr<MediaRecorder> object = new MediaRecorder(aStream, ownerWindow);
object->SetOptions(aInitDict);
return object.forget();
}
/* static */
already_AddRefed<MediaRecorder> MediaRecorder::Constructor(
const GlobalObject& aGlobal, AudioNode& aSrcAudioNode, uint32_t aSrcOutput,
const MediaRecorderOptions& aInitDict, ErrorResult& aRv) {
// Allow recording from audio node only when pref is on.
if (!Preferences::GetBool("media.recorder.audio_node.enabled", false)) {
// Pretending that this constructor is not defined.
NS_NAMED_LITERAL_STRING(argStr, "Argument 1 of MediaRecorder.constructor");
NS_NAMED_LITERAL_STRING(typeStr, "MediaStream");
aRv.ThrowTypeError<MSG_DOES_NOT_IMPLEMENT_INTERFACE>(argStr, typeStr);
return nullptr;
}
nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
do_QueryInterface(aGlobal.GetAsSupports());
if (!ownerWindow) {
aRv.Throw(NS_ERROR_FAILURE);
return nullptr;
}
// aSrcOutput doesn't matter to destination node because it has no output.
if (aSrcAudioNode.NumberOfOutputs() > 0 &&
aSrcOutput >= aSrcAudioNode.NumberOfOutputs()) {
aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
return nullptr;
}
if (!IsTypeSupported(aInitDict.mMimeType)) {
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return nullptr;
}
RefPtr<MediaRecorder> object =
new MediaRecorder(aSrcAudioNode, aSrcOutput, ownerWindow);
object->SetOptions(aInitDict);
return object.forget();
}
void MediaRecorder::SetOptions(const MediaRecorderOptions& aInitDict) {
SetMimeType(aInitDict.mMimeType);
mAudioBitsPerSecond = aInitDict.mAudioBitsPerSecond.WasPassed()
? aInitDict.mAudioBitsPerSecond.Value()
: 0;
mVideoBitsPerSecond = aInitDict.mVideoBitsPerSecond.WasPassed()
? aInitDict.mVideoBitsPerSecond.Value()
: 0;
mBitsPerSecond = aInitDict.mBitsPerSecond.WasPassed()
? aInitDict.mBitsPerSecond.Value()
: 0;
// We're not handling dynamic changes yet. Eventually we'll handle
// setting audio, video and/or total -- and anything that isn't set,
// we'll derive. Calculated versions require querying bitrates after
// the encoder is Init()ed. This happens only after data is
// available and thus requires dynamic changes.
//
// Until dynamic changes are supported, I prefer to be safe and err
// slightly high
if (aInitDict.mBitsPerSecond.WasPassed() &&
!aInitDict.mVideoBitsPerSecond.WasPassed()) {
mVideoBitsPerSecond = mBitsPerSecond;
}
}
static char const* const gWebMVideoEncoderCodecs[4] = {
"opus",
"vp8",
"vp8.0",
// no VP9 yet
nullptr,
};
static char const* const gWebMAudioEncoderCodecs[4] = {
"opus",
nullptr,
};
static char const* const gOggAudioEncoderCodecs[2] = {
"opus",
// we could support vorbis here too, but don't
nullptr,
};
template <class String>
static bool CodecListContains(char const* const* aCodecs,
const String& aCodec) {
for (int32_t i = 0; aCodecs[i]; ++i) {
if (aCodec.EqualsASCII(aCodecs[i])) return true;
}
return false;
}
/* static */
bool MediaRecorder::IsTypeSupported(GlobalObject& aGlobal,
const nsAString& aMIMEType) {
return IsTypeSupported(aMIMEType);
}
/* static */
bool MediaRecorder::IsTypeSupported(const nsAString& aMIMEType) {
char const* const* codeclist = nullptr;
if (aMIMEType.IsEmpty()) {
return true;
}
nsContentTypeParser parser(aMIMEType);
nsAutoString mimeType;
nsresult rv = parser.GetType(mimeType);
if (NS_FAILED(rv)) {
return false;
}
// effectively a 'switch (mimeType) {'
if (mimeType.EqualsLiteral(AUDIO_OGG)) {
if (MediaDecoder::IsOggEnabled() && MediaDecoder::IsOpusEnabled()) {
codeclist = gOggAudioEncoderCodecs;
}
}
#ifdef MOZ_WEBM_ENCODER
else if ((mimeType.EqualsLiteral(VIDEO_WEBM) ||
mimeType.EqualsLiteral(AUDIO_WEBM)) &&
MediaEncoder::IsWebMEncoderEnabled()) {
if (mimeType.EqualsLiteral(AUDIO_WEBM)) {
codeclist = gWebMAudioEncoderCodecs;
} else {
codeclist = gWebMVideoEncoderCodecs;
}
}
#endif
// codecs don't matter if we don't support the container
if (!codeclist) {
return false;
}
// now filter on codecs, and if needed rescind support
nsAutoString codecstring;
rv = parser.GetParameter("codecs", codecstring);
nsTArray<nsString> codecs;
if (!ParseCodecsString(codecstring, codecs)) {
return false;
}
for (const nsString& codec : codecs) {
if (!CodecListContains(codeclist, codec)) {
// Totally unsupported codec
return false;
}
}
return true;
}
nsresult MediaRecorder::CreateAndDispatchBlobEvent(Blob* aBlob) {
MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
BlobEventInit init;
init.mBubbles = false;
init.mCancelable = false;
init.mData = aBlob;
RefPtr<BlobEvent> event =
BlobEvent::Constructor(this, NS_LITERAL_STRING("dataavailable"), init);
event->SetTrusted(true);
ErrorResult rv;
DispatchEvent(*event, rv);
return rv.StealNSResult();
}
void MediaRecorder::DispatchSimpleEvent(const nsAString& aStr) {
MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
nsresult rv = CheckInnerWindowCorrectness();
if (NS_FAILED(rv)) {
return;
}
rv = DOMEventTargetHelper::DispatchTrustedEvent(aStr);
if (NS_FAILED(rv)) {
LOG(LogLevel::Error,
("MediaRecorder.DispatchSimpleEvent: DispatchTrustedEvent failed %p",
this));
NS_ERROR("Failed to dispatch the event!!!");
}
}
void MediaRecorder::NotifyError(nsresult aRv) {
MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
nsresult rv = CheckInnerWindowCorrectness();
if (NS_FAILED(rv)) {
return;
}
MediaRecorderErrorEventInit init;
init.mBubbles = false;
init.mCancelable = false;
// These DOMExceptions have been created earlier so they can contain stack
// traces. We attach the appropriate one here to be fired. We should have
// exceptions here, but defensively check.
switch (aRv) {
case NS_ERROR_DOM_SECURITY_ERR:
if (!mSecurityDomException) {
LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
"mSecurityDomException was not initialized"));
mSecurityDomException = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR);
}
init.mError = mSecurityDomException.forget();
break;
default:
if (!mUnknownDomException) {
LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
"mUnknownDomException was not initialized"));
mUnknownDomException = DOMException::Create(NS_ERROR_DOM_UNKNOWN_ERR);
}
LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
"mUnknownDomException being fired for aRv: %X",
uint32_t(aRv)));
init.mError = mUnknownDomException.forget();
}
RefPtr<MediaRecorderErrorEvent> event = MediaRecorderErrorEvent::Constructor(
this, NS_LITERAL_STRING("error"), init);
event->SetTrusted(true);
IgnoredErrorResult res;
DispatchEvent(*event, res);
if (res.Failed()) {
NS_ERROR("Failed to dispatch the error event!!!");
}
}
void MediaRecorder::RemoveSession(Session* aSession) {
LOG(LogLevel::Debug, ("MediaRecorder.RemoveSession (%p)", aSession));
mSessions.RemoveElement(aSession);
}
void MediaRecorder::NotifyOwnerDocumentActivityChanged() {
nsPIDOMWindowInner* window = GetOwner();
NS_ENSURE_TRUE_VOID(window);
Document* doc = window->GetExtantDoc();
NS_ENSURE_TRUE_VOID(doc);
bool inFrameSwap = false;
if (nsDocShell* docShell = static_cast<nsDocShell*>(doc->GetDocShell())) {
inFrameSwap = docShell->InFrameSwap();
}
LOG(LogLevel::Debug, ("MediaRecorder %p NotifyOwnerDocumentActivityChanged "
"IsActive=%d, "
"IsVisible=%d, "
"InFrameSwap=%d",
this, doc->IsActive(), doc->IsVisible(), inFrameSwap));
if (!doc->IsActive() || !(inFrameSwap || doc->IsVisible())) {
// Stop the session.
ErrorResult result;
Stop(result);
result.SuppressException();
}
}
void MediaRecorder::ForceInactive() {
LOG(LogLevel::Debug, ("MediaRecorder.ForceInactive %p", this));
mState = RecordingState::Inactive;
}
void MediaRecorder::StopForSessionDestruction() {
LOG(LogLevel::Debug, ("MediaRecorder.StopForSessionDestruction %p", this));
MediaRecorderReporter::RemoveMediaRecorder(this);
// We do not perform a mState != RecordingState::Recording) check here as
// we may already be inactive due to ForceInactive().
mState = RecordingState::Inactive;
MOZ_ASSERT(mSessions.Length() > 0);
mSessions.LastElement()->Stop();
// This is a coarse calculation and does not reflect the duration of the
// final recording for reasons such as pauses. However it allows us an idea
// of how long people are running their recorders for.
TimeDuration timeDelta = TimeStamp::Now() - mStartTime;
Telemetry::Accumulate(Telemetry::MEDIA_RECORDER_RECORDING_DURATION,
timeDelta.ToSeconds());
}
void MediaRecorder::InitializeDomExceptions() {
mSecurityDomException = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR);
mUnknownDomException = DOMException::Create(NS_ERROR_DOM_UNKNOWN_ERR);
}
RefPtr<MediaRecorder::SizeOfPromise> MediaRecorder::SizeOfExcludingThis(
mozilla::MallocSizeOf aMallocSizeOf) {
MOZ_ASSERT(NS_IsMainThread());
// The return type of a chained MozPromise cannot be changed, so we create a
// holder for our desired return type and resolve that from All()->Then().
auto holder = MakeRefPtr<Refcountable<MozPromiseHolder<SizeOfPromise>>>();
RefPtr<SizeOfPromise> promise = holder->Ensure(__func__);
nsTArray<RefPtr<SizeOfPromise>> promises(mSessions.Length());
for (const RefPtr<Session>& session : mSessions) {
promises.AppendElement(session->SizeOfExcludingThis(aMallocSizeOf));
}
SizeOfPromise::All(GetCurrentThreadSerialEventTarget(), promises)
->Then(GetCurrentThreadSerialEventTarget(), __func__,
[holder](const nsTArray<size_t>& sizes) {
size_t total = 0;
for (const size_t& size : sizes) {
total += size;
}
holder->Resolve(total, __func__);
},
[]() { MOZ_CRASH("Unexpected reject"); });
return promise;
}
StaticRefPtr<MediaRecorderReporter> MediaRecorderReporter::sUniqueInstance;
} // namespace dom
} // namespace mozilla