Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "MediaTrackGraph.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 "nsGlobalWindowInner.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->GetOwnerWindow()) {
return false;
}
nsCOMPtr<Document> doc = aRecorder->GetOwnerWindow()->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->GetOwnerWindow()
? aAudioNode->GetOwnerWindow()->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_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Session,
DOMMediaStream::TrackListener)
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)),
mMimeType(SelectMimeType(
mMediaStreamTracks.Contains(TrackTypeComparator::VIDEO,
TrackTypeComparator()),
mRecorder->mAudioNode ||
mMediaStreamTracks.Contains(TrackTypeComparator::AUDIO,
TrackTypeComparator()),
mRecorder->mConstrainedMimeType)),
mVideoBitsPerSecond(aVideoBitsPerSecond),
mAudioBitsPerSecond(aAudioBitsPerSecond),
mRunningState(RunningState::Idling) {
MOZ_ASSERT(NS_IsMainThread());
}
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(
GetMainThreadSerialEventTarget(), __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");