summaryrefslogtreecommitdiffstats
path: root/dom/media/MediaRecorder.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/MediaRecorder.cpp')
-rw-r--r--dom/media/MediaRecorder.cpp1894
1 files changed, 1894 insertions, 0 deletions
diff --git a/dom/media/MediaRecorder.cpp b/dom/media/MediaRecorder.cpp
new file mode 100644
index 0000000000..9f8c2ad425
--- /dev/null
+++ b/dom/media/MediaRecorder.cpp
@@ -0,0 +1,1894 @@
+/* -*- 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 "AudioNodeTrack.h"
+#include "DOMMediaStream.h"
+#include "MediaDecoder.h"
+#include "MediaEncoder.h"
+#include "MediaTrackGraphImpl.h"
+#include "VideoUtils.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/AudioStreamTrack.h"
+#include "mozilla/dom/BlobEvent.h"
+#include "mozilla/dom/EmptyBlobImpl.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/MediaRecorderErrorEvent.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 "nsContentTypeParser.h"
+#include "nsContentUtils.h"
+#include "nsDocShell.h"
+#include "nsError.h"
+#include "mozilla/dom/Document.h"
+#include "nsIPrincipal.h"
+#include "nsIScriptError.h"
+#include "nsMimeTypes.h"
+#include "nsProxyRelease.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTArray.h"
+
+mozilla::LazyLogModule gMediaRecorderLog("MediaRecorder");
+#define LOG(type, msg) MOZ_LOG(gMediaRecorderLog, type, msg)
+
+constexpr int MIN_VIDEO_BITRATE_BPS = 10e3; // 10kbps
+constexpr int DEFAULT_VIDEO_BITRATE_BPS = 2500e3; // 2.5Mbps
+constexpr int MAX_VIDEO_BITRATE_BPS = 100e6; // 100Mbps
+
+constexpr int MIN_AUDIO_BITRATE_BPS = 500; // 500bps
+constexpr int DEFAULT_AUDIO_BITRATE_BPS = 128e3; // 128kbps
+constexpr int MAX_AUDIO_BITRATE_BPS = 512e3; // 512kbps
+
+namespace mozilla::dom {
+
+using namespace mozilla::media;
+
+/**
+ * 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(GetCurrentSerialEventTarget(), promises)
+ ->Then(
+ GetCurrentSerialEventTarget(), __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(""_ns, "explicit/media/recorder"_ns,
+ KIND_HEAP, UNITS_BYTES, sum,
+ "Memory used by media recorder."_ns, 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(mStream)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioNode)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOtherDomException)
+ 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(mStream)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioNode)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOtherDomException)
+ 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)
+
+namespace {
+bool PrincipalSubsumes(MediaRecorder* aRecorder, nsIPrincipal* aPrincipal) {
+ if (!aRecorder->GetOwner()) {
+ return false;
+ }
+ nsCOMPtr<Document> doc = aRecorder->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(
+ MediaRecorder* aRecorder,
+ const nsTArray<RefPtr<MediaStreamTrack>>& aTracks) {
+ nsCOMPtr<nsIPrincipal> principal = nullptr;
+ for (const auto& track : aTracks) {
+ nsContentUtils::CombineResourcePrincipals(&principal,
+ track->GetPrincipal());
+ }
+ return PrincipalSubsumes(aRecorder, principal);
+}
+
+bool AudioNodePrincipalSubsumes(MediaRecorder* aRecorder,
+ AudioNode* aAudioNode) {
+ MOZ_ASSERT(aAudioNode);
+ Document* doc =
+ aAudioNode->GetOwner() ? aAudioNode->GetOwner()->GetExtantDoc() : nullptr;
+ nsCOMPtr<nsIPrincipal> principal = doc ? doc->NodePrincipal() : nullptr;
+ return PrincipalSubsumes(aRecorder, principal);
+}
+
+// This list is sorted so that lesser failures are later, so that
+// IsTypeSupportedImpl() can report the error from audio or video types that
+// is closer to being supported.
+enum class TypeSupport {
+ MediaTypeInvalid,
+ NoVideoWithAudioType,
+ ContainersDisabled,
+ CodecsDisabled,
+ ContainerUnsupported,
+ CodecUnsupported,
+ CodecDuplicated,
+ Supported,
+};
+
+nsCString TypeSupportToCString(TypeSupport aSupport,
+ const nsAString& aMimeType) {
+ nsAutoCString mime = NS_ConvertUTF16toUTF8(aMimeType);
+ switch (aSupport) {
+ case TypeSupport::Supported:
+ return nsPrintfCString("%s is supported", mime.get());
+ case TypeSupport::MediaTypeInvalid:
+ return nsPrintfCString("%s is not a valid media type", mime.get());
+ case TypeSupport::NoVideoWithAudioType:
+ return nsPrintfCString(
+ "Video cannot be recorded with %s as it is an audio type",
+ mime.get());
+ case TypeSupport::ContainersDisabled:
+ return "All containers are disabled"_ns;
+ case TypeSupport::CodecsDisabled:
+ return "All codecs are disabled"_ns;
+ case TypeSupport::ContainerUnsupported:
+ return nsPrintfCString("%s indicates an unsupported container",
+ mime.get());
+ case TypeSupport::CodecUnsupported:
+ return nsPrintfCString("%s indicates an unsupported codec", mime.get());
+ case TypeSupport::CodecDuplicated:
+ return nsPrintfCString("%s contains the same codec multiple times",
+ mime.get());
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown TypeSupport");
+ return "Unknown error"_ns;
+ }
+}
+
+TypeSupport CanRecordAudioTrackWith(const Maybe<MediaContainerType>& aMimeType,
+ const nsAString& aMimeTypeString) {
+ if (aMimeTypeString.IsEmpty()) {
+ // For the empty string we just need to check whether we have support for an
+ // audio container and an audio codec.
+ if (!MediaEncoder::IsWebMEncoderEnabled() &&
+ !MediaDecoder::IsOggEnabled()) {
+ // No container support for audio.
+ return TypeSupport::ContainersDisabled;
+ }
+
+ if (!MediaDecoder::IsOpusEnabled()) {
+ // No codec support for audio.
+ return TypeSupport::CodecsDisabled;
+ }
+
+ return TypeSupport::Supported;
+ }
+
+ if (!aMimeType) {
+ // A mime type string was set, but it couldn't be parsed to a valid
+ // MediaContainerType.
+ return TypeSupport::MediaTypeInvalid;
+ }
+
+ if (aMimeType->Type() != MEDIAMIMETYPE(VIDEO_WEBM) &&
+ aMimeType->Type() != MEDIAMIMETYPE(AUDIO_WEBM) &&
+ aMimeType->Type() != MEDIAMIMETYPE(AUDIO_OGG)) {
+ // Any currently supported container can record audio.
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (aMimeType->Type() == MEDIAMIMETYPE(VIDEO_WEBM) &&
+ !MediaEncoder::IsWebMEncoderEnabled()) {
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (aMimeType->Type() == MEDIAMIMETYPE(AUDIO_WEBM) &&
+ !MediaEncoder::IsWebMEncoderEnabled()) {
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (aMimeType->Type() == MEDIAMIMETYPE(AUDIO_OGG) &&
+ !MediaDecoder::IsOggEnabled()) {
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (!MediaDecoder::IsOpusEnabled()) {
+ return TypeSupport::CodecUnsupported;
+ }
+
+ if (!aMimeType->ExtendedType().HaveCodecs()) {
+ // No codecs constrained, we can pick opus.
+ return TypeSupport::Supported;
+ }
+
+ size_t opus = 0;
+ size_t unknown = 0;
+ for (const auto& codec : aMimeType->ExtendedType().Codecs().Range()) {
+ // Ignore video codecs.
+ if (codec.EqualsLiteral("vp8")) {
+ continue;
+ }
+ if (codec.EqualsLiteral("vp8.0")) {
+ continue;
+ }
+ if (codec.EqualsLiteral("opus")) {
+ // All containers support opus
+ opus++;
+ continue;
+ }
+ unknown++;
+ }
+
+ if (unknown > 0) {
+ // Unsupported codec.
+ return TypeSupport::CodecUnsupported;
+ }
+
+ if (opus == 0) {
+ // Codecs specified but not opus. Unsupported for audio.
+ return TypeSupport::CodecUnsupported;
+ }
+
+ if (opus > 1) {
+ // Opus specified more than once. Bad form.
+ return TypeSupport::CodecDuplicated;
+ }
+
+ return TypeSupport::Supported;
+}
+
+TypeSupport CanRecordVideoTrackWith(const Maybe<MediaContainerType>& aMimeType,
+ const nsAString& aMimeTypeString) {
+ if (aMimeTypeString.IsEmpty()) {
+ // For the empty string we just need to check whether we have support for a
+ // video container and a video codec. The VP8 encoder is always available.
+ if (!MediaEncoder::IsWebMEncoderEnabled()) {
+ // No container support for video.
+ return TypeSupport::ContainersDisabled;
+ }
+
+ return TypeSupport::Supported;
+ }
+
+ if (!aMimeType) {
+ // A mime type string was set, but it couldn't be parsed to a valid
+ // MediaContainerType.
+ return TypeSupport::MediaTypeInvalid;
+ }
+
+ if (!aMimeType->Type().HasVideoMajorType()) {
+ return TypeSupport::NoVideoWithAudioType;
+ }
+
+ if (aMimeType->Type() != MEDIAMIMETYPE(VIDEO_WEBM)) {
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (!MediaEncoder::IsWebMEncoderEnabled()) {
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (!aMimeType->ExtendedType().HaveCodecs()) {
+ // No codecs constrained, we can pick vp8.
+ return TypeSupport::Supported;
+ }
+
+ size_t vp8 = 0;
+ size_t unknown = 0;
+ for (const auto& codec : aMimeType->ExtendedType().Codecs().Range()) {
+ if (codec.EqualsLiteral("opus")) {
+ // Ignore audio codecs.
+ continue;
+ }
+ if (codec.EqualsLiteral("vp8")) {
+ vp8++;
+ continue;
+ }
+ if (codec.EqualsLiteral("vp8.0")) {
+ vp8++;
+ continue;
+ }
+ unknown++;
+ }
+
+ if (unknown > 0) {
+ // Unsupported codec.
+ return TypeSupport::CodecUnsupported;
+ }
+
+ if (vp8 == 0) {
+ // Codecs specified but not vp8. Unsupported for video.
+ return TypeSupport::CodecUnsupported;
+ }
+
+ if (vp8 > 1) {
+ // Vp8 specified more than once. Bad form.
+ return TypeSupport::CodecDuplicated;
+ }
+
+ return TypeSupport::Supported;
+}
+
+TypeSupport CanRecordWith(MediaStreamTrack* aTrack,
+ const Maybe<MediaContainerType>& aMimeType,
+ const nsAString& aMimeTypeString) {
+ if (aTrack->AsAudioStreamTrack()) {
+ return CanRecordAudioTrackWith(aMimeType, aMimeTypeString);
+ }
+
+ if (aTrack->AsVideoStreamTrack()) {
+ return CanRecordVideoTrackWith(aMimeType, aMimeTypeString);
+ }
+
+ MOZ_CRASH("Unexpected track type");
+}
+
+TypeSupport IsTypeSupportedImpl(const nsAString& aMIMEType) {
+ if (aMIMEType.IsEmpty()) {
+ // Lie and return true even if no container/codec support is enabled,
+ // because the spec mandates it.
+ return TypeSupport::Supported;
+ }
+ Maybe<MediaContainerType> mime = MakeMediaContainerType(aMIMEType);
+ TypeSupport audioSupport = CanRecordAudioTrackWith(mime, aMIMEType);
+ TypeSupport videoSupport = CanRecordVideoTrackWith(mime, aMIMEType);
+ return std::max(audioSupport, videoSupport);
+}
+
+nsString SelectMimeType(bool aHasVideo, bool aHasAudio,
+ const nsString& aConstrainedMimeType) {
+ MOZ_ASSERT(aHasVideo || aHasAudio);
+
+ Maybe<MediaContainerType> constrainedType =
+ MakeMediaContainerType(aConstrainedMimeType);
+
+ // If we are recording video, Start() should have rejected any non-video mime
+ // types.
+ MOZ_ASSERT_IF(constrainedType && aHasVideo,
+ constrainedType->Type().HasVideoMajorType());
+ // IsTypeSupported() rejects application mime types.
+ MOZ_ASSERT_IF(constrainedType,
+ !constrainedType->Type().HasApplicationMajorType());
+
+ nsString result;
+ if (constrainedType && constrainedType->ExtendedType().HaveCodecs()) {
+ // The constrained mime type is fully defined (it has codecs!). No need to
+ // select anything.
+ CopyUTF8toUTF16(constrainedType->OriginalString(), result);
+ } else {
+ // There is no constrained mime type, or there is and it is not fully
+ // defined but still valid. Select what's missing, so that we have major
+ // type, container and codecs.
+
+ // If there is a constrained mime type it should not have codecs defined,
+ // because then it is fully defined and used unchanged (covered earlier).
+ MOZ_ASSERT_IF(constrainedType,
+ !constrainedType->ExtendedType().HaveCodecs());
+
+ nsCString majorType;
+ {
+ if (constrainedType) {
+ // There is a constrained type. It has both major type and container in
+ // order to be valid. Use them as is.
+ majorType = constrainedType->Type().AsString();
+ } else if (aHasVideo) {
+ majorType = nsLiteralCString(VIDEO_WEBM);
+ } else {
+ majorType = nsLiteralCString(AUDIO_OGG);
+ }
+ }
+
+ nsCString codecs;
+ {
+ if (aHasVideo && aHasAudio) {
+ codecs = "\"vp8, opus\""_ns;
+ } else if (aHasVideo) {
+ codecs = "vp8"_ns;
+ } else {
+ codecs = "opus"_ns;
+ }
+ }
+ result = NS_ConvertUTF8toUTF16(
+ nsPrintfCString("%s; codecs=%s", majorType.get(), codecs.get()));
+ }
+
+ MOZ_ASSERT_IF(aHasAudio,
+ CanRecordAudioTrackWith(MakeMediaContainerType(result),
+ result) == TypeSupport::Supported);
+ MOZ_ASSERT_IF(aHasVideo,
+ CanRecordVideoTrackWith(MakeMediaContainerType(result),
+ result) == TypeSupport::Supported);
+ return result;
+}
+
+void SelectBitrates(uint32_t aBitsPerSecond, uint8_t aNumVideoTracks,
+ uint32_t* aOutVideoBps, uint8_t aNumAudioTracks,
+ uint32_t* aOutAudioBps) {
+ uint32_t vbps = 0;
+ uint32_t abps = 0;
+
+ const uint32_t minVideoBps = MIN_VIDEO_BITRATE_BPS * aNumVideoTracks;
+ const uint32_t maxVideoBps = MAX_VIDEO_BITRATE_BPS * aNumVideoTracks;
+
+ const uint32_t minAudioBps = MIN_AUDIO_BITRATE_BPS * aNumAudioTracks;
+ const uint32_t maxAudioBps = MAX_AUDIO_BITRATE_BPS * aNumAudioTracks;
+
+ if (aNumVideoTracks == 0) {
+ MOZ_DIAGNOSTIC_ASSERT(aNumAudioTracks > 0);
+ abps = std::min(maxAudioBps, std::max(minAudioBps, aBitsPerSecond));
+ } else if (aNumAudioTracks == 0) {
+ vbps = std::min(maxVideoBps, std::max(minVideoBps, aBitsPerSecond));
+ } else {
+ // Scale the bits so that video gets 20 times the bits of audio.
+ // Since we must account for varying number of tracks of each type we weight
+ // them by type; video = weight 20, audio = weight 1.
+ const uint32_t videoWeight = aNumVideoTracks * 20;
+ const uint32_t audioWeight = aNumAudioTracks;
+ const uint32_t totalWeights = audioWeight + videoWeight;
+ const uint32_t videoBitrate =
+ uint64_t(aBitsPerSecond) * videoWeight / totalWeights;
+ const uint32_t audioBitrate =
+ uint64_t(aBitsPerSecond) * audioWeight / totalWeights;
+ vbps = std::min(maxVideoBps, std::max(minVideoBps, videoBitrate));
+ abps = std::min(maxAudioBps, std::max(minAudioBps, audioBitrate));
+ }
+
+ *aOutVideoBps = vbps;
+ *aOutAudioBps = abps;
+}
+} // namespace
+
+/**
+ * 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 encoder thread. For the same reason, we
+ * do not await encoder thread shutdown in MediaRecorder::Stop.
+ * If someone calls MediaRecorder::Start before encoder thread shutdown, the
+ * same recording context in MediaRecorder might be accessed by two distinct
+ * encoder threads, which would be racy. With the recording context, including
+ * the encoder thread, in a Session object the problem is solved.
+ *
+ * Lifetime of MediaRecorder and Session objects.
+ * 1) MediaRecorder creates a Session in MediaRecorder::Start() and holds
+ * a reference to it. 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 after Session::DoSessionEndTask() has been called
+ * _and_ all encoded media data has been passed to OnDataAvailable handler.
+ * In some cases the encoded media can be discarded before being passed to
+ * the OnDataAvailable handler.
+ * 3) Session::DoSessionEndTask is called by an application through
+ * MediaRecorder::Stop(), from a MediaEncoder Shutdown notification, from the
+ * document going inactive or invisible, or from the ShutdownBlocker.
+ */
+class MediaRecorder::Session : public PrincipalChangeObserver<MediaStreamTrack>,
+ public DOMMediaStream::TrackListener {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Session)
+
+ struct TrackTypeComparator {
+ enum Type {
+ AUDIO,
+ VIDEO,
+ };
+ static bool Equals(const RefPtr<MediaStreamTrack>& aTrack, Type aType) {
+ return (aType == AUDIO && aTrack->AsAudioStreamTrack()) ||
+ (aType == VIDEO && aTrack->AsVideoStreamTrack());
+ }
+ };
+
+ public:
+ Session(MediaRecorder* aRecorder,
+ nsTArray<RefPtr<MediaStreamTrack>> aMediaStreamTracks,
+ uint32_t aVideoBitsPerSecond, uint32_t aAudioBitsPerSecond)
+ : mRecorder(aRecorder),
+ mMediaStreamTracks(std::move(aMediaStreamTracks)),
+ mMainThread(mRecorder->GetOwner()->EventTargetFor(TaskCategory::Other)),
+ mMimeType(SelectMimeType(
+ mMediaStreamTracks.Contains(TrackTypeComparator::VIDEO,
+ TrackTypeComparator()),
+ mRecorder->mAudioNode ||
+ mMediaStreamTracks.Contains(TrackTypeComparator::AUDIO,
+ TrackTypeComparator()),
+ mRecorder->mConstrainedMimeType)),
+ mVideoBitsPerSecond(aVideoBitsPerSecond),
+ mAudioBitsPerSecond(aAudioBitsPerSecond),
+ mStartTime(TimeStamp::Now()),
+ mRunningState(RunningState::Idling) {
+ MOZ_ASSERT(NS_IsMainThread());
+ Telemetry::ScalarAdd(Telemetry::ScalarID::MEDIARECORDER_RECORDING_COUNT, 1);
+ }
+
+ void PrincipalChanged(MediaStreamTrack* aTrack) override {
+ NS_ASSERTION(mMediaStreamTracks.Contains(aTrack),
+ "Principal changed for unrecorded track");
+ if (!MediaStreamTracksPrincipalSubsumes(mRecorder, mMediaStreamTracks)) {
+ 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));
+ // There's a chance we have a sensible JS stack here.
+ if (!mRecorder->mOtherDomException) {
+ mRecorder->mOtherDomException = DOMException::Create(
+ NS_ERROR_DOM_INVALID_MODIFICATION_ERR,
+ "An attempt was made to add a track to the recorded MediaStream "
+ "during the recording"_ns);
+ }
+ DoSessionEndTask(NS_ERROR_DOM_INVALID_MODIFICATION_ERR);
+ }
+
+ void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override {
+ if (aTrack->Ended()) {
+ // TrackEncoder will pickup tracks that end itself.
+ return;
+ }
+ LOG(LogLevel::Warning,
+ ("Session.NotifyTrackRemoved %p Raising error due to track set change",
+ this));
+ // There's a chance we have a sensible JS stack here.
+ if (!mRecorder->mOtherDomException) {
+ mRecorder->mOtherDomException = DOMException::Create(
+ NS_ERROR_DOM_INVALID_MODIFICATION_ERR,
+ "An attempt was made to remove a track from the recorded MediaStream "
+ "during the recording"_ns);
+ }
+ DoSessionEndTask(NS_ERROR_DOM_INVALID_MODIFICATION_ERR);
+ }
+
+ void Start(TimeDuration aTimeslice) {
+ LOG(LogLevel::Debug, ("Session.Start %p", this));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mRecorder->mStream) {
+ // The TrackListener reports back when tracks are added or removed from
+ // the MediaStream.
+ mMediaStream = mRecorder->mStream;
+ mMediaStream->RegisterTrackListener(this);
+
+ uint8_t trackTypes = 0;
+ for (const auto& track : mMediaStreamTracks) {
+ if (track->AsAudioStreamTrack()) {
+ trackTypes |= ContainerWriter::CREATE_AUDIO_TRACK;
+ } else if (track->AsVideoStreamTrack()) {
+ trackTypes |= ContainerWriter::CREATE_VIDEO_TRACK;
+ } else {
+ MOZ_CRASH("Unexpected track type");
+ }
+ }
+
+ for (const auto& t : mMediaStreamTracks) {
+ t->AddPrincipalChangeObserver(this);
+ }
+
+ LOG(LogLevel::Debug, ("Session.Start track types = (%d)", trackTypes));
+ InitEncoder(trackTypes, mMediaStreamTracks[0]->Graph()->GraphRate(),
+ aTimeslice);
+ return;
+ }
+
+ if (mRecorder->mAudioNode) {
+ TrackRate trackRate =
+ mRecorder->mAudioNode->Context()->Graph()->GraphRate();
+
+ // Web Audio node has only audio.
+ InitEncoder(ContainerWriter::CREATE_AUDIO_TRACK, trackRate, aTimeslice);
+ return;
+ }
+
+ MOZ_ASSERT(false, "Unknown source");
+ }
+
+ void Stop() {
+ LOG(LogLevel::Debug, ("Session.Stop %p", this));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mEncoder) {
+ mEncoder->DisconnectTracks();
+ }
+
+ // Remove main thread state added in Start().
+ if (mMediaStream) {
+ mMediaStream->UnregisterTrackListener(this);
+ mMediaStream = nullptr;
+ }
+
+ {
+ for (const auto& track : mMediaStreamTracks) {
+ track->RemovePrincipalChangeObserver(this);
+ }
+ }
+
+ if (mRunningState.isOk() &&
+ mRunningState.inspect() == RunningState::Idling) {
+ LOG(LogLevel::Debug, ("Session.Stop Explicit end task %p", this));
+ // End the Session directly if there is no encoder.
+ DoSessionEndTask(NS_OK);
+ } else if (mRunningState.isOk() &&
+ (mRunningState.inspect() == RunningState::Starting ||
+ mRunningState.inspect() == RunningState::Running)) {
+ if (mRunningState.inspect() == RunningState::Starting) {
+ // The MediaEncoder might not report started, but by spec we must fire
+ // "start".
+ mStartedListener.DisconnectIfExists();
+ NS_DispatchToMainThread(NewRunnableMethod(
+ "MediaRecorder::Session::Stop", this, &Session::OnStarted));
+ }
+ mRunningState = RunningState::Stopping;
+ }
+ }
+
+ void Pause() {
+ LOG(LogLevel::Debug, ("Session.Pause"));
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(mRunningState.isOk(),
+ mRunningState.unwrap() != RunningState::Idling);
+ if (mRunningState.isErr() ||
+ mRunningState.unwrap() == RunningState::Stopping ||
+ mRunningState.unwrap() == RunningState::Stopped) {
+ return;
+ }
+ MOZ_ASSERT(mEncoder);
+ mEncoder->Suspend();
+ }
+
+ void Resume() {
+ LOG(LogLevel::Debug, ("Session.Resume"));
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(mRunningState.isOk(),
+ mRunningState.unwrap() != RunningState::Idling);
+ if (mRunningState.isErr() ||
+ mRunningState.unwrap() == RunningState::Stopping ||
+ mRunningState.unwrap() == RunningState::Stopped) {
+ return;
+ }
+ MOZ_ASSERT(mEncoder);
+ mEncoder->Resume();
+ }
+
+ void RequestData() {
+ LOG(LogLevel::Debug, ("Session.RequestData"));
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mEncoder);
+
+ InvokeAsync(mEncoderThread, mEncoder.get(), __func__,
+ &MediaEncoder::RequestData)
+ ->Then(
+ mMainThread, __func__,
+ [this, self = RefPtr<Session>(this)](
+ const MediaEncoder::BlobPromise::ResolveOrRejectValue& aRrv) {
+ if (aRrv.IsReject()) {
+ LOG(LogLevel::Warning, ("RequestData failed"));
+ DoSessionEndTask(aRrv.RejectValue());
+ return;
+ }
+
+ nsresult rv =
+ mRecorder->CreateAndDispatchBlobEvent(aRrv.ResolveValue());
+ if (NS_FAILED(rv)) {
+ DoSessionEndTask(NS_OK);
+ }
+ });
+ }
+
+ public:
+ RefPtr<SizeOfPromise> SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mEncoder) {
+ return SizeOfPromise::CreateAndResolve(0, __func__);
+ }
+
+ return mEncoder->SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ virtual ~Session() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mShutdownPromise);
+ MOZ_ASSERT(!mShutdownBlocker);
+ LOG(LogLevel::Debug, ("Session.~Session (%p)", this));
+ }
+
+ void InitEncoder(uint8_t aTrackTypes, TrackRate aTrackRate,
+ TimeDuration aTimeslice) {
+ LOG(LogLevel::Debug, ("Session.InitEncoder %p", this));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mRunningState.isOk() ||
+ mRunningState.inspect() != 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_WORKER);
+ if (!pool) {
+ LOG(LogLevel::Debug, ("Session.InitEncoder %p Failed to create "
+ "MediaRecorderReadThread thread pool",
+ this));
+ DoSessionEndTask(NS_ERROR_FAILURE);
+ return;
+ }
+
+ mEncoderThread =
+ TaskQueue::Create(pool.forget(), "MediaRecorderReadThread");
+
+ MOZ_DIAGNOSTIC_ASSERT(!mShutdownBlocker);
+ // Add a shutdown blocker so mEncoderThread can be shutdown async.
+ class Blocker : public ShutdownBlocker {
+ const RefPtr<Session> mSession;
+
+ public:
+ Blocker(RefPtr<Session> aSession, const nsString& aName)
+ : ShutdownBlocker(aName), mSession(std::move(aSession)) {}
+
+ NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient*) override {
+ mSession->DoSessionEndTask(NS_ERROR_ABORT);
+ return NS_OK;
+ }
+ };
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+ if (!barrier) {
+ LOG(LogLevel::Error,
+ ("Session.InitEncoder %p Failed to get shutdown barrier", this));
+ DoSessionEndTask(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsString name;
+ name.AppendPrintf("MediaRecorder::Session %p shutdown", this);
+ mShutdownBlocker = MakeAndAddRef<Blocker>(this, name);
+ nsresult rv = barrier->AddBlocker(
+ mShutdownBlocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
+ u"MediaRecorder::Session: shutdown"_ns);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+
+ uint32_t maxMemory = Preferences::GetUint("media.recorder.max_memory",
+ MAX_ALLOW_MEMORY_BUFFER);
+
+ mEncoder = MediaEncoder::CreateEncoder(
+ mEncoderThread, mMimeType, mAudioBitsPerSecond, mVideoBitsPerSecond,
+ aTrackTypes, aTrackRate, maxMemory, aTimeslice);
+
+ if (!mEncoder) {
+ LOG(LogLevel::Error, ("Session.InitEncoder !mEncoder %p", this));
+ DoSessionEndTask(NS_ERROR_ABORT);
+ return;
+ }
+
+ mStartedListener = mEncoder->StartedEvent().Connect(mMainThread, this,
+ &Session::OnStarted);
+ mDataAvailableListener = mEncoder->DataAvailableEvent().Connect(
+ mMainThread, this, &Session::OnDataAvailable);
+ mErrorListener =
+ mEncoder->ErrorEvent().Connect(mMainThread, this, &Session::OnError);
+ mShutdownListener = mEncoder->ShutdownEvent().Connect(mMainThread, this,
+ &Session::OnShutdown);
+
+ if (mRecorder->mAudioNode) {
+ mEncoder->ConnectAudioNode(mRecorder->mAudioNode,
+ mRecorder->mAudioNodeOutput);
+ }
+
+ for (const auto& track : mMediaStreamTracks) {
+ mEncoder->ConnectMediaStreamTrack(track);
+ }
+
+ // Set mRunningState to Running so that DoSessionEndTask will
+ // take the responsibility to end the session.
+ mRunningState = RunningState::Starting;
+ }
+
+ // This is the task that will stop recording per spec:
+ // - If rv is NS_ERROR_ABORT or NS_ERROR_DOM_SECURITY_ERR, cancel the encoders
+ // - Otherwise, stop the encoders gracefully, this still encodes buffered data
+ // - Set state to "inactive"
+ // - Fire an error event, if NS_FAILED(rv)
+ // - Discard blob data if rv is NS_ERROR_DOM_SECURITY_ERR
+ // - Fire a Blob event
+ // - Fire an event named stop
+ void DoSessionEndTask(nsresult rv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mRunningState.isErr()) {
+ // We have already ended with an error.
+ return;
+ }
+
+ if (mRunningState.isOk() &&
+ mRunningState.inspect() == RunningState::Stopped) {
+ // We have already ended gracefully.
+ return;
+ }
+
+ bool needsStartEvent = false;
+ if (mRunningState.isOk() &&
+ (mRunningState.inspect() == RunningState::Idling ||
+ mRunningState.inspect() == RunningState::Starting)) {
+ needsStartEvent = true;
+ }
+
+ // Set a terminated running state. Future DoSessionEnd tasks will exit
+ // early.
+ if (rv == NS_OK) {
+ mRunningState = RunningState::Stopped;
+ } else {
+ mRunningState = Err(rv);
+ }
+
+ RefPtr<MediaEncoder::BlobPromise> blobPromise;
+ if (!mEncoder) {
+ blobPromise = MediaEncoder::BlobPromise::CreateAndReject(NS_OK, __func__);
+ } else {
+ blobPromise =
+ (rv == NS_ERROR_ABORT || rv == NS_ERROR_DOM_SECURITY_ERR
+ ? mEncoder->Cancel()
+ : mEncoder->Stop())
+ ->Then(mEncoderThread, __func__,
+ [encoder = mEncoder](
+ const GenericNonExclusivePromise::ResolveOrRejectValue&
+ aValue) {
+ MOZ_DIAGNOSTIC_ASSERT(aValue.IsResolve());
+ return encoder->RequestData();
+ });
+ }
+
+ blobPromise
+ ->Then(
+ mMainThread, __func__,
+ [this, self = RefPtr<Session>(this), rv, needsStartEvent](
+ const MediaEncoder::BlobPromise::ResolveOrRejectValue& aRv) {
+ if (mRecorder->mSessions.LastElement() == this) {
+ // Set state to inactive, but only if the recorder is not
+ // controlled by another session already.
+ mRecorder->Inactivate();
+ }
+
+ if (needsStartEvent) {
+ mRecorder->DispatchSimpleEvent(u"start"_ns);
+ }
+
+ // If there was an error, Fire the appropriate one
+ if (NS_FAILED(rv)) {
+ mRecorder->NotifyError(rv);
+ }
+
+ // Fire a blob event named dataavailable
+ RefPtr<BlobImpl> blobImpl;
+ if (rv == NS_ERROR_DOM_SECURITY_ERR || aRv.IsReject()) {
+ // In case of SecurityError, the blob data must be discarded.
+ // We create a new empty one and throw the blob with its data
+ // away.
+ // In case we failed to gather blob data, we create an empty
+ // memory blob instead.
+ blobImpl = new EmptyBlobImpl(mMimeType);
+ } else {
+ blobImpl = aRv.ResolveValue();
+ }
+ if (NS_FAILED(mRecorder->CreateAndDispatchBlobEvent(blobImpl))) {
+ // Failed to dispatch blob event. That's unexpected. It's
+ // probably all right to fire an error event if we haven't
+ // already.
+ if (NS_SUCCEEDED(rv)) {
+ mRecorder->NotifyError(NS_ERROR_FAILURE);
+ }
+ }
+
+ // Fire an event named stop
+ mRecorder->DispatchSimpleEvent(u"stop"_ns);
+
+ // And finally, Shutdown and destroy the Session
+ return Shutdown();
+ })
+ ->Then(mMainThread, __func__, [this, self = RefPtr<Session>(this)] {
+ // Guard against the case where we fail to add a blocker due to being
+ // in XPCOM shutdown. If we're in this state we shouldn't try and get
+ // a shutdown barrier as we'll fail.
+ if (!mShutdownBlocker) {
+ return;
+ }
+ MustGetShutdownBarrier()->RemoveBlocker(mShutdownBlocker);
+ mShutdownBlocker = nullptr;
+ });
+ }
+
+ void OnStarted() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mRunningState.isErr()) {
+ return;
+ }
+ RunningState state = mRunningState.inspect();
+ 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).
+ mRunningState = RunningState::Running;
+
+ mRecorder->mMimeType = mEncoder->mMimeType;
+ }
+ mRecorder->DispatchSimpleEvent(u"start"_ns);
+ }
+ }
+
+ void OnDataAvailable(const RefPtr<BlobImpl>& aBlob) {
+ if (mRunningState.isErr() &&
+ mRunningState.unwrapErr() == NS_ERROR_DOM_SECURITY_ERR) {
+ return;
+ }
+ if (NS_WARN_IF(NS_FAILED(mRecorder->CreateAndDispatchBlobEvent(aBlob)))) {
+ LOG(LogLevel::Warning,
+ ("MediaRecorder %p Creating or dispatching BlobEvent failed", this));
+ DoSessionEndTask(NS_OK);
+ }
+ }
+
+ void OnError() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DoSessionEndTask(NS_ERROR_FAILURE);
+ }
+
+ void OnShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DoSessionEndTask(NS_OK);
+ }
+
+ RefPtr<ShutdownPromise> Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Debug, ("Session Shutdown %p", this));
+
+ if (mShutdownPromise) {
+ return mShutdownPromise;
+ }
+
+ // 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());
+
+ mShutdownPromise = ShutdownPromise::CreateAndResolve(true, __func__);
+
+ if (mEncoder) {
+ mShutdownPromise =
+ mShutdownPromise
+ ->Then(mMainThread, __func__,
+ [this, self = RefPtr<Session>(this)] {
+ mStartedListener.DisconnectIfExists();
+ mDataAvailableListener.DisconnectIfExists();
+ mErrorListener.DisconnectIfExists();
+ mShutdownListener.DisconnectIfExists();
+ return mEncoder->Cancel();
+ })
+ ->Then(mEncoderThread, __func__, [] {
+ // Meh, this is just to convert the promise type to match
+ // mShutdownPromise.
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ });
+ }
+
+ // Remove main thread state. This could be needed if Stop() wasn't called.
+ 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.
+ mShutdownPromise = mShutdownPromise->Then(
+ mMainThread, __func__,
+ [self = RefPtr<Session>(this)]() {
+ self->mRecorder->RemoveSession(self);
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ },
+ []() {
+ MOZ_ASSERT_UNREACHABLE("Unexpected reject");
+ return ShutdownPromise::CreateAndReject(false, __func__);
+ });
+
+ if (mEncoderThread) {
+ mShutdownPromise = mShutdownPromise->Then(
+ mMainThread, __func__,
+ [encoderThread = mEncoderThread]() {
+ 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 received data
+ Stopping, // Stop() has been called
+ Stopped, // Session has stopped without any error
+ };
+
+ // Our associated MediaRecorder.
+ const RefPtr<MediaRecorder> mRecorder;
+
+ // Stream currently recorded.
+ RefPtr<DOMMediaStream> mMediaStream;
+
+ // Tracks currently recorded. This should be a subset of mMediaStream's track
+ // set.
+ nsTArray<RefPtr<MediaStreamTrack>> mMediaStreamTracks;
+
+ // Main thread used for MozPromise operations.
+ const RefPtr<nsISerialEventTarget> mMainThread;
+ // Runnable thread for reading data from MediaEncoder.
+ RefPtr<TaskQueue> mEncoderThread;
+ // MediaEncoder pipeline.
+ RefPtr<MediaEncoder> mEncoder;
+ // Listener connected to mMediaEncoder::StartedEvent().
+ MediaEventListener mStartedListener;
+ // Listener connected to mMediaEncoder::DataAvailableEvent().
+ MediaEventListener mDataAvailableListener;
+ // Listener connected to mMediaEncoder::ErrorEvent().
+ MediaEventListener mErrorListener;
+ // Listener connected to mMediaEncoder::ShutdownEvent().
+ MediaEventListener mShutdownListener;
+ // Set in Shutdown() and resolved when shutdown is complete.
+ RefPtr<ShutdownPromise> mShutdownPromise;
+ // Session mimeType
+ const nsString mMimeType;
+ // The video bitrate the recorder was configured with.
+ const uint32_t mVideoBitsPerSecond;
+ // The audio bitrate the recorder was configured with.
+ const uint32_t mAudioBitsPerSecond;
+ // The time this session started, for telemetry.
+ const TimeStamp mStartTime;
+ // The session's current main thread state. The error type gets set when
+ // ending a recording with an error. An NS_OK error is invalid.
+ // Main thread only.
+ Result<RunningState, nsresult> mRunningState;
+ // Shutdown blocker unique for this Session. Main thread only.
+ RefPtr<ShutdownBlocker> mShutdownBlocker;
+};
+
+MediaRecorder::~MediaRecorder() {
+ LOG(LogLevel::Debug, ("~MediaRecorder (%p)", this));
+ UnRegisterActivityObserver();
+}
+
+MediaRecorder::MediaRecorder(nsPIDOMWindowInner* aOwnerWindow)
+ : DOMEventTargetHelper(aOwnerWindow) {
+ MOZ_ASSERT(aOwnerWindow);
+ 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::GetMimeType(nsString& aMimeType) { aMimeType = mMimeType; }
+
+void MediaRecorder::Start(const Optional<uint32_t>& aTimeslice,
+ ErrorResult& aResult) {
+ LOG(LogLevel::Debug, ("MediaRecorder.Start %p", this));
+
+ InitializeDomExceptions();
+
+ // When a MediaRecorder object’s start() method is invoked, the UA MUST run
+ // the following steps:
+
+ // 1. Let recorder be the MediaRecorder object on which the method was
+ // invoked.
+
+ // 2. Let timeslice be the method’s first argument, if provided, or undefined.
+ TimeDuration timeslice =
+ aTimeslice.WasPassed()
+ ? TimeDuration::FromMilliseconds(aTimeslice.Value())
+ : TimeDuration::Forever();
+
+ // 3. Let stream be the value of recorder’s stream attribute.
+
+ // 4. Let tracks be the set of live tracks in stream’s track set.
+ nsTArray<RefPtr<MediaStreamTrack>> tracks;
+ if (mStream) {
+ mStream->GetTracks(tracks);
+ }
+ tracks.RemoveLastElements(
+ tracks.end() - std::remove_if(tracks.begin(), tracks.end(),
+ [](const auto& t) { return t->Ended(); }));
+
+ // 5. If the value of recorder’s state attribute is not inactive, throw an
+ // InvalidStateError DOMException and abort these steps.
+ if (mState != RecordingState::Inactive) {
+ aResult.ThrowInvalidStateError(
+ "The MediaRecorder has already been started");
+ return;
+ }
+
+ // 6. If the isolation properties of stream disallow access from recorder,
+ // throw a SecurityError DOMException and abort these steps.
+ if (mStream) {
+ RefPtr<nsIPrincipal> streamPrincipal = mStream->GetPrincipal();
+ if (!streamPrincipal) {
+ // This is more or less part of the step 7, see below.
+ aResult.ThrowNotSupportedError("The MediaStream is inactive");
+ return;
+ }
+
+ if (!PrincipalSubsumes(this, streamPrincipal)) {
+ aResult.ThrowSecurityError(
+ "The MediaStream's isolation properties disallow access from "
+ "MediaRecorder");
+ return;
+ }
+ }
+ if (mAudioNode && !AudioNodePrincipalSubsumes(this, mAudioNode)) {
+ LOG(LogLevel::Warning,
+ ("MediaRecorder %p Start AudioNode principal check failed", this));
+ aResult.ThrowSecurityError(
+ "The AudioNode's isolation properties disallow access from "
+ "MediaRecorder");
+ return;
+ }
+
+ // 7. If stream is inactive, throw a NotSupportedError DOMException and abort
+ // these steps.
+ if (mStream && !mStream->Active()) {
+ aResult.ThrowNotSupportedError("The MediaStream is inactive");
+ return;
+ }
+
+ // 8. If the [[ConstrainedMimeType]] slot specifies a media type, container,
+ // or codec, then run the following sub steps:
+ // 1. Constrain the configuration of recorder to the media type, container,
+ // and codec specified in the [[ConstrainedMimeType]] slot.
+ // 2. For each track in tracks, if the User Agent cannot record the track
+ // using the current configuration, then throw a NotSupportedError
+ // DOMException and abort all steps.
+ Maybe<MediaContainerType> mime;
+ if (mConstrainedMimeType.Length() > 0) {
+ mime = MakeMediaContainerType(mConstrainedMimeType);
+ MOZ_DIAGNOSTIC_ASSERT(
+ mime,
+ "Invalid media MIME type should have been caught by IsTypeSupported");
+ }
+ for (const auto& track : tracks) {
+ TypeSupport support = CanRecordWith(track, mime, mConstrainedMimeType);
+ if (support != TypeSupport::Supported) {
+ nsString id;
+ track->GetId(id);
+ aResult.ThrowNotSupportedError(nsPrintfCString(
+ "%s track cannot be recorded: %s",
+ track->AsAudioStreamTrack() ? "An audio" : "A video",
+ TypeSupportToCString(support, mConstrainedMimeType).get()));
+ return;
+ }
+ }
+ if (mAudioNode) {
+ TypeSupport support = CanRecordAudioTrackWith(mime, mConstrainedMimeType);
+ if (support != TypeSupport::Supported) {
+ aResult.ThrowNotSupportedError(nsPrintfCString(
+ "An AudioNode cannot be recorded: %s",
+ TypeSupportToCString(support, mConstrainedMimeType).get()));
+ return;
+ }
+ }
+
+ // 9. If recorder’s [[ConstrainedBitsPerSecond]] slot is not undefined, set
+ // recorder’s videoBitsPerSecond and audioBitsPerSecond attributes to
+ // values the User Agent deems reasonable for the respective media types,
+ // for recording all tracks in tracks, such that the sum of
+ // videoBitsPerSecond and audioBitsPerSecond is close to the value of
+ // recorder’s
+ // [[ConstrainedBitsPerSecond]] slot.
+ uint8_t numVideoTracks = 0;
+ uint8_t numAudioTracks = 0;
+ for (const auto& t : tracks) {
+ if (t->AsVideoStreamTrack() && numVideoTracks < UINT8_MAX) {
+ ++numVideoTracks;
+ } else if (t->AsAudioStreamTrack() && numAudioTracks < UINT8_MAX) {
+ ++numAudioTracks;
+ }
+ }
+ if (mAudioNode) {
+ MOZ_DIAGNOSTIC_ASSERT(!mStream);
+ ++numAudioTracks;
+ }
+ if (mConstrainedBitsPerSecond) {
+ SelectBitrates(*mConstrainedBitsPerSecond, numVideoTracks,
+ &mVideoBitsPerSecond, numAudioTracks, &mAudioBitsPerSecond);
+ }
+
+ // 10. Let videoBitrate be the value of recorder’s videoBitsPerSecond
+ // attribute, and constrain the configuration of recorder to target an
+ // aggregate bitrate of videoBitrate bits per second for all video tracks
+ // recorder will be recording. videoBitrate is a hint for the encoder and
+ // the value might be surpassed, not achieved, or only be achieved over a
+ // long period of time.
+ const uint32_t videoBitrate = mVideoBitsPerSecond;
+
+ // 11. Let audioBitrate be the value of recorder’s audioBitsPerSecond
+ // attribute, and constrain the configuration of recorder to target an
+ // aggregate bitrate of audioBitrate bits per second for all audio tracks
+ // recorder will be recording. audioBitrate is a hint for the encoder and
+ // the value might be surpassed, not achieved, or only be achieved over a
+ // long period of time.
+ const uint32_t audioBitrate = mAudioBitsPerSecond;
+
+ // 12. Constrain the configuration of recorder to encode using the BitrateMode
+ // specified by the value of recorder’s audioBitrateMode attribute for all
+ // audio tracks recorder will be recording.
+ // -- NOT IMPLEMENTED
+
+ // 13. For each track in tracks, if the User Agent cannot record the track
+ // using the current configuration, then throw a NotSupportedError
+ // DOMException and abort these steps.
+ if (numVideoTracks > 1) {
+ aResult.ThrowNotSupportedError(
+ "MediaRecorder does not support recording more than one video track"_ns);
+ return;
+ }
+ if (numAudioTracks > 1) {
+ aResult.ThrowNotSupportedError(
+ "MediaRecorder does not support recording more than one audio track"_ns);
+ return;
+ }
+
+ // 14. Set recorder’s state to recording
+ mState = RecordingState::Recording;
+
+ MediaRecorderReporter::AddMediaRecorder(this);
+ // Start a session.
+ mSessions.AppendElement();
+ mSessions.LastElement() =
+ new Session(this, std::move(tracks), videoBitrate, audioBitrate);
+ mSessions.LastElement()->Start(timeslice);
+}
+
+void MediaRecorder::Stop(ErrorResult& aResult) {
+ LOG(LogLevel::Debug, ("MediaRecorder.Stop %p", this));
+ MediaRecorderReporter::RemoveMediaRecorder(this);
+
+ // When a MediaRecorder object’s stop() method is invoked, the UA MUST run the
+ // following steps:
+
+ // 1. Let recorder be the MediaRecorder object on which the method was
+ // invoked.
+
+ // 2. If recorder’s state attribute is inactive, abort these steps.
+ if (mState == RecordingState::Inactive) {
+ return;
+ }
+
+ // 3. Inactivate the recorder with recorder.
+ Inactivate();
+
+ // 4. Queue a task, using the DOM manipulation task source, that runs the
+ // following steps:
+ // 1. Stop gathering data.
+ // 2. Let blob be the Blob of collected data so far, then fire a blob event
+ // named dataavailable at recorder with blob.
+ // 3. Fire an event named stop at recorder.
+ MOZ_ASSERT(mSessions.Length() > 0);
+ mSessions.LastElement()->Stop();
+
+ // 5. return undefined.
+}
+
+void MediaRecorder::Pause(ErrorResult& aResult) {
+ LOG(LogLevel::Debug, ("MediaRecorder.Pause %p", this));
+
+ // When a MediaRecorder object’s pause() method is invoked, the UA MUST run
+ // the following steps:
+
+ // 1. If state is inactive, throw an InvalidStateError DOMException and abort
+ // these steps.
+ if (mState == RecordingState::Inactive) {
+ aResult.ThrowInvalidStateError("The MediaRecorder is inactive");
+ return;
+ }
+
+ // 2. If state is paused, abort these steps.
+ if (mState == RecordingState::Paused) {
+ return;
+ }
+
+ // 3. Set state to paused, and queue a task, using the DOM manipulation task
+ // source, that runs the following steps:
+ mState = RecordingState::Paused;
+
+ // XXX - We pause synchronously pending spec issue
+ // https://github.com/w3c/mediacapture-record/issues/131
+ // 1. Stop gathering data into blob (but keep it available so that
+ // recording can be resumed in the future).
+ MOZ_ASSERT(!mSessions.IsEmpty());
+ mSessions.LastElement()->Pause();
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "MediaRecorder::Pause", [recorder = RefPtr<MediaRecorder>(this)] {
+ // 2. Let target be the MediaRecorder context object. Fire an event
+ // named pause at target.
+ recorder->DispatchSimpleEvent(u"pause"_ns);
+ }));
+
+ // 4. return undefined.
+}
+
+void MediaRecorder::Resume(ErrorResult& aResult) {
+ LOG(LogLevel::Debug, ("MediaRecorder.Resume %p", this));
+
+ // When a MediaRecorder object’s resume() method is invoked, the UA MUST run
+ // the following steps:
+
+ // 1. If state is inactive, throw an InvalidStateError DOMException and abort
+ // these steps.
+ if (mState == RecordingState::Inactive) {
+ aResult.ThrowInvalidStateError("The MediaRecorder is inactive");
+ return;
+ }
+
+ // 2. If state is recording, abort these steps.
+ if (mState == RecordingState::Recording) {
+ return;
+ }
+
+ // 3. Set state to recording, and queue a task, using the DOM manipulation
+ // task source, that runs the following steps:
+ mState = RecordingState::Recording;
+
+ // XXX - We resume synchronously pending spec issue
+ // https://github.com/w3c/mediacapture-record/issues/131
+ // 1. Resume (or continue) gathering data into the current blob.
+ MOZ_ASSERT(!mSessions.IsEmpty());
+ mSessions.LastElement()->Resume();
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "MediaRecorder::Resume", [recorder = RefPtr<MediaRecorder>(this)] {
+ // 2. Let target be the MediaRecorder context object. Fire an event
+ // named resume at target.
+ recorder->DispatchSimpleEvent(u"resume"_ns);
+ }));
+
+ // 4. return undefined.
+}
+
+void MediaRecorder::RequestData(ErrorResult& aResult) {
+ LOG(LogLevel::Debug, ("MediaRecorder.RequestData %p", this));
+
+ // When a MediaRecorder object’s requestData() method is invoked, the UA MUST
+ // run the following steps:
+
+ // 1. If state is inactive throw an InvalidStateError DOMException and
+ // terminate these steps. Otherwise the UA MUST queue a task, using the DOM
+ // manipulation task source, that runs the following steps:
+ // 1. Let blob be the Blob of collected data so far and let target be the
+ // MediaRecorder context object, then fire a blob event named
+ // dataavailable at target with blob. (Note that blob will be empty if no
+ // data has been gathered yet.)
+ // 2. Create a new Blob and gather subsequent data into it.
+ if (mState == RecordingState::Inactive) {
+ aResult.ThrowInvalidStateError("The MediaRecorder is inactive");
+ return;
+ }
+ MOZ_ASSERT(mSessions.Length() > 0);
+ mSessions.LastElement()->RequestData();
+
+ // 2. return undefined.
+}
+
+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& aOptions, ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!ownerWindow) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // When the MediaRecorder() constructor is invoked, the User Agent MUST run
+ // the following steps:
+
+ // 1. Let stream be the constructor’s first argument.
+
+ // 2. Let options be the constructor’s second argument.
+
+ // 3. If invoking is type supported with options’ mimeType member as its
+ // argument returns false, throw a NotSupportedError DOMException and abort
+ // these steps.
+ TypeSupport support = IsTypeSupportedImpl(aOptions.mMimeType);
+ if (support != TypeSupport::Supported) {
+ // This catches also the empty string mimeType when support for any encoders
+ // has been disabled.
+ aRv.ThrowNotSupportedError(
+ TypeSupportToCString(support, aOptions.mMimeType));
+ return nullptr;
+ }
+
+ // 4. Let recorder be a newly constructed MediaRecorder object.
+ RefPtr<MediaRecorder> recorder = new MediaRecorder(ownerWindow);
+
+ // 5. Let recorder have a [[ConstrainedMimeType]] internal slot, initialized
+ // to the value of options' mimeType member.
+ recorder->mConstrainedMimeType = aOptions.mMimeType;
+
+ // 6. Let recorder have a [[ConstrainedBitsPerSecond]] internal slot,
+ // initialized to the value of options’ bitsPerSecond member, if it is
+ // present, otherwise undefined.
+ recorder->mConstrainedBitsPerSecond =
+ aOptions.mBitsPerSecond.WasPassed()
+ ? Some(aOptions.mBitsPerSecond.Value())
+ : Nothing();
+
+ // 7. Initialize recorder’s stream attribute to stream.
+ recorder->mStream = &aStream;
+
+ // 8. Initialize recorder’s mimeType attribute to the value of recorder’s
+ // [[ConstrainedMimeType]] slot.
+ recorder->mMimeType = recorder->mConstrainedMimeType;
+
+ // 9. Initialize recorder’s state attribute to inactive.
+ recorder->mState = RecordingState::Inactive;
+
+ // 10. Initialize recorder’s videoBitsPerSecond attribute to the value of
+ // options’ videoBitsPerSecond member, if it is present. Otherwise, choose
+ // a target value the User Agent deems reasonable for video.
+ recorder->mVideoBitsPerSecond = aOptions.mVideoBitsPerSecond.WasPassed()
+ ? aOptions.mVideoBitsPerSecond.Value()
+ : DEFAULT_VIDEO_BITRATE_BPS;
+
+ // 11. Initialize recorder’s audioBitsPerSecond attribute to the value of
+ // options’ audioBitsPerSecond member, if it is present. Otherwise, choose
+ // a target value the User Agent deems reasonable for audio.
+ recorder->mAudioBitsPerSecond = aOptions.mAudioBitsPerSecond.WasPassed()
+ ? aOptions.mAudioBitsPerSecond.Value()
+ : DEFAULT_AUDIO_BITRATE_BPS;
+
+ // 12. If recorder’s [[ConstrainedBitsPerSecond]] slot is not undefined, set
+ // recorder’s videoBitsPerSecond and audioBitsPerSecond attributes to
+ // values the User Agent deems reasonable for the respective media types,
+ // such that the sum of videoBitsPerSecond and audioBitsPerSecond is close
+ // to the value of recorder’s [[ConstrainedBitsPerSecond]] slot.
+ if (recorder->mConstrainedBitsPerSecond) {
+ SelectBitrates(*recorder->mConstrainedBitsPerSecond, 1,
+ &recorder->mVideoBitsPerSecond, 1,
+ &recorder->mAudioBitsPerSecond);
+ }
+
+ // 13. Return recorder.
+ return recorder.forget();
+}
+
+/* static */
+already_AddRefed<MediaRecorder> MediaRecorder::Constructor(
+ const GlobalObject& aGlobal, AudioNode& aAudioNode,
+ uint32_t aAudioNodeOutput, const MediaRecorderOptions& aOptions,
+ 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.
+ aRv.ThrowTypeError<MSG_DOES_NOT_IMPLEMENT_INTERFACE>("Argument 1",
+ "MediaStream");
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!ownerWindow) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // aAudioNodeOutput doesn't matter to destination node because it has no
+ // output.
+ if (aAudioNode.NumberOfOutputs() > 0 &&
+ aAudioNodeOutput >= aAudioNode.NumberOfOutputs()) {
+ aRv.ThrowIndexSizeError("Invalid AudioNode output index");
+ return nullptr;
+ }
+
+ // When the MediaRecorder() constructor is invoked, the User Agent MUST run
+ // the following steps:
+
+ // 1. Let stream be the constructor’s first argument. (we'll let audioNode be
+ // the first arg, and audioNodeOutput the second)
+
+ // 2. Let options be the constructor’s second argument. (we'll let options be
+ // the third arg)
+
+ // 3. If invoking is type supported with options’ mimeType member as its
+ // argument returns false, throw a NotSupportedError DOMException and abort
+ // these steps.
+ TypeSupport support = IsTypeSupportedImpl(aOptions.mMimeType);
+ if (support != TypeSupport::Supported) {
+ // This catches also the empty string mimeType when support for any encoders
+ // has been disabled.
+ aRv.ThrowNotSupportedError(
+ TypeSupportToCString(support, aOptions.mMimeType));
+ return nullptr;
+ }
+
+ // 4. Let recorder be a newly constructed MediaRecorder object.
+ RefPtr<MediaRecorder> recorder = new MediaRecorder(ownerWindow);
+
+ // 5. Let recorder have a [[ConstrainedMimeType]] internal slot, initialized
+ // to the value of options' mimeType member.
+ recorder->mConstrainedMimeType = aOptions.mMimeType;
+
+ // 6. Let recorder have a [[ConstrainedBitsPerSecond]] internal slot,
+ // initialized to the value of options’ bitsPerSecond member, if it is
+ // present, otherwise undefined.
+ recorder->mConstrainedBitsPerSecond =
+ aOptions.mBitsPerSecond.WasPassed()
+ ? Some(aOptions.mBitsPerSecond.Value())
+ : Nothing();
+
+ // 7. Initialize recorder’s stream attribute to stream. (make that the
+ // audioNode and audioNodeOutput equivalents)
+ recorder->mAudioNode = &aAudioNode;
+ recorder->mAudioNodeOutput = aAudioNodeOutput;
+
+ // 8. Initialize recorder’s mimeType attribute to the value of recorder’s
+ // [[ConstrainedMimeType]] slot.
+ recorder->mMimeType = recorder->mConstrainedMimeType;
+
+ // 9. Initialize recorder’s state attribute to inactive.
+ recorder->mState = RecordingState::Inactive;
+
+ // 10. Initialize recorder’s videoBitsPerSecond attribute to the value of
+ // options’ videoBitsPerSecond member, if it is present. Otherwise, choose
+ // a target value the User Agent deems reasonable for video.
+ recorder->mVideoBitsPerSecond = aOptions.mVideoBitsPerSecond.WasPassed()
+ ? aOptions.mVideoBitsPerSecond.Value()
+ : DEFAULT_VIDEO_BITRATE_BPS;
+
+ // 11. Initialize recorder’s audioBitsPerSecond attribute to the value of
+ // options’ audioBitsPerSecond member, if it is present. Otherwise, choose
+ // a target value the User Agent deems reasonable for audio.
+ recorder->mAudioBitsPerSecond = aOptions.mAudioBitsPerSecond.WasPassed()
+ ? aOptions.mAudioBitsPerSecond.Value()
+ : DEFAULT_AUDIO_BITRATE_BPS;
+
+ // 12. If recorder’s [[ConstrainedBitsPerSecond]] slot is not undefined, set
+ // recorder’s videoBitsPerSecond and audioBitsPerSecond attributes to
+ // values the User Agent deems reasonable for the respective media types,
+ // such that the sum of videoBitsPerSecond and audioBitsPerSecond is close
+ // to the value of recorder’s [[ConstrainedBitsPerSecond]] slot.
+ if (recorder->mConstrainedBitsPerSecond) {
+ SelectBitrates(*recorder->mConstrainedBitsPerSecond, 1,
+ &recorder->mVideoBitsPerSecond, 1,
+ &recorder->mAudioBitsPerSecond);
+ }
+
+ // 13. Return recorder.
+ return recorder.forget();
+}
+
+/* static */
+bool MediaRecorder::IsTypeSupported(GlobalObject& aGlobal,
+ const nsAString& aMIMEType) {
+ return MediaRecorder::IsTypeSupported(aMIMEType);
+}
+
+/* static */
+bool MediaRecorder::IsTypeSupported(const nsAString& aMIMEType) {
+ return IsTypeSupportedImpl(aMIMEType) == TypeSupport::Supported;
+}
+
+nsresult MediaRecorder::CreateAndDispatchBlobEvent(BlobImpl* aBlobImpl) {
+ MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
+
+ if (!GetOwnerGlobal()) {
+ // This MediaRecorder has been disconnected in the meantime.
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Blob> blob = Blob::Create(GetOwnerGlobal(), aBlobImpl);
+ if (NS_WARN_IF(!blob)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ BlobEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ init.mData = blob;
+
+ RefPtr<BlobEvent> event =
+ BlobEvent::Constructor(this, u"dataavailable"_ns, 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 = CheckCurrentGlobalCorrectness();
+ 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 = CheckCurrentGlobalCorrectness();
+ 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 = std::move(mSecurityDomException);
+ break;
+ default:
+ if (mOtherDomException && aRv == mOtherDomException->GetResult()) {
+ LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
+ "mOtherDomException being fired for aRv: %X",
+ uint32_t(aRv)));
+ init.mError = std::move(mOtherDomException);
+ break;
+ }
+ 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 = std::move(mUnknownDomException);
+ break;
+ }
+
+ RefPtr<MediaRecorderErrorEvent> event =
+ MediaRecorderErrorEvent::Constructor(this, u"error"_ns, 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);
+
+ LOG(LogLevel::Debug, ("MediaRecorder %p NotifyOwnerDocumentActivityChanged "
+ "IsActive=%d, "
+ "IsVisible=%d, ",
+ this, doc->IsActive(), doc->IsVisible()));
+ if (!doc->IsActive() || !doc->IsVisible()) {
+ // Stop the session.
+ ErrorResult result;
+ Stop(result);
+ result.SuppressException();
+ }
+}
+
+void MediaRecorder::Inactivate() {
+ LOG(LogLevel::Debug, ("MediaRecorder.Inactivate %p", this));
+ // The Inactivate the recorder algorithm given a recorder, is as follows:
+
+ // 1. Set recorder’s mimeType attribute to the value of the
+ // [[ConstrainedMimeType]] slot.
+ mMimeType = mConstrainedMimeType;
+
+ // 2. Set recorder’s state attribute to inactive.
+ mState = RecordingState::Inactive;
+
+ // 3. If recorder’s [[ConstrainedBitsPerSecond]] slot is not undefined, set
+ // recorder’s videoBitsPerSecond and audioBitsPerSecond attributes to
+ // values the User Agent deems reasonable for the respective media types,
+ // such that the sum of videoBitsPerSecond and audioBitsPerSecond is close
+ // to the value of recorder’s [[ConstrainedBitsPerSecond]] slot.
+ if (mConstrainedBitsPerSecond) {
+ SelectBitrates(*mConstrainedBitsPerSecond, 1, &mVideoBitsPerSecond, 1,
+ &mAudioBitsPerSecond);
+ }
+}
+
+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(GetCurrentSerialEventTarget(), promises)
+ ->Then(
+ GetCurrentSerialEventTarget(), __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 mozilla::dom
+
+#undef LOG