summaryrefslogtreecommitdiffstats
path: root/dom/media/MediaRecorder.cpp
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /dom/media/MediaRecorder.cpp
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/media/MediaRecorder.cpp')
-rw-r--r--dom/media/MediaRecorder.cpp1535
1 files changed, 1535 insertions, 0 deletions
diff --git a/dom/media/MediaRecorder.cpp b/dom/media/MediaRecorder.cpp
new file mode 100644
index 000000000..e2d72b112
--- /dev/null
+++ b/dom/media/MediaRecorder.cpp
@@ -0,0 +1,1535 @@
+/* -*- 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 "EncodedBufferCache.h"
+#include "MediaDecoder.h"
+#include "MediaEncoder.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/AudioStreamTrack.h"
+#include "mozilla/dom/BlobEvent.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/RecordErrorEvent.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "nsAutoPtr.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"
+#include "nsContentTypeParser.h"
+#include "nsCharSeparatedTokenizer.h"
+
+#ifdef LOG
+#undef LOG
+#endif
+
+mozilla::LazyLogModule gMediaRecorderLog("MediaRecorder");
+#define LOG(type, msg) MOZ_LOG(gMediaRecorderLog, type, msg)
+
+namespace mozilla {
+
+namespace dom {
+
+/**
++ * 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:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ MediaRecorderReporter() {};
+ static MediaRecorderReporter* UniqueInstance();
+ void InitMemoryReporter();
+
+ static void AddMediaRecorder(MediaRecorder *aRecorder)
+ {
+ GetRecorders().AppendElement(aRecorder);
+ }
+
+ static void RemoveMediaRecorder(MediaRecorder *aRecorder)
+ {
+ RecordersArray& recorders = GetRecorders();
+ recorders.RemoveElement(aRecorder);
+ if (recorders.IsEmpty()) {
+ sUniqueInstance = nullptr;
+ }
+ }
+
+ NS_IMETHOD
+ CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) override
+ {
+ int64_t amount = 0;
+ RecordersArray& recorders = GetRecorders();
+ for (size_t i = 0; i < recorders.Length(); ++i) {
+ amount += recorders[i]->SizeOfExcludingThis(MallocSizeOf);
+ }
+
+ MOZ_COLLECT_REPORT(
+ "explicit/media/recorder", KIND_HEAP, UNITS_BYTES, amount,
+ "Memory used by media recorder.");
+
+ return NS_OK;
+ }
+
+private:
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+ virtual ~MediaRecorderReporter();
+ static StaticRefPtr<MediaRecorderReporter> sUniqueInstance;
+ typedef nsTArray<MediaRecorder*> RecordersArray;
+ static RecordersArray& GetRecorders()
+ {
+ return UniqueInstance()->mRecorders;
+ }
+ RecordersArray 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(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)
+ tmp->UnRegisterActivityObserver();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(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 MediaRecoder::Stop and MedaiRecorder::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 MediaRecoder::Start before Read Thread shutdown,
+ * the same recording context in MediaRecoder 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
+ * ShutdownObserver and also holds a reference to 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,
+ 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 Runnable
+ {
+ public:
+ explicit PushBlobRunnable(Session* aSession)
+ : mSession(aSession)
+ { }
+
+ NS_IMETHOD Run() override
+ {
+ LOG(LogLevel::Debug, ("Session.PushBlobRunnable s=(%p)", mSession.get()));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<MediaRecorder> recorder = mSession->mRecorder;
+ if (!recorder) {
+ return NS_OK;
+ }
+
+ nsresult rv = recorder->CreateAndDispatchBlobEvent(mSession->GetEncodedData());
+ if (NS_FAILED(rv)) {
+ recorder->NotifyError(rv);
+ }
+
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<Session> mSession;
+ };
+
+ // Notify encoder error, run in main thread task. (Bug 1095381)
+ class EncoderErrorNotifierRunnable : public Runnable
+ {
+ public:
+ explicit EncoderErrorNotifierRunnable(Session* aSession)
+ : 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;
+ }
+
+ if (mSession->IsEncoderError()) {
+ recorder->NotifyError(NS_ERROR_UNEXPECTED);
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<Session> mSession;
+ };
+
+ // Fire start event and set mimeType, run in main thread task.
+ class DispatchStartEventRunnable : public Runnable
+ {
+ public:
+ DispatchStartEventRunnable(Session* aSession, const nsAString & aEventName)
+ : mSession(aSession)
+ , mEventName(aEventName)
+ { }
+
+ NS_IMETHOD Run() override
+ {
+ LOG(LogLevel::Debug, ("Session.DispatchStartEventRunnable s=(%p)", mSession.get()));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ NS_ENSURE_TRUE(mSession->mRecorder, NS_OK);
+ RefPtr<MediaRecorder> recorder = mSession->mRecorder;
+
+ recorder->SetMimeType(mSession->mMimeType);
+ recorder->DispatchSimpleEvent(mEventName);
+
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<Session> mSession;
+ nsString mEventName;
+ };
+
+ // Record thread task and it run in Media Encoder thread.
+ // Fetch encoded Audio/Video data from MediaEncoder.
+ class ExtractRunnable : public Runnable
+ {
+ public:
+ explicit ExtractRunnable(Session* aSession)
+ : mSession(aSession) {}
+
+ ~ExtractRunnable()
+ {}
+
+ NS_IMETHOD Run() override
+ {
+ MOZ_ASSERT(NS_GetCurrentThread() == mSession->mReadThread);
+
+ LOG(LogLevel::Debug, ("Session.ExtractRunnable shutdown = %d", mSession->mEncoder->IsShutdown()));
+ if (!mSession->mEncoder->IsShutdown()) {
+ mSession->Extract(false);
+ if (NS_FAILED(NS_DispatchToCurrentThread(this))) {
+ NS_WARNING("Failed to dispatch ExtractRunnable to encoder thread");
+ }
+ } else {
+ // Flush out remaining encoded data.
+ mSession->Extract(true);
+ if (mSession->mIsRegisterProfiler)
+ profiler_unregister_thread();
+ if (NS_FAILED(NS_DispatchToMainThread(
+ new DestroyRunnable(mSession.forget())))) {
+ MOZ_ASSERT(false, "NS_DispatchToMainThread DestroyRunnable failed");
+ }
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<Session> mSession;
+ };
+
+ // For Ensure recorder has tracks to record.
+ class TracksAvailableCallback : public OnTracksAvailableCallback
+ {
+ public:
+ explicit TracksAvailableCallback(Session *aSession, TrackRate aTrackRate)
+ : mSession(aSession)
+ , mTrackRate(aTrackRate) {}
+
+ 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, mTrackRate);
+ }
+ private:
+ RefPtr<Session> mSession;
+ TrackRate mTrackRate;
+ };
+ // Main thread task.
+ // To delete RecordingSession object.
+ class DestroyRunnable : public Runnable
+ {
+ public:
+ explicit DestroyRunnable(Session* aSession)
+ : mSession(aSession) {}
+
+ explicit DestroyRunnable(already_AddRefed<Session> aSession)
+ : mSession(aSession) {}
+
+ NS_IMETHOD Run() override
+ {
+ LOG(LogLevel::Debug, ("Session.DestroyRunnable session refcnt = (%d) stopIssued %d s=(%p)",
+ (int)mSession->mRefCnt, mSession->mStopIssued, 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->mStopIssued) {
+ ErrorResult result;
+ mSession->mStopIssued = true;
+ recorder->Stop(result);
+ if (NS_FAILED(NS_DispatchToMainThread(new DestroyRunnable(mSession.forget())))) {
+ MOZ_ASSERT(false, "NS_DispatchToMainThread failed");
+ }
+ return NS_OK;
+ }
+
+ // Dispatch stop event and clear MIME type.
+ mSession->mMimeType = NS_LITERAL_STRING("");
+ recorder->SetMimeType(mSession->mMimeType);
+ recorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop"));
+ mSession->BreakCycle();
+ return NS_OK;
+ }
+
+ private:
+ // Call mSession::Release automatically while DestroyRunnable be destroy.
+ RefPtr<Session> mSession;
+ };
+
+ friend class EncoderErrorNotifierRunnable;
+ friend class PushBlobRunnable;
+ friend class ExtractRunnable;
+ friend class DestroyRunnable;
+ friend class TracksAvailableCallback;
+
+public:
+ Session(MediaRecorder* aRecorder, int32_t aTimeSlice)
+ : mRecorder(aRecorder)
+ , mTimeSlice(aTimeSlice)
+ , mStopIssued(false)
+ , mIsStartEventFired(false)
+ , mIsRegisterProfiler(false)
+ , mNeedSessionEndTask(true)
+ , mSelectedVideoTrackID(TRACK_NONE)
+ {
+ 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
+ {
+ // Handle possible early removal of direct video listener
+ if (mEncoder) {
+ RefPtr<VideoStreamTrack> videoTrack = aTrack->AsVideoStreamTrack();
+ if (videoTrack) {
+ videoTrack->RemoveDirectListener(mEncoder->GetVideoSink());
+ }
+ }
+
+ 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();
+ TrackRate trackRate = gm->GraphRate();
+ mTrackUnionStream = gm->CreateTrackUnionStream();
+ MOZ_ASSERT(mTrackUnionStream, "CreateTrackUnionStream failed");
+
+ mTrackUnionStream->SetAutofinish(true);
+
+ DOMMediaStream* domStream = mRecorder->Stream();
+ if (domStream) {
+ // 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, trackRate);
+ 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, trackRate);
+ }
+ }
+
+ void Stop()
+ {
+ LOG(LogLevel::Debug, ("Session.Stop %p", this));
+ MOZ_ASSERT(NS_IsMainThread());
+ mStopIssued = true;
+ CleanupStreams();
+ if (mNeedSessionEndTask) {
+ LOG(LogLevel::Debug, ("Session.Stop mNeedSessionEndTask %p", this));
+ // End the Session directly if there is no ExtractRunnable.
+ DoSessionEndTask(NS_OK);
+ }
+ // If we don't do this, the Session will be purged only when the navigator exit
+ // by the ShutdownObserver and the memory and number of threads will quickly
+ // grows with each couple stop/start.
+ nsContentUtils::UnregisterShutdownObserver(this);
+ }
+
+ nsresult Pause()
+ {
+ LOG(LogLevel::Debug, ("Session.Pause"));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE);
+ mTrackUnionStream->Suspend();
+ if (mEncoder) {
+ mEncoder->Suspend();
+ }
+
+ return NS_OK;
+ }
+
+ nsresult Resume()
+ {
+ LOG(LogLevel::Debug, ("Session.Resume"));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ NS_ENSURE_TRUE(mTrackUnionStream, NS_ERROR_FAILURE);
+ if (mEncoder) {
+ mEncoder->Resume();
+ }
+ mTrackUnionStream->Resume();
+
+ return NS_OK;
+ }
+
+ nsresult RequestData()
+ {
+ LOG(LogLevel::Debug, ("Session.RequestData"));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_FAILED(NS_DispatchToMainThread(new EncoderErrorNotifierRunnable(this))) ||
+ NS_FAILED(NS_DispatchToMainThread(new PushBlobRunnable(this)))) {
+ MOZ_ASSERT(false, "RequestData NS_DispatchToMainThread failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+ }
+
+ already_AddRefed<nsIDOMBlob> GetEncodedData()
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mEncodedBufferCache->ExtractBlob(mRecorder->GetParentObject(),
+ mMimeType);
+ }
+
+ bool IsEncoderError()
+ {
+ if (mEncoder && mEncoder->HasError()) {
+ return true;
+ }
+ return false;
+ }
+
+ size_t
+ SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
+ {
+ 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();
+ if (mReadThread) {
+ mReadThread->Shutdown();
+ mReadThread = nullptr;
+ // Inside the if() so that if we delete after xpcom-shutdown's Observe(), we
+ // won't try to remove it after the observer service is shut down.
+ // Unregistering for safety in case Stop() was never called
+ nsContentUtils::UnregisterShutdownObserver(this);
+ }
+ }
+ // 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)
+ {
+ MOZ_ASSERT(NS_GetCurrentThread() == mReadThread);
+ LOG(LogLevel::Debug, ("Session.Extract %p", this));
+
+ if (!mIsRegisterProfiler) {
+ char aLocal;
+ profiler_register_thread("Media_Encoder", &aLocal);
+ mIsRegisterProfiler = true;
+ }
+
+ PROFILER_LABEL("MediaRecorder", "Session Extract",
+ js::ProfileEntry::Category::OTHER);
+
+ // Pull encoded media data from MediaEncoder
+ nsTArray<nsTArray<uint8_t> > encodedBuf;
+ mEncoder->GetEncodedData(&encodedBuf, mMimeType);
+
+ // Append pulled data into cache buffer.
+ for (uint32_t i = 0; i < encodedBuf.Length(); i++) {
+ if (!encodedBuf[i].IsEmpty()) {
+ mEncodedBufferCache->AppendBuffer(encodedBuf[i]);
+ // Fire the start event when encoded data is available.
+ if (!mIsStartEventFired) {
+ NS_DispatchToMainThread(
+ new DispatchStartEventRunnable(this, NS_LITERAL_STRING("start")));
+ mIsStartEventFired = true;
+ }
+ }
+ }
+
+ // Whether push encoded data back to onDataAvailable automatically or we
+ // need a flush.
+ bool pushBlob = false;
+ if ((mTimeSlice > 0) &&
+ ((TimeStamp::Now()-mLastBlobTimeStamp).ToMilliseconds() > mTimeSlice)) {
+ pushBlob = true;
+ }
+ if (pushBlob || aForceFlush) {
+ // Fire the start event before the blob.
+ if (!mIsStartEventFired) {
+ NS_DispatchToMainThread(
+ new DispatchStartEventRunnable(this, NS_LITERAL_STRING("start")));
+ mIsStartEventFired = true;
+ }
+ 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");
+ } 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()
+ ? mRecorder->mAudioNode->GetOwner()->GetExtantDoc()
+ : nullptr;
+ nsCOMPtr<nsIPrincipal> principal = doc ? doc->NodePrincipal() : nullptr;
+ return PrincipalSubsumes(principal);
+ }
+
+ bool CheckPermission(const char* aType)
+ {
+ if (!mRecorder || !mRecorder->GetOwner()) {
+ return false;
+ }
+
+ nsCOMPtr<nsIDocument> doc = mRecorder->GetOwner()->GetExtantDoc();
+ if (!doc) {
+ return false;
+ }
+
+ // Certified applications can always assign AUDIO_3GPP
+ if (doc->NodePrincipal()->GetAppStatus() ==
+ nsIPrincipal::APP_STATUS_CERTIFIED) {
+ return true;
+ }
+
+ nsCOMPtr<nsIPermissionManager> pm =
+ do_GetService(NS_PERMISSIONMANAGER_CONTRACTID);
+
+ if (!pm) {
+ return false;
+ }
+
+ uint32_t perm = nsIPermissionManager::DENY_ACTION;
+ pm->TestExactPermissionFromPrincipal(doc->NodePrincipal(), aType, &perm);
+ return perm == nsIPermissionManager::ALLOW_ACTION;
+ }
+
+ void InitEncoder(uint8_t aTrackTypes, TrackRate aTrackRate)
+ {
+ LOG(LogLevel::Debug, ("Session.InitEncoder %p", this));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mRecorder) {
+ LOG(LogLevel::Debug, ("Session.InitEncoder failure, mRecorder is null %p", this));
+ return;
+ }
+ // Allocate encoder and bind with union stream.
+ // At this stage, the API doesn't allow UA to choose the output mimeType format.
+
+ // Make sure the application has permission to assign AUDIO_3GPP
+ if (mRecorder->mMimeType.EqualsLiteral(AUDIO_3GPP) && CheckPermission("audio-capture:3gpp")) {
+ mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(AUDIO_3GPP),
+ mRecorder->GetAudioBitrate(),
+ mRecorder->GetVideoBitrate(),
+ mRecorder->GetBitrate(),
+ aTrackTypes, aTrackRate);
+ } else if (mRecorder->mMimeType.EqualsLiteral(AUDIO_3GPP2) && CheckPermission("audio-capture:3gpp2")) {
+ mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(AUDIO_3GPP2),
+ mRecorder->GetAudioBitrate(),
+ mRecorder->GetVideoBitrate(),
+ mRecorder->GetBitrate(),
+ aTrackTypes, aTrackRate);
+ } else {
+ mEncoder = MediaEncoder::CreateEncoder(NS_LITERAL_STRING(""),
+ mRecorder->GetAudioBitrate(),
+ mRecorder->GetVideoBitrate(),
+ mRecorder->GetBitrate(),
+ aTrackTypes, aTrackRate);
+ }
+
+ if (!mEncoder) {
+ LOG(LogLevel::Debug, ("Session.InitEncoder !mEncoder %p", this));
+ DoSessionEndTask(NS_ERROR_ABORT);
+ return;
+ }
+
+ // Media stream is ready but UA issues a stop method follow by start method.
+ // The Session::stop would clean the mTrackUnionStream. If the AfterTracksAdded
+ // comes after stop command, this function would crash.
+ if (!mTrackUnionStream) {
+ LOG(LogLevel::Debug, ("Session.InitEncoder !mTrackUnionStream %p", this));
+ DoSessionEndTask(NS_OK);
+ return;
+ }
+ mTrackUnionStream->AddListener(mEncoder.get());
+
+ nsTArray<RefPtr<mozilla::dom::VideoStreamTrack>> videoTracks;
+ DOMMediaStream* domStream = mRecorder->Stream();
+ if (domStream) {
+ domStream->GetVideoTracks(videoTracks);
+ if (!videoTracks.IsEmpty()) {
+ // Right now, the MediaRecorder hasn't dealt with multiple video track
+ // issues. So we just bind with the first video track. Bug 1276928 is
+ // the following.
+ videoTracks[0]->AddDirectListener(mEncoder->GetVideoSink());
+ }
+ }
+
+ // Try to use direct listeners if possible
+ if (domStream && domStream->GetInputStream()) {
+ mInputStream = domStream->GetInputStream()->AsSourceStream();
+ if (mInputStream) {
+ mInputStream->AddDirectListener(mEncoder.get());
+ mEncoder->SetDirectConnect(true);
+ }
+ }
+
+ // Create a thread to read encode media data from MediaEncoder.
+ if (!mReadThread) {
+ nsresult rv = NS_NewNamedThread("Media_Encoder", getter_AddRefs(mReadThread));
+ if (NS_FAILED(rv)) {
+ LOG(LogLevel::Debug, ("Session.InitEncoder !mReadThread %p", this));
+ DoSessionEndTask(rv);
+ return;
+ }
+ }
+
+ // In case source media stream does not notify track end, receive
+ // shutdown notification and stop Read Thread.
+ nsContentUtils::RegisterShutdownObserver(this);
+
+ nsCOMPtr<nsIRunnable> event = new ExtractRunnable(this);
+ if (NS_FAILED(mReadThread->Dispatch(event, NS_DISPATCH_NORMAL))) {
+ NS_WARNING("Failed to dispatch ExtractRunnable at beginning");
+ LOG(LogLevel::Debug, ("Session.InitEncoder !ReadThread->Dispatch %p", this));
+ DoSessionEndTask(NS_ERROR_ABORT);
+ }
+ // Set mNeedSessionEndTask to false because the
+ // ExtractRunnable/DestroyRunnable will take the response to
+ // end the session.
+ mNeedSessionEndTask = false;
+ }
+ // application should get blob and onstop event
+ void DoSessionEndTask(nsresult rv)
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ CleanupStreams();
+ NS_DispatchToMainThread(
+ new DispatchStartEventRunnable(this, NS_LITERAL_STRING("start")));
+
+ if (NS_FAILED(rv)) {
+ NS_DispatchToMainThread(NewRunnableMethod<nsresult>(mRecorder,
+ &MediaRecorder::NotifyError, rv));
+ }
+ if (NS_FAILED(NS_DispatchToMainThread(new EncoderErrorNotifierRunnable(this)))) {
+ MOZ_ASSERT(false, "NS_DispatchToMainThread EncoderErrorNotifierRunnable 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 (mInputStream) {
+ if (mEncoder) {
+ mInputStream->RemoveDirectListener(mEncoder.get());
+ }
+ mInputStream = nullptr;
+ }
+ for (RefPtr<MediaInputPort>& inputPort : mInputPorts) {
+ MOZ_ASSERT(inputPort);
+ inputPort->Destroy();
+ }
+ mInputPorts.Clear();
+
+ if (mTrackUnionStream) {
+ if (mEncoder) {
+ nsTArray<RefPtr<mozilla::dom::VideoStreamTrack>> videoTracks;
+ DOMMediaStream* domStream = mRecorder->Stream();
+ if (domStream) {
+ domStream->GetVideoTracks(videoTracks);
+ if (!videoTracks.IsEmpty()) {
+ videoTracks[0]->RemoveDirectListener(mEncoder->GetVideoSink());
+ }
+ }
+ }
+
+ // Sometimes the MediaEncoder might be initialized fail and go to
+ // |CleanupStreams|. So the mEncoder might be a nullptr in this case.
+ if (mEncoder && mSelectedVideoTrackID != TRACK_NONE) {
+ mTrackUnionStream->RemoveVideoOutput(mEncoder->GetVideoSink(), mSelectedVideoTrackID);
+ }
+ if (mEncoder) {
+ mTrackUnionStream->RemoveListener(mEncoder.get());
+ }
+ mTrackUnionStream->Destroy();
+ mTrackUnionStream = nullptr;
+ }
+
+ if (mMediaStream) {
+ mMediaStream->UnregisterTrackListener(this);
+ mMediaStream = nullptr;
+ }
+
+ for (RefPtr<MediaStreamTrack>& track : mMediaStreamTracks) {
+ track->RemovePrincipalChangeObserver(this);
+ }
+ mMediaStreamTracks.Clear();
+ }
+
+ NS_IMETHOD 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.
+ mEncoder->Cancel();
+ if (mReadThread) {
+ mReadThread->Shutdown();
+ mReadThread = nullptr;
+ }
+ nsContentUtils::UnregisterShutdownObserver(this);
+ BreakCycle();
+ Stop();
+ }
+
+ return NS_OK;
+ }
+
+ // Break the cycle reference between Session and MediaRecorder.
+ void BreakCycle()
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mRecorder) {
+ mRecorder->RemoveSession(this);
+ mRecorder = nullptr;
+ }
+ }
+
+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<SourceMediaStream> mInputStream;
+ 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
+ nsString mMimeType;
+ // Timestamp of the last fired dataavailable event.
+ TimeStamp mLastBlobTimeStamp;
+ // The interval of passing encoded data from EncodedBufferCache 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;
+ // Indicate this session's stop has been called.
+ bool mStopIssued;
+ // Indicate the session had fire start event. Encoding thread only.
+ bool mIsStartEventFired;
+ // The register flag for "Media_Encoder" thread to profiler
+ bool mIsRegisterProfiler;
+ // False if the InitEncoder called successfully, ensure the
+ // ExtractRunnable/DestroyRunnable will end the session.
+ // Main thread only.
+ bool mNeedSessionEndTask;
+ TrackID mSelectedVideoTrackID;
+};
+
+NS_IMPL_ISUPPORTS(MediaRecorder::Session, nsIObserver)
+
+MediaRecorder::~MediaRecorder()
+{
+ if (mPipeStream != nullptr) {
+ mInputPort->Destroy();
+ mPipeStream->Destroy();
+ }
+ LOG(LogLevel::Debug, ("~MediaRecorder (%p)", this));
+ UnRegisterActivityObserver();
+}
+
+MediaRecorder::MediaRecorder(DOMMediaStream& aSourceMediaStream,
+ nsPIDOMWindowInner* aOwnerWindow)
+ : DOMEventTargetHelper(aOwnerWindow)
+ , mState(RecordingState::Inactive)
+{
+ MOZ_ASSERT(aOwnerWindow);
+ MOZ_ASSERT(aOwnerWindow->IsInnerWindow());
+ mDOMStream = &aSourceMediaStream;
+
+ RegisterActivityObserver();
+}
+
+MediaRecorder::MediaRecorder(AudioNode& aSrcAudioNode,
+ uint32_t aSrcOutput,
+ nsPIDOMWindowInner* aOwnerWindow)
+ : DOMEventTargetHelper(aOwnerWindow)
+ , mState(RecordingState::Inactive)
+{
+ MOZ_ASSERT(aOwnerWindow);
+ MOZ_ASSERT(aOwnerWindow->IsInnerWindow());
+
+ // Only AudioNodeStream of kind EXTERNAL_STREAM stores output audio data in
+ // the track (see AudioNodeStream::AdvanceOutputSegment()). That means track
+ // union stream in recorder session won't be able to copy data from the
+ // stream of non-destination node. Create a pipe stream in this case.
+ if (aSrcAudioNode.NumberOfOutputs() > 0) {
+ AudioContext* ctx = aSrcAudioNode.Context();
+ AudioNodeEngine* engine = new AudioNodeEngine(nullptr);
+ AudioNodeStream::Flags flags =
+ AudioNodeStream::EXTERNAL_OUTPUT |
+ AudioNodeStream::NEED_MAIN_THREAD_FINISHED;
+ mPipeStream = AudioNodeStream::Create(ctx, engine, flags, ctx->Graph());
+ AudioNodeStream* ns = aSrcAudioNode.GetStream();
+ if (ns) {
+ mInputPort =
+ mPipeStream->AllocateInputPort(aSrcAudioNode.GetStream(),
+ TRACK_ANY, TRACK_ANY,
+ 0, aSrcOutput);
+ }
+ }
+ 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));
+ if (mState != RecordingState::Inactive) {
+ aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (GetSourceMediaStream()->IsFinished() || GetSourceMediaStream()->IsDestroyed()) {
+ 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;
+ nsIDocument* 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();
+}
+
+void
+MediaRecorder::Stop(ErrorResult& aResult)
+{
+ LOG(LogLevel::Debug, ("MediaRecorder.Stop %p", this));
+ MediaRecorderReporter::RemoveMediaRecorder(this);
+ if (mState == RecordingState::Inactive) {
+ aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ mState = RecordingState::Inactive;
+ MOZ_ASSERT(mSessions.Length() > 0);
+ mSessions.LastElement()->Stop();
+}
+
+void
+MediaRecorder::Pause(ErrorResult& aResult)
+{
+ LOG(LogLevel::Debug, ("MediaRecorder.Pause"));
+ if (mState != RecordingState::Recording) {
+ aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ 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"));
+ if (mState != RecordingState::Paused) {
+ aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ 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::Recording) {
+ 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 MediaRecorderBinding::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 gWebMAudioEncoderCodecs[2] = {
+ "opus",
+ // no VP9 yet
+ nullptr,
+};
+static char const *const gWebMVideoEncoderCodecs[4] = {
+ "opus",
+ "vp8",
+ "vp8.0",
+ // no VP9 yet
+ 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) &&
+ MediaEncoder::IsWebMEncoderEnabled()) {
+ 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(already_AddRefed<nsIDOMBlob>&& aBlob)
+{
+ MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
+
+ BlobEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+
+ nsCOMPtr<nsIDOMBlob> blob = aBlob;
+ init.mData = static_cast<Blob*>(blob.get());
+
+ RefPtr<BlobEvent> event =
+ BlobEvent::Constructor(this,
+ NS_LITERAL_STRING("dataavailable"),
+ init);
+ event->SetTrusted(true);
+ return DispatchDOMEvent(nullptr, event, nullptr, nullptr);
+}
+
+void
+MediaRecorder::DispatchSimpleEvent(const nsAString & aStr)
+{
+ MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
+ nsresult rv = CheckInnerWindowCorrectness();
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
+ event->InitEvent(aStr, false, false);
+ event->SetTrusted(true);
+
+ rv = DispatchDOMEvent(nullptr, event, nullptr, nullptr);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Failed to dispatch the event!!!");
+ return;
+ }
+}
+
+void
+MediaRecorder::NotifyError(nsresult aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
+ nsresult rv = CheckInnerWindowCorrectness();
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ nsString errorMsg;
+ switch (aRv) {
+ case NS_ERROR_DOM_SECURITY_ERR:
+ errorMsg = NS_LITERAL_STRING("SecurityError");
+ break;
+ case NS_ERROR_OUT_OF_MEMORY:
+ errorMsg = NS_LITERAL_STRING("OutOfMemoryError");
+ break;
+ default:
+ errorMsg = NS_LITERAL_STRING("GenericError");
+ }
+
+ RecordErrorEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ init.mName = errorMsg;
+
+ RefPtr<RecordErrorEvent> event =
+ RecordErrorEvent::Constructor(this, NS_LITERAL_STRING("error"), init);
+ event->SetTrusted(true);
+
+ rv = DispatchDOMEvent(nullptr, event, nullptr, nullptr);
+ if (NS_FAILED(rv)) {
+ NS_ERROR("Failed to dispatch the error event!!!");
+ return;
+ }
+ return;
+}
+
+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);
+ nsIDocument* doc = window->GetExtantDoc();
+ NS_ENSURE_TRUE_VOID(doc);
+
+ LOG(LogLevel::Debug, ("MediaRecorder %p document IsActive %d isVisible %d\n",
+ this, doc->IsActive(), doc->IsVisible()));
+ if (!doc->IsActive() || !doc->IsVisible()) {
+ // Stop the session.
+ ErrorResult result;
+ Stop(result);
+ result.SuppressException();
+ }
+}
+
+MediaStream*
+MediaRecorder::GetSourceMediaStream()
+{
+ if (mDOMStream != nullptr) {
+ return mDOMStream->GetPlaybackStream();
+ }
+ MOZ_ASSERT(mAudioNode != nullptr);
+ return mPipeStream ? mPipeStream.get() : mAudioNode->GetStream();
+}
+
+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;
+}
+
+StaticRefPtr<MediaRecorderReporter> MediaRecorderReporter::sUniqueInstance;
+
+MediaRecorderReporter* MediaRecorderReporter::UniqueInstance()
+{
+ if (!sUniqueInstance) {
+ sUniqueInstance = new MediaRecorderReporter();
+ sUniqueInstance->InitMemoryReporter();
+ }
+ return sUniqueInstance;
+ }
+
+void MediaRecorderReporter::InitMemoryReporter()
+{
+ RegisterWeakMemoryReporter(this);
+}
+
+MediaRecorderReporter::~MediaRecorderReporter()
+{
+ UnregisterWeakMemoryReporter(this);
+}
+
+} // namespace dom
+} // namespace mozilla