Source code

Revision control

Other Tools

1
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2
/* vim:set ts=2 sw=2 sts=2 et cindent: */
3
/* This Source Code Form is subject to the terms of the Mozilla Public
4
* License, v. 2.0. If a copy of the MPL was not distributed with this
5
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7
#include "MediaRecorder.h"
8
9
#include "AudioNodeEngine.h"
10
#include "AudioNodeStream.h"
11
#include "DOMMediaStream.h"
12
#include "GeckoProfiler.h"
13
#include "MediaDecoder.h"
14
#include "MediaEncoder.h"
15
#include "MediaStreamGraphImpl.h"
16
#include "VideoUtils.h"
17
#include "mozilla/DOMEventTargetHelper.h"
18
#include "mozilla/dom/AudioStreamTrack.h"
19
#include "mozilla/dom/BlobEvent.h"
20
#include "mozilla/dom/File.h"
21
#include "mozilla/dom/MediaRecorderErrorEvent.h"
22
#include "mozilla/dom/MutableBlobStorage.h"
23
#include "mozilla/dom/VideoStreamTrack.h"
24
#include "mozilla/media/MediaUtils.h"
25
#include "mozilla/MemoryReporting.h"
26
#include "mozilla/Preferences.h"
27
#include "mozilla/StaticPtr.h"
28
#include "mozilla/TaskQueue.h"
29
#include "nsAutoPtr.h"
30
#include "nsCharSeparatedTokenizer.h"
31
#include "nsContentTypeParser.h"
32
#include "nsContentUtils.h"
33
#include "nsDocShell.h"
34
#include "nsError.h"
35
#include "mozilla/dom/Document.h"
36
#include "nsIPermissionManager.h"
37
#include "nsIPrincipal.h"
38
#include "nsIScriptError.h"
39
#include "nsMimeTypes.h"
40
#include "nsProxyRelease.h"
41
#include "nsTArray.h"
42
43
mozilla::LazyLogModule gMediaRecorderLog("MediaRecorder");
44
#define LOG(type, msg) MOZ_LOG(gMediaRecorderLog, type, msg)
45
46
namespace mozilla {
47
48
namespace dom {
49
50
using namespace mozilla::media;
51
52
/* static */ StaticRefPtr<nsIAsyncShutdownBlocker>
53
gMediaRecorderShutdownBlocker;
54
static nsTHashtable<nsRefPtrHashKey<MediaRecorder::Session>> gSessions;
55
56
/**
57
* MediaRecorderReporter measures memory being used by the Media Recorder.
58
*
59
* It is a singleton reporter and the single class object lives as long as at
60
* least one Recorder is registered. In MediaRecorder, the reporter is
61
* unregistered when it is destroyed.
62
*/
63
class MediaRecorderReporter final : public nsIMemoryReporter {
64
public:
65
static void AddMediaRecorder(MediaRecorder* aRecorder) {
66
if (!sUniqueInstance) {
67
sUniqueInstance = MakeAndAddRef<MediaRecorderReporter>();
68
RegisterWeakAsyncMemoryReporter(sUniqueInstance);
69
}
70
sUniqueInstance->mRecorders.AppendElement(aRecorder);
71
}
72
73
static void RemoveMediaRecorder(MediaRecorder* aRecorder) {
74
if (!sUniqueInstance) {
75
return;
76
}
77
78
sUniqueInstance->mRecorders.RemoveElement(aRecorder);
79
if (sUniqueInstance->mRecorders.IsEmpty()) {
80
UnregisterWeakMemoryReporter(sUniqueInstance);
81
sUniqueInstance = nullptr;
82
}
83
}
84
85
NS_DECL_THREADSAFE_ISUPPORTS
86
87
MediaRecorderReporter() = default;
88
89
NS_IMETHOD
90
CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData,
91
bool aAnonymize) override {
92
nsTArray<RefPtr<MediaRecorder::SizeOfPromise>> promises;
93
for (const RefPtr<MediaRecorder>& recorder : mRecorders) {
94
promises.AppendElement(recorder->SizeOfExcludingThis(MallocSizeOf));
95
}
96
97
nsCOMPtr<nsIHandleReportCallback> handleReport = aHandleReport;
98
nsCOMPtr<nsISupports> data = aData;
99
MediaRecorder::SizeOfPromise::All(GetCurrentThreadSerialEventTarget(),
100
promises)
101
->Then(
102
GetCurrentThreadSerialEventTarget(), __func__,
103
[handleReport, data](const nsTArray<size_t>& sizes) {
104
nsCOMPtr<nsIMemoryReporterManager> manager =
105
do_GetService("@mozilla.org/memory-reporter-manager;1");
106
if (!manager) {
107
return;
108
}
109
110
size_t sum = 0;
111
for (const size_t& size : sizes) {
112
sum += size;
113
}
114
115
handleReport->Callback(
116
EmptyCString(), NS_LITERAL_CSTRING("explicit/media/recorder"),
117
KIND_HEAP, UNITS_BYTES, sum,
118
NS_LITERAL_CSTRING("Memory used by media recorder."), data);
119
120
manager->EndReport();
121
},
122
[](size_t) { MOZ_CRASH("Unexpected reject"); });
123
124
return NS_OK;
125
}
126
127
private:
128
MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
129
130
virtual ~MediaRecorderReporter() {
131
MOZ_ASSERT(mRecorders.IsEmpty(), "All recorders must have been removed");
132
}
133
134
static StaticRefPtr<MediaRecorderReporter> sUniqueInstance;
135
136
nsTArray<RefPtr<MediaRecorder>> mRecorders;
137
};
138
NS_IMPL_ISUPPORTS(MediaRecorderReporter, nsIMemoryReporter);
139
140
NS_IMPL_CYCLE_COLLECTION_CLASS(MediaRecorder)
141
142
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaRecorder,
143
DOMEventTargetHelper)
144
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMStream)
145
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioNode)
146
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSecurityDomException)
147
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mUnknownDomException)
148
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
149
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
150
151
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaRecorder,
152
DOMEventTargetHelper)
153
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMStream)
154
NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioNode)
155
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSecurityDomException)
156
NS_IMPL_CYCLE_COLLECTION_UNLINK(mUnknownDomException)
157
tmp->UnRegisterActivityObserver();
158
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
159
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
160
161
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaRecorder)
162
NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity)
163
NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
164
165
NS_IMPL_ADDREF_INHERITED(MediaRecorder, DOMEventTargetHelper)
166
NS_IMPL_RELEASE_INHERITED(MediaRecorder, DOMEventTargetHelper)
167
168
/**
169
* Session is an object to represent a single recording event.
170
* In original design, all recording context is stored in MediaRecorder, which
171
* causes a problem if someone calls MediaRecorder::Stop and
172
* MediaRecorder::Start quickly. To prevent blocking main thread, media encoding
173
* is executed in a second thread, named as Read Thread. For the same reason, we
174
* do not wait Read Thread shutdown in MediaRecorder::Stop. If someone call
175
* MediaRecorder::Start before Read Thread shutdown, the same recording context
176
* in MediaRecorder might be access by two Reading Threads, which cause a
177
* problem. In the new design, we put recording context into Session object,
178
* including Read Thread. Each Session has its own recording context and Read
179
* Thread, problem is been resolved.
180
*
181
* Life cycle of a Session object.
182
* 1) Initialization Stage (in main thread)
183
* Setup media streams in MSG, and bind MediaEncoder with Source Stream when
184
* mStream is available. Resource allocation, such as encoded data cache buffer
185
* and MediaEncoder. Create read thread. Automatically switch to Extract stage
186
* in the end of this stage. 2) Extract Stage (in Read Thread) Pull encoded A/V
187
* frames from MediaEncoder, dispatch to OnDataAvailable handler. Unless a
188
* client calls Session::Stop, Session object keeps stay in this stage. 3)
189
* Destroy Stage (in main thread) Switch from Extract stage to Destroy stage by
190
* calling Session::Stop. Release session resource and remove associated streams
191
* from MSG.
192
*
193
* Lifetime of MediaRecorder and Session objects.
194
* 1) MediaRecorder creates a Session in MediaRecorder::Start function and holds
195
* a reference to Session. Then the Session registers itself to a
196
* ShutdownBlocker and also holds a reference to MediaRecorder.
197
* Therefore, the reference dependency in gecko is:
198
* ShutdownBlocker -> Session <-> MediaRecorder, note that there is a cycle
199
* reference between Session and MediaRecorder.
200
* 2) A Session is destroyed after MediaRecorder::Stop has been called _and_ all
201
* encoded media data has been passed to OnDataAvailable handler. 3)
202
* MediaRecorder::Stop is called by user or the document is going to inactive or
203
* invisible.
204
*/
205
class MediaRecorder::Session : public PrincipalChangeObserver<MediaStreamTrack>,
206
public DOMMediaStream::TrackListener {
207
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Session)
208
209
class StoreEncodedBufferRunnable final : public Runnable {
210
RefPtr<Session> mSession;
211
nsTArray<nsTArray<uint8_t>> mBuffer;
212
213
public:
214
StoreEncodedBufferRunnable(Session* aSession,
215
nsTArray<nsTArray<uint8_t>>&& aBuffer)
216
: Runnable("StoreEncodedBufferRunnable"),
217
mSession(aSession),
218
mBuffer(std::move(aBuffer)) {}
219
220
NS_IMETHOD
221
Run() override {
222
MOZ_ASSERT(NS_IsMainThread());
223
mSession->MaybeCreateMutableBlobStorage();
224
for (uint32_t i = 0; i < mBuffer.Length(); i++) {
225
if (mBuffer[i].IsEmpty()) {
226
continue;
227
}
228
229
nsresult rv = mSession->mMutableBlobStorage->Append(
230
mBuffer[i].Elements(), mBuffer[i].Length());
231
if (NS_WARN_IF(NS_FAILED(rv))) {
232
mSession->DoSessionEndTask(rv);
233
break;
234
}
235
}
236
237
return NS_OK;
238
}
239
};
240
241
// Fire a named event, run in main thread task.
242
class DispatchEventRunnable : public Runnable {
243
public:
244
explicit DispatchEventRunnable(Session* aSession,
245
const nsAString& aEventName)
246
: Runnable("dom::MediaRecorder::Session::DispatchEventRunnable"),
247
mSession(aSession),
248
mEventName(aEventName) {}
249
250
NS_IMETHOD Run() override {
251
LOG(LogLevel::Debug,
252
("Session.DispatchEventRunnable s=(%p) e=(%s)", mSession.get(),
253
NS_ConvertUTF16toUTF8(mEventName).get()));
254
MOZ_ASSERT(NS_IsMainThread());
255
256
NS_ENSURE_TRUE(mSession->mRecorder, NS_OK);
257
mSession->mRecorder->DispatchSimpleEvent(mEventName);
258
259
return NS_OK;
260
}
261
262
private:
263
RefPtr<Session> mSession;
264
nsString mEventName;
265
};
266
267
class EncoderListener : public MediaEncoderListener {
268
public:
269
EncoderListener(TaskQueue* aEncoderThread, Session* aSession)
270
: mEncoderThread(aEncoderThread), mSession(aSession) {}
271
272
void Forget() {
273
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
274
mSession = nullptr;
275
}
276
277
void Initialized() override {
278
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
279
if (mSession) {
280
mSession->MediaEncoderInitialized();
281
}
282
}
283
284
void DataAvailable() override {
285
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
286
if (mSession) {
287
mSession->MediaEncoderDataAvailable();
288
}
289
}
290
291
void Error() override {
292
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
293
if (mSession) {
294
mSession->MediaEncoderError();
295
}
296
}
297
298
void Shutdown() override {
299
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
300
if (mSession) {
301
mSession->MediaEncoderShutdown();
302
}
303
}
304
305
protected:
306
RefPtr<TaskQueue> mEncoderThread;
307
RefPtr<Session> mSession;
308
};
309
310
public:
311
Session(MediaRecorder* aRecorder, uint32_t aTimeSlice)
312
: mRecorder(aRecorder),
313
mMediaStreamReady(false),
314
mMainThread(mRecorder->GetOwner()->EventTargetFor(TaskCategory::Other)),
315
mTimeSlice(aTimeSlice),
316
mStartTime(TimeStamp::Now()),
317
mRunningState(RunningState::Idling) {
318
MOZ_ASSERT(NS_IsMainThread());
319
320
aRecorder->GetMimeType(mMimeType);
321
mMaxMemory = Preferences::GetUint("media.recorder.max_memory",
322
MAX_ALLOW_MEMORY_BUFFER);
323
mLastBlobTimeStamp = mStartTime;
324
Telemetry::ScalarAdd(Telemetry::ScalarID::MEDIARECORDER_RECORDING_COUNT, 1);
325
}
326
327
void PrincipalChanged(MediaStreamTrack* aTrack) override {
328
NS_ASSERTION(mMediaStreamTracks.Contains(aTrack),
329
"Principal changed for unrecorded track");
330
if (!MediaStreamTracksPrincipalSubsumes()) {
331
DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
332
}
333
}
334
335
void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override {
336
LOG(LogLevel::Warning,
337
("Session.NotifyTrackAdded %p Raising error due to track set change",
338
this));
339
if (mMediaStreamReady) {
340
DoSessionEndTask(NS_ERROR_ABORT);
341
}
342
343
NS_DispatchToMainThread(
344
NewRunnableMethod("MediaRecorder::Session::MediaStreamReady", this,
345
&Session::MediaStreamReady));
346
return;
347
}
348
349
void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override {
350
if (!mMediaStreamReady) {
351
// We haven't chosen the track set to record yet.
352
return;
353
}
354
355
if (aTrack->Ended()) {
356
// TrackEncoder will pickup tracks that end itself.
357
return;
358
}
359
360
MOZ_ASSERT(mEncoder);
361
if (mEncoder) {
362
mEncoder->RemoveMediaStreamTrack(aTrack);
363
}
364
365
LOG(LogLevel::Warning,
366
("Session.NotifyTrackRemoved %p Raising error due to track set change",
367
this));
368
DoSessionEndTask(NS_ERROR_ABORT);
369
}
370
371
void Start() {
372
LOG(LogLevel::Debug, ("Session.Start %p", this));
373
MOZ_ASSERT(NS_IsMainThread());
374
375
DOMMediaStream* domStream = mRecorder->Stream();
376
if (domStream) {
377
// The callback reports back when tracks are available and can be
378
// attached to MediaEncoder. This allows `recorder.start()` before any
379
// tracks are available. We have supported this historically and have
380
// mochitests assuming this behavior.
381
mMediaStream = domStream;
382
mMediaStream->RegisterTrackListener(this);
383
nsTArray<RefPtr<MediaStreamTrack>> tracks(2);
384
mMediaStream->GetTracks(tracks);
385
for (const auto& track : tracks) {
386
// Notify of existing tracks, as the stream doesn't do this by itself.
387
NotifyTrackAdded(track);
388
}
389
return;
390
}
391
392
if (mRecorder->mAudioNode) {
393
// Check that we may access the audio node's content.
394
if (!AudioNodePrincipalSubsumes()) {
395
LOG(LogLevel::Warning,
396
("Session.Start AudioNode principal check failed"));
397
DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
398
return;
399
}
400
401
TrackRate trackRate =
402
mRecorder->mAudioNode->Context()->Graph()->GraphRate();
403
404
// Web Audio node has only audio.
405
InitEncoder(ContainerWriter::CREATE_AUDIO_TRACK, trackRate);
406
return;
407
}
408
409
MOZ_ASSERT(false, "Unknown source");
410
}
411
412
void Stop() {
413
LOG(LogLevel::Debug, ("Session.Stop %p", this));
414
MOZ_ASSERT(NS_IsMainThread());
415
416
if (mEncoder) {
417
mEncoder->Stop();
418
}
419
420
// Remove main thread state added in Start().
421
if (mMediaStream) {
422
mMediaStream->UnregisterTrackListener(this);
423
mMediaStream = nullptr;
424
}
425
426
{
427
auto tracks(std::move(mMediaStreamTracks));
428
for (RefPtr<MediaStreamTrack>& track : tracks) {
429
track->RemovePrincipalChangeObserver(this);
430
}
431
}
432
433
if (mRunningState.isOk() &&
434
mRunningState.inspect() == RunningState::Idling) {
435
LOG(LogLevel::Debug, ("Session.Stop Explicit end task %p", this));
436
// End the Session directly if there is no encoder.
437
DoSessionEndTask(NS_OK);
438
} else if (mRunningState.isOk() &&
439
(mRunningState.inspect() == RunningState::Starting ||
440
mRunningState.inspect() == RunningState::Running)) {
441
mRunningState = RunningState::Stopping;
442
}
443
}
444
445
nsresult Pause() {
446
LOG(LogLevel::Debug, ("Session.Pause"));
447
MOZ_ASSERT(NS_IsMainThread());
448
449
if (!mEncoder) {
450
return NS_ERROR_FAILURE;
451
}
452
453
mEncoder->Suspend();
454
NS_DispatchToMainThread(
455
new DispatchEventRunnable(this, NS_LITERAL_STRING("pause")));
456
return NS_OK;
457
}
458
459
nsresult Resume() {
460
LOG(LogLevel::Debug, ("Session.Resume"));
461
MOZ_ASSERT(NS_IsMainThread());
462
463
if (!mEncoder) {
464
return NS_ERROR_FAILURE;
465
}
466
467
mEncoder->Resume();
468
NS_DispatchToMainThread(
469
new DispatchEventRunnable(this, NS_LITERAL_STRING("resume")));
470
return NS_OK;
471
}
472
473
void RequestData() {
474
LOG(LogLevel::Debug, ("Session.RequestData"));
475
MOZ_ASSERT(NS_IsMainThread());
476
477
GatherBlob()->Then(
478
mMainThread, __func__,
479
[this, self = RefPtr<Session>(this)](
480
const BlobPromise::ResolveOrRejectValue& aResult) {
481
if (aResult.IsReject()) {
482
LOG(LogLevel::Warning, ("GatherBlob failed for RequestData()"));
483
DoSessionEndTask(aResult.RejectValue());
484
return;
485
}
486
487
nsresult rv =
488
mRecorder->CreateAndDispatchBlobEvent(aResult.ResolveValue());
489
if (NS_FAILED(rv)) {
490
DoSessionEndTask(NS_OK);
491
}
492
});
493
}
494
495
void MaybeCreateMutableBlobStorage() {
496
if (!mMutableBlobStorage) {
497
mMutableBlobStorage = new MutableBlobStorage(
498
MutableBlobStorage::eCouldBeInTemporaryFile, nullptr, mMaxMemory);
499
}
500
}
501
502
static const bool IsExclusive = false;
503
using BlobPromise =
504
MozPromise<nsMainThreadPtrHandle<Blob>, nsresult, IsExclusive>;
505
class BlobStorer : public MutableBlobStorageCallback {
506
MozPromiseHolder<BlobPromise> mHolder;
507
508
virtual ~BlobStorer() = default;
509
510
public:
511
BlobStorer() = default;
512
513
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BlobStorer, override)
514
515
void BlobStoreCompleted(MutableBlobStorage*, Blob* aBlob,
516
nsresult aRv) override {
517
MOZ_ASSERT(NS_IsMainThread());
518
if (NS_FAILED(aRv)) {
519
mHolder.Reject(aRv, __func__);
520
} else {
521
mHolder.Resolve(nsMainThreadPtrHandle<Blob>(
522
MakeAndAddRef<nsMainThreadPtrHolder<Blob>>(
523
"BlobStorer::ResolveBlob", aBlob)),
524
__func__);
525
}
526
}
527
528
RefPtr<BlobPromise> Promise() { return mHolder.Ensure(__func__); }
529
};
530
531
protected:
532
RefPtr<BlobPromise> GatherBlobImpl() {
533
RefPtr<BlobStorer> storer = MakeAndAddRef<BlobStorer>();
534
MaybeCreateMutableBlobStorage();
535
mMutableBlobStorage->GetBlobWhenReady(
536
mRecorder->GetOwner(), NS_ConvertUTF16toUTF8(mMimeType), storer);
537
mMutableBlobStorage = nullptr;
538
539
storer->Promise()->Then(
540
mMainThread, __func__,
541
[self = RefPtr<Session>(this), p = storer->Promise()] {
542
if (self->mBlobPromise == p) {
543
// Reset BlobPromise.
544
self->mBlobPromise = nullptr;
545
}
546
});
547
548
return storer->Promise();
549
}
550
551
public:
552
// Stops gathering data into the current blob and resolves when the current
553
// blob is available. Future data will be stored in a new blob.
554
// Should a previous async GatherBlob() operation still be in progress, we'll
555
// wait for it to finish before starting this one.
556
RefPtr<BlobPromise> GatherBlob() {
557
MOZ_ASSERT(NS_IsMainThread());
558
if (!mBlobPromise) {
559
return mBlobPromise = GatherBlobImpl();
560
}
561
return mBlobPromise = mBlobPromise->Then(mMainThread, __func__,
562
[self = RefPtr<Session>(this)] {
563
return self->GatherBlobImpl();
564
});
565
}
566
567
RefPtr<SizeOfPromise> SizeOfExcludingThis(
568
mozilla::MallocSizeOf aMallocSizeOf) {
569
MOZ_ASSERT(NS_IsMainThread());
570
size_t encodedBufferSize =
571
mMutableBlobStorage ? mMutableBlobStorage->SizeOfCurrentMemoryBuffer()
572
: 0;
573
574
if (!mEncoder) {
575
return SizeOfPromise::CreateAndResolve(encodedBufferSize, __func__);
576
}
577
578
auto& encoder = mEncoder;
579
return InvokeAsync(
580
mEncoderThread, __func__,
581
[encoder, encodedBufferSize, aMallocSizeOf]() {
582
return SizeOfPromise::CreateAndResolve(
583
encodedBufferSize + encoder->SizeOfExcludingThis(aMallocSizeOf),
584
__func__);
585
});
586
}
587
588
private:
589
virtual ~Session() {
590
MOZ_ASSERT(NS_IsMainThread());
591
MOZ_ASSERT(mShutdownPromise);
592
LOG(LogLevel::Debug, ("Session.~Session (%p)", this));
593
}
594
595
// Pull encoded media data from MediaEncoder and put into MutableBlobStorage.
596
// If the bool aForceFlush is true, we will force a dispatch of a blob to
597
// main thread.
598
void Extract(bool aForceFlush) {
599
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
600
601
LOG(LogLevel::Debug, ("Session.Extract %p", this));
602
603
AUTO_PROFILER_LABEL("MediaRecorder::Session::Extract", OTHER);
604
605
// Pull encoded media data from MediaEncoder
606
nsTArray<nsTArray<uint8_t>> encodedBuf;
607
nsresult rv = mEncoder->GetEncodedData(&encodedBuf);
608
if (NS_FAILED(rv)) {
609
MOZ_RELEASE_ASSERT(encodedBuf.IsEmpty());
610
// Even if we failed to encode more data, it might be time to push a blob
611
// with already encoded data.
612
}
613
614
// Append pulled data into cache buffer.
615
NS_DispatchToMainThread(
616
new StoreEncodedBufferRunnable(this, std::move(encodedBuf)));
617
618
// Whether push encoded data back to onDataAvailable automatically or we
619
// need a flush.
620
bool pushBlob = aForceFlush;
621
if (!pushBlob && mTimeSlice > 0 &&
622
(TimeStamp::Now() - mLastBlobTimeStamp).ToMilliseconds() > mTimeSlice) {
623
pushBlob = true;
624
}
625
if (pushBlob) {
626
mLastBlobTimeStamp = TimeStamp::Now();
627
InvokeAsync(mMainThread, this, __func__, &Session::GatherBlob)
628
->Then(mMainThread, __func__,
629
[this, self = RefPtr<Session>(this)](
630
const BlobPromise::ResolveOrRejectValue& aResult) {
631
if (aResult.IsReject()) {
632
LOG(LogLevel::Warning,
633
("GatherBlob failed for pushing blob"));
634
DoSessionEndTask(aResult.RejectValue());
635
return;
636
}
637
638
nsresult rv = mRecorder->CreateAndDispatchBlobEvent(
639
aResult.ResolveValue());
640
if (NS_FAILED(rv)) {
641
DoSessionEndTask(NS_OK);
642
}
643
});
644
}
645
}
646
647
void MediaStreamReady() {
648
if (!mMediaStream) {
649
// Already shut down. This can happen because MediaStreamReady is async.
650
return;
651
}
652
653
if (mMediaStreamReady) {
654
return;
655
}
656
657
if (!mRunningState.isOk() ||
658
mRunningState.inspect() != RunningState::Idling) {
659
return;
660
}
661
662
nsTArray<RefPtr<mozilla::dom::MediaStreamTrack>> tracks;
663
mMediaStream->GetTracks(tracks);
664
uint8_t trackTypes = 0;
665
int32_t audioTracks = 0;
666
int32_t videoTracks = 0;
667
for (auto& track : tracks) {
668
if (track->Ended()) {
669
continue;
670
}
671
672
ConnectMediaStreamTrack(*track);
673
674
if (track->AsAudioStreamTrack()) {
675
++audioTracks;
676
trackTypes |= ContainerWriter::CREATE_AUDIO_TRACK;
677
} else if (track->AsVideoStreamTrack()) {
678
++videoTracks;
679
trackTypes |= ContainerWriter::CREATE_VIDEO_TRACK;
680
} else {
681
MOZ_CRASH("Unexpected track type");
682
}
683
}
684
685
if (trackTypes == 0) {
686
MOZ_ASSERT(audioTracks == 0);
687
MOZ_ASSERT(videoTracks == 0);
688
return;
689
}
690
691
mMediaStreamReady = true;
692
693
if (audioTracks > 1 || videoTracks > 1) {
694
// When MediaRecorder supports multiple tracks, we should set up a single
695
// MediaInputPort from the input stream, and let main thread check
696
// track principals async later.
697
nsPIDOMWindowInner* window = mRecorder->GetOwner();
698
Document* document = window ? window->GetExtantDoc() : nullptr;
699
nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
700
NS_LITERAL_CSTRING("Media"), document,
701
nsContentUtils::eDOM_PROPERTIES,
702
"MediaRecorderMultiTracksNotSupported");
703
DoSessionEndTask(NS_ERROR_ABORT);
704
return;
705
}
706
707
// Check that we may access the tracks' content.
708
if (!MediaStreamTracksPrincipalSubsumes()) {
709
LOG(LogLevel::Warning, ("Session.MediaTracksReady MediaStreamTracks "
710
"principal check failed"));
711
DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
712
return;
713
}
714
715
LOG(LogLevel::Debug,
716
("Session.MediaTracksReady track type = (%d)", trackTypes));
717
InitEncoder(trackTypes, mMediaStreamTracks[0]->Graph()->GraphRate());
718
}
719
720
void ConnectMediaStreamTrack(MediaStreamTrack& aTrack) {
721
for (auto& track : mMediaStreamTracks) {
722
if (track->AsAudioStreamTrack() && aTrack.AsAudioStreamTrack()) {
723
// We only allow one audio track. See bug 1276928.
724
return;
725
}
726
if (track->AsVideoStreamTrack() && aTrack.AsVideoStreamTrack()) {
727
// We only allow one video track. See bug 1276928.
728
return;
729
}
730
}
731
mMediaStreamTracks.AppendElement(&aTrack);
732
aTrack.AddPrincipalChangeObserver(this);
733
}
734
735
bool PrincipalSubsumes(nsIPrincipal* aPrincipal) {
736
if (!mRecorder->GetOwner()) return false;
737
nsCOMPtr<Document> doc = mRecorder->GetOwner()->GetExtantDoc();
738
if (!doc) {
739
return false;
740
}
741
if (!aPrincipal) {
742
return false;
743
}
744
bool subsumes;
745
if (NS_FAILED(doc->NodePrincipal()->Subsumes(aPrincipal, &subsumes))) {
746
return false;
747
}
748
return subsumes;
749
}
750
751
bool MediaStreamTracksPrincipalSubsumes() {
752
MOZ_ASSERT(mRecorder->mDOMStream);
753
nsCOMPtr<nsIPrincipal> principal = nullptr;
754
for (RefPtr<MediaStreamTrack>& track : mMediaStreamTracks) {
755
nsContentUtils::CombineResourcePrincipals(&principal,
756
track->GetPrincipal());
757
}
758
return PrincipalSubsumes(principal);
759
}
760
761
bool AudioNodePrincipalSubsumes() {
762
MOZ_ASSERT(mRecorder->mAudioNode);
763
Document* doc = mRecorder->mAudioNode->GetOwner()
764
? mRecorder->mAudioNode->GetOwner()->GetExtantDoc()
765
: nullptr;
766
nsCOMPtr<nsIPrincipal> principal = doc ? doc->NodePrincipal() : nullptr;
767
return PrincipalSubsumes(principal);
768
}
769
770
void InitEncoder(uint8_t aTrackTypes, TrackRate aTrackRate) {
771
LOG(LogLevel::Debug, ("Session.InitEncoder %p", this));
772
MOZ_ASSERT(NS_IsMainThread());
773
774
if (!mRunningState.isOk() ||
775
mRunningState.inspect() != RunningState::Idling) {
776
MOZ_ASSERT_UNREACHABLE("Double-init");
777
return;
778
}
779
780
// Create a TaskQueue to read encode media data from MediaEncoder.
781
MOZ_RELEASE_ASSERT(!mEncoderThread);
782
RefPtr<SharedThreadPool> pool =
783
GetMediaThreadPool(MediaThreadType::WEBRTC_DECODER);
784
if (!pool) {
785
LOG(LogLevel::Debug, ("Session.InitEncoder %p Failed to create "
786
"MediaRecorderReadThread thread pool",
787
this));
788
DoSessionEndTask(NS_ERROR_FAILURE);
789
return;
790
}
791
792
mEncoderThread =
793
MakeAndAddRef<TaskQueue>(pool.forget(), "MediaRecorderReadThread");
794
795
if (!gMediaRecorderShutdownBlocker) {
796
// Add a shutdown blocker so mEncoderThread can be shutdown async.
797
class Blocker : public ShutdownBlocker {
798
public:
799
Blocker()
800
: ShutdownBlocker(
801
NS_LITERAL_STRING("MediaRecorder::Session: shutdown")) {}
802
803
NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient*) override {
804
// Distribute the global async shutdown blocker in a ticket. If there
805
// are zero graphs then shutdown is unblocked when we go out of scope.
806
RefPtr<ShutdownTicket> ticket =
807
MakeAndAddRef<ShutdownTicket>(gMediaRecorderShutdownBlocker);
808
gMediaRecorderShutdownBlocker = nullptr;
809
810
nsTArray<RefPtr<ShutdownPromise>> promises(gSessions.Count());
811
for (auto iter = gSessions.Iter(); !iter.Done(); iter.Next()) {
812
promises.AppendElement(iter.Get()->GetKey()->Shutdown());
813
}
814
gSessions.Clear();
815
ShutdownPromise::All(GetCurrentThreadSerialEventTarget(), promises)
816
->Then(
817
GetCurrentThreadSerialEventTarget(), __func__,
818
[ticket]() mutable {
819
MOZ_ASSERT(gSessions.Count() == 0);
820
// Unblock shutdown
821
ticket = nullptr;
822
},
823
[]() { MOZ_CRASH("Not reached"); });
824
return NS_OK;
825
}
826
};
827
828
gMediaRecorderShutdownBlocker = MakeAndAddRef<Blocker>();
829
RefPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
830
nsresult rv = barrier->AddBlocker(
831
gMediaRecorderShutdownBlocker, NS_LITERAL_STRING(__FILE__), __LINE__,
832
NS_LITERAL_STRING("MediaRecorder::Session: shutdown"));
833
MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
834
}
835
836
gSessions.PutEntry(this);
837
838
uint32_t audioBitrate = mRecorder->GetAudioBitrate();
839
uint32_t videoBitrate = mRecorder->GetVideoBitrate();
840
uint32_t bitrate = mRecorder->GetBitrate();
841
if (bitrate > 0) {
842
// There's a total cap set. We have to make sure the type-specific limits
843
// are within range.
844
if ((aTrackTypes & ContainerWriter::CREATE_AUDIO_TRACK) &&
845
(aTrackTypes & ContainerWriter::CREATE_VIDEO_TRACK) &&
846
audioBitrate + videoBitrate > bitrate) {
847
LOG(LogLevel::Info, ("Session.InitEncoder Bitrates higher than total "
848
"cap. Recalculating."));
849
double factor =
850
bitrate / static_cast<double>(audioBitrate + videoBitrate);
851
audioBitrate = static_cast<uint32_t>(audioBitrate * factor);
852
videoBitrate = static_cast<uint32_t>(videoBitrate * factor);
853
} else if ((aTrackTypes & ContainerWriter::CREATE_AUDIO_TRACK) &&
854
!(aTrackTypes & ContainerWriter::CREATE_VIDEO_TRACK)) {
855
audioBitrate = std::min(audioBitrate, bitrate);
856
videoBitrate = 0;
857
} else if (!(aTrackTypes & ContainerWriter::CREATE_AUDIO_TRACK) &&
858
(aTrackTypes & ContainerWriter::CREATE_VIDEO_TRACK)) {
859
audioBitrate = 0;
860
videoBitrate = std::min(videoBitrate, bitrate);
861
}
862
MOZ_ASSERT(audioBitrate + videoBitrate <= bitrate);
863
}
864
865
// Allocate encoder and bind with union stream.
866
// At this stage, the API doesn't allow UA to choose the output mimeType
867
// format.
868
869
mEncoder =
870
MediaEncoder::CreateEncoder(mEncoderThread, mMimeType, audioBitrate,
871
videoBitrate, aTrackTypes, aTrackRate);
872
873
if (!mEncoder) {
874
LOG(LogLevel::Error, ("Session.InitEncoder !mEncoder %p", this));
875
DoSessionEndTask(NS_ERROR_ABORT);
876
return;
877
}
878
879
mEncoderListener = MakeAndAddRef<EncoderListener>(mEncoderThread, this);
880
nsresult rv =
881
mEncoderThread->Dispatch(NewRunnableMethod<RefPtr<EncoderListener>>(
882
"mozilla::MediaEncoder::RegisterListener", mEncoder,
883
&MediaEncoder::RegisterListener, mEncoderListener));
884
MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
885
Unused << rv;
886
887
if (mRecorder->mAudioNode) {
888
mEncoder->ConnectAudioNode(mRecorder->mAudioNode,
889
mRecorder->mAudioNodeOutput);
890
}
891
892
for (auto& track : mMediaStreamTracks) {
893
mEncoder->ConnectMediaStreamTrack(track);
894
}
895
896
// If user defines timeslice interval for video blobs we have to set
897
// appropriate video keyframe interval defined in milliseconds.
898
mEncoder->SetVideoKeyFrameInterval(mTimeSlice);
899
900
// Set mRunningState to Running so that DoSessionEndTask will
901
// take the responsibility to end the session.
902
mRunningState = RunningState::Starting;
903
}
904
905
// This is the task that will stop recording per spec:
906
// - Stop gathering data (this is inherently async)
907
// - Set state to "inactive"
908
// - Fire an error event, if NS_FAILED(rv)
909
// - Discard blob data if rv is NS_ERROR_DOM_SECURITY_ERR
910
// - Fire a Blob event
911
// - Fire an event named stop
912
void DoSessionEndTask(nsresult rv) {
913
MOZ_ASSERT(NS_IsMainThread());
914
if (mRunningState.isErr()) {
915
// We have already ended with an error.
916
return;
917
}
918
919
if (mRunningState.isOk() &&
920
mRunningState.inspect() == RunningState::Stopped) {
921
// We have already ended gracefully.
922
return;
923
}
924
925
bool needsStartEvent = false;
926
if (mRunningState.isOk() &&
927
(mRunningState.inspect() == RunningState::Idling ||
928
mRunningState.inspect() == RunningState::Starting)) {
929
needsStartEvent = true;
930
}
931
932
if (rv == NS_OK) {
933
mRunningState = RunningState::Stopped;
934
} else {
935
mRunningState = Err(rv);
936
}
937
938
GatherBlob()
939
->Then(mMainThread, __func__,
940
[this, self = RefPtr<Session>(this), rv, needsStartEvent](
941
const BlobPromise::ResolveOrRejectValue& aResult) {
942
if (mRecorder->mSessions.LastElement() == this) {
943
// Set state to inactive, but only if the recorder is not
944
// controlled by another session already.
945
mRecorder->ForceInactive();
946
}
947
948
if (needsStartEvent) {
949
mRecorder->DispatchSimpleEvent(NS_LITERAL_STRING("start"));
950
}
951
952
// If there was an error, Fire the appropriate one
953
if (NS_FAILED(rv)) {
954
mRecorder->NotifyError(rv);
955
}
956
957
// Fire a blob event named dataavailable
958
RefPtr<Blob> blob;
959
if (rv == NS_ERROR_DOM_SECURITY_ERR || aResult.IsReject()) {
960
// In case of SecurityError, the blob data must be discarded.
961
// We create a new empty one and throw the blob with its data
962
// away.
963
// In case we failed to gather blob data, we create an empty
964
// memory blob instead.
965
blob = Blob::CreateEmptyBlob(mRecorder->GetParentObject(),
966
mMimeType);
967
} else {
968
blob = aResult.ResolveValue();
969
}
970
if (NS_FAILED(mRecorder->CreateAndDispatchBlobEvent(blob))) {
971
// Failed to dispatch blob event. That's unexpected. It's
972
// probably all right to fire an error event if we haven't
973
// already.
974
if (NS_SUCCEEDED(rv)) {
975
mRecorder->NotifyError(NS_ERROR_FAILURE);
976
}
977
}
978
979
// Dispatch stop event and clear MIME type.
980
mMimeType = NS_LITERAL_STRING("");
981
mRecorder->SetMimeType(mMimeType);
982
983
// Fire an event named stop
984
mRecorder->DispatchSimpleEvent(NS_LITERAL_STRING("stop"));
985
986
// And finally, Shutdown and destroy the Session
987
return Shutdown();
988
})
989
->Then(mMainThread, __func__, [this, self = RefPtr<Session>(this)] {
990
gSessions.RemoveEntry(this);
991
if (gSessions.Count() == 0 && gMediaRecorderShutdownBlocker) {
992
// All sessions finished before shutdown, no need to keep the
993
// blocker.
994
RefPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
995
barrier->RemoveBlocker(gMediaRecorderShutdownBlocker);
996
gMediaRecorderShutdownBlocker = nullptr;
997
}
998
});
999
}
1000
1001
void MediaEncoderInitialized() {
1002
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
1003
1004
NS_DispatchToMainThread(NewRunnableFrom([self = RefPtr<Session>(this), this,
1005
mime = mEncoder->MimeType()]() {
1006
if (mRunningState.isErr()) {
1007
return NS_OK;
1008
}
1009
mMimeType = mime;
1010
mRecorder->SetMimeType(mime);
1011
RunningState state = mRunningState.inspect();
1012
if (state == RunningState::Starting || state == RunningState::Stopping) {
1013
if (state == RunningState::Starting) {
1014
// We set it to Running in the runnable since we can only assign
1015
// mRunningState on main thread. We set it before running the start
1016
// event runnable since that dispatches synchronously (and may cause
1017
// js calls to methods depending on mRunningState).
1018
mRunningState = RunningState::Running;
1019
}
1020
mRecorder->DispatchSimpleEvent(NS_LITERAL_STRING("start"));
1021
}
1022
return NS_OK;
1023
}));
1024
1025
Extract(false);
1026
}
1027
1028
void MediaEncoderDataAvailable() {
1029
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
1030
1031
Extract(false);
1032
}
1033
1034
void MediaEncoderError() {
1035
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
1036
NS_DispatchToMainThread(NewRunnableMethod<nsresult>(
1037
"dom::MediaRecorder::Session::DoSessionEndTask", this,
1038
&Session::DoSessionEndTask, NS_ERROR_FAILURE));
1039
}
1040
1041
void MediaEncoderShutdown() {
1042
MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
1043
MOZ_ASSERT(mEncoder->IsShutdown());
1044
1045
mMainThread->Dispatch(NewRunnableMethod<nsresult>(
1046
"MediaRecorder::Session::MediaEncoderShutdown->DoSessionEndTask", this,
1047
&Session::DoSessionEndTask, NS_OK));
1048
1049
// Clean up.
1050
mEncoderListener->Forget();
1051
DebugOnly<bool> unregistered =
1052
mEncoder->UnregisterListener(mEncoderListener);
1053
MOZ_ASSERT(unregistered);
1054
}
1055
1056
RefPtr<ShutdownPromise> Shutdown() {
1057
MOZ_ASSERT(NS_IsMainThread());
1058
LOG(LogLevel::Debug, ("Session Shutdown %p", this));
1059
1060
if (mShutdownPromise) {
1061
return mShutdownPromise;
1062
}
1063
1064
// This is a coarse calculation and does not reflect the duration of the
1065
// final recording for reasons such as pauses. However it allows us an
1066
// idea of how long people are running their recorders for.
1067
TimeDuration timeDelta = TimeStamp::Now() - mStartTime;
1068
Telemetry::Accumulate(Telemetry::MEDIA_RECORDER_RECORDING_DURATION,
1069
timeDelta.ToSeconds());
1070
1071
mShutdownPromise = ShutdownPromise::CreateAndResolve(true, __func__);
1072
RefPtr<Session> self = this;
1073
1074
if (mEncoder) {
1075
auto& encoder = mEncoder;
1076
encoder->Cancel();
1077
1078
MOZ_RELEASE_ASSERT(mEncoderListener);
1079
auto& encoderListener = mEncoderListener;
1080
mShutdownPromise = mShutdownPromise->Then(
1081
mEncoderThread, __func__,
1082
[encoder, encoderListener]() {
1083
encoder->UnregisterListener(encoderListener);
1084
encoderListener->Forget();
1085
return ShutdownPromise::CreateAndResolve(true, __func__);
1086
},
1087
[]() {
1088
MOZ_ASSERT_UNREACHABLE("Unexpected reject");
1089
return ShutdownPromise::CreateAndReject(false, __func__);
1090
});
1091
}
1092
1093
// Remove main thread state. This could be needed if Stop() wasn't called.
1094
if (mMediaStream) {
1095
mMediaStream->UnregisterTrackListener(this);
1096
mMediaStream = nullptr;
1097
}
1098
1099
{
1100
auto tracks(std::move(mMediaStreamTracks));
1101
for (RefPtr<MediaStreamTrack>& track : tracks) {
1102
track->RemovePrincipalChangeObserver(this);
1103
}
1104
}
1105
1106
// Break the cycle reference between Session and MediaRecorder.
1107
mShutdownPromise = mShutdownPromise->Then(
1108
GetCurrentThreadSerialEventTarget(), __func__,
1109
[self]() {
1110
self->mRecorder->RemoveSession(self);
1111
return ShutdownPromise::CreateAndResolve(true, __func__);
1112
},
1113
[]() {
1114
MOZ_ASSERT_UNREACHABLE("Unexpected reject");
1115
return ShutdownPromise::CreateAndReject(false, __func__);
1116
});
1117
1118
if (mEncoderThread) {
1119
RefPtr<TaskQueue>& encoderThread = mEncoderThread;
1120
mShutdownPromise = mShutdownPromise->Then(
1121
GetCurrentThreadSerialEventTarget(), __func__,
1122
[encoderThread]() { return encoderThread->BeginShutdown(); },
1123
[]() {
1124
MOZ_ASSERT_UNREACHABLE("Unexpected reject");
1125
return ShutdownPromise::CreateAndReject(false, __func__);
1126
});
1127
}
1128
1129
return mShutdownPromise;
1130
}
1131
1132
private:
1133
enum class RunningState {
1134
Idling, // Session has been created
1135
Starting, // MediaEncoder started, waiting for data
1136
Running, // MediaEncoder has produced data
1137
Stopping, // Stop() has been called
1138
Stopped, // Session has stopped without any error
1139
};
1140
1141
// Our associated MediaRecorder.
1142
const RefPtr<MediaRecorder> mRecorder;
1143
1144
// Stream currently recorded.
1145
RefPtr<DOMMediaStream> mMediaStream;
1146
1147
// True after we have decided on the track set to use for the recording.
1148
bool mMediaStreamReady;
1149
1150
// Tracks currently recorded. This should be a subset of mMediaStream's track
1151
// set.
1152
nsTArray<RefPtr<MediaStreamTrack>> mMediaStreamTracks;
1153
1154
// Main thread used for MozPromise operations.
1155
const RefPtr<nsISerialEventTarget> mMainThread;
1156
// Runnable thread for reading data from MediaEncoder.
1157
RefPtr<TaskQueue> mEncoderThread;
1158
// MediaEncoder pipeline.
1159
RefPtr<MediaEncoder> mEncoder;
1160
// Listener through which MediaEncoder signals us.
1161
RefPtr<EncoderListener> mEncoderListener;
1162
// Set in Shutdown() and resolved when shutdown is complete.
1163
RefPtr<ShutdownPromise> mShutdownPromise;
1164
// A buffer to cache encoded media data.
1165
RefPtr<MutableBlobStorage> mMutableBlobStorage;
1166
// Max memory to use for the MutableBlobStorage.
1167
uint64_t mMaxMemory;
1168
// If set, is a promise for the latest GatherBlob() operation. Allows
1169
// GatherBlob() operations to be serialized in order to avoid races.
1170
RefPtr<BlobPromise> mBlobPromise;
1171
// Current session mimeType
1172
nsString mMimeType;
1173
// Timestamp of the last fired dataavailable event.
1174
TimeStamp mLastBlobTimeStamp;
1175
// The interval of passing encoded data from MutableBlobStorage to
1176
// onDataAvailable handler.
1177
const uint32_t mTimeSlice;
1178
// The time this session started, for telemetry.
1179
const TimeStamp mStartTime;
1180
// The session's current main thread state. The error type gets set when
1181
// ending a recording with an error. An NS_OK error is invalid.
1182
// Main thread only.
1183
Result<RunningState, nsresult> mRunningState;
1184
};
1185
1186
MediaRecorder::~MediaRecorder() {
1187
LOG(LogLevel::Debug, ("~MediaRecorder (%p)", this));
1188
UnRegisterActivityObserver();
1189
}
1190
1191
MediaRecorder::MediaRecorder(DOMMediaStream& aSourceMediaStream,
1192
nsPIDOMWindowInner* aOwnerWindow)
1193
: DOMEventTargetHelper(aOwnerWindow),
1194
mAudioNodeOutput(0),
1195
mState(RecordingState::Inactive),
1196
mAudioBitsPerSecond(0),
1197
mVideoBitsPerSecond(0),
1198
mBitsPerSecond(0) {
1199
MOZ_ASSERT(aOwnerWindow);
1200
mDOMStream = &aSourceMediaStream;
1201
1202
RegisterActivityObserver();
1203
}
1204
1205
MediaRecorder::MediaRecorder(AudioNode& aSrcAudioNode, uint32_t aSrcOutput,
1206
nsPIDOMWindowInner* aOwnerWindow)
1207
: DOMEventTargetHelper(aOwnerWindow),
1208
mAudioNodeOutput(aSrcOutput),
1209
mState(RecordingState::Inactive),
1210
mAudioBitsPerSecond(0),
1211
mVideoBitsPerSecond(0),
1212
mBitsPerSecond(0) {
1213
MOZ_ASSERT(aOwnerWindow);
1214
1215
mAudioNode = &aSrcAudioNode;
1216
1217
RegisterActivityObserver();
1218
}
1219
1220
void MediaRecorder::RegisterActivityObserver() {
1221
if (nsPIDOMWindowInner* window = GetOwner()) {
1222
mDocument = window->GetExtantDoc();
1223
if (mDocument) {
1224
mDocument->RegisterActivityObserver(
1225
NS_ISUPPORTS_CAST(nsIDocumentActivity*, this));
1226
}
1227
}
1228
}
1229
1230
void MediaRecorder::UnRegisterActivityObserver() {
1231
if (mDocument) {
1232
mDocument->UnregisterActivityObserver(
1233
NS_ISUPPORTS_CAST(nsIDocumentActivity*, this));
1234
}
1235
}
1236
1237
void MediaRecorder::SetMimeType(const nsString& aMimeType) {
1238
mMimeType = aMimeType;
1239
}
1240
1241
void MediaRecorder::GetMimeType(nsString& aMimeType) { aMimeType = mMimeType; }
1242
1243
void MediaRecorder::Start(const Optional<uint32_t>& aTimeSlice,
1244
ErrorResult& aResult) {
1245
LOG(LogLevel::Debug, ("MediaRecorder.Start %p", this));
1246
1247
InitializeDomExceptions();
1248
1249
if (mState != RecordingState::Inactive) {
1250
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
1251
return;
1252
}
1253
1254
nsTArray<RefPtr<MediaStreamTrack>> tracks;
1255
if (mDOMStream) {
1256
mDOMStream->GetTracks(tracks);
1257
}
1258
if (!tracks.IsEmpty()) {
1259
// If there are tracks already available that we're not allowed
1260
// to record, we should throw a security error.
1261
RefPtr<nsIPrincipal> streamPrincipal = mDOMStream->GetPrincipal();
1262
bool subsumes = false;
1263
nsPIDOMWindowInner* window;
1264
Document* doc;
1265
if (!(window = GetOwner()) || !(doc = window->GetExtantDoc()) ||
1266
NS_FAILED(doc->NodePrincipal()->Subsumes(streamPrincipal, &subsumes)) ||
1267
!subsumes) {
1268
aResult.Throw(NS_ERROR_DOM_SECURITY_ERR);
1269
return;
1270
}
1271
}
1272
1273
uint32_t timeSlice = aTimeSlice.WasPassed() ? aTimeSlice.Value() : 0;
1274
MediaRecorderReporter::AddMediaRecorder(this);
1275
mState = RecordingState::Recording;
1276
// Start a session.
1277
mSessions.AppendElement();
1278
mSessions.LastElement() = new Session(this, timeSlice);
1279
mSessions.LastElement()->Start();
1280
}
1281
1282
void MediaRecorder::Stop(ErrorResult& aResult) {
1283
LOG(LogLevel::Debug, ("MediaRecorder.Stop %p", this));
1284
MediaRecorderReporter::RemoveMediaRecorder(this);
1285
if (mState == RecordingState::Inactive) {
1286
return;
1287
}
1288
mState = RecordingState::Inactive;
1289
MOZ_ASSERT(mSessions.Length() > 0);
1290
mSessions.LastElement()->Stop();
1291
}
1292
1293
void MediaRecorder::Pause(ErrorResult& aResult) {
1294
LOG(LogLevel::Debug, ("MediaRecorder.Pause %p", this));
1295
if (mState == RecordingState::Inactive) {
1296
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
1297
return;
1298
}
1299
1300
if (mState == RecordingState::Paused) {
1301
return;
1302
}
1303
1304
MOZ_ASSERT(mSessions.Length() > 0);
1305
nsresult rv = mSessions.LastElement()->Pause();
1306
if (NS_FAILED(rv)) {
1307
NotifyError(rv);
1308
return;
1309
}
1310
1311
mState = RecordingState::Paused;
1312
}
1313
1314
void MediaRecorder::Resume(ErrorResult& aResult) {
1315
LOG(LogLevel::Debug, ("MediaRecorder.Resume %p", this));
1316
if (mState == RecordingState::Inactive) {
1317
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
1318
return;
1319
}
1320
1321
if (mState == RecordingState::Recording) {
1322
return;
1323
}
1324
1325
MOZ_ASSERT(mSessions.Length() > 0);
1326
nsresult rv = mSessions.LastElement()->Resume();
1327
if (NS_FAILED(rv)) {
1328
NotifyError(rv);
1329
return;
1330
}
1331
1332
mState = RecordingState::Recording;
1333
}
1334
1335
void MediaRecorder::RequestData(ErrorResult& aResult) {
1336
if (mState == RecordingState::Inactive) {
1337
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
1338
return;
1339
}
1340
MOZ_ASSERT(mSessions.Length() > 0);
1341
mSessions.LastElement()->RequestData();
1342
}
1343
1344
JSObject* MediaRecorder::WrapObject(JSContext* aCx,
1345
JS::Handle<JSObject*> aGivenProto) {
1346
return MediaRecorder_Binding::Wrap(aCx, this, aGivenProto);
1347
}
1348
1349
/* static */
1350
already_AddRefed<MediaRecorder> MediaRecorder::Constructor(
1351
const GlobalObject& aGlobal, DOMMediaStream& aStream,
1352
const MediaRecorderOptions& aInitDict, ErrorResult& aRv) {
1353
nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
1354
do_QueryInterface(aGlobal.GetAsSupports());
1355
if (!ownerWindow) {
1356
aRv.Throw(NS_ERROR_FAILURE);
1357
return nullptr;
1358
}
1359
1360
if (!IsTypeSupported(aInitDict.mMimeType)) {
1361
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
1362
return nullptr;
1363
}
1364
1365
RefPtr<MediaRecorder> object = new MediaRecorder(aStream, ownerWindow);
1366
object->SetOptions(aInitDict);
1367
return object.forget();
1368
}
1369
1370
/* static */
1371
already_AddRefed<MediaRecorder> MediaRecorder::Constructor(
1372
const GlobalObject& aGlobal, AudioNode& aSrcAudioNode, uint32_t aSrcOutput,
1373
const MediaRecorderOptions& aInitDict, ErrorResult& aRv) {
1374
// Allow recording from audio node only when pref is on.
1375
if (!Preferences::GetBool("media.recorder.audio_node.enabled", false)) {
1376
// Pretending that this constructor is not defined.
1377
NS_NAMED_LITERAL_STRING(argStr, "Argument 1 of MediaRecorder.constructor");
1378
NS_NAMED_LITERAL_STRING(typeStr, "MediaStream");
1379
aRv.ThrowTypeError<MSG_DOES_NOT_IMPLEMENT_INTERFACE>(argStr, typeStr);
1380
return nullptr;
1381
}
1382
1383
nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
1384
do_QueryInterface(aGlobal.GetAsSupports());
1385
if (!ownerWindow) {
1386
aRv.Throw(NS_ERROR_FAILURE);
1387
return nullptr;
1388
}
1389
1390
// aSrcOutput doesn't matter to destination node because it has no output.
1391
if (aSrcAudioNode.NumberOfOutputs() > 0 &&
1392
aSrcOutput >= aSrcAudioNode.NumberOfOutputs()) {
1393
aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
1394
return nullptr;
1395
}
1396
1397
if (!IsTypeSupported(aInitDict.mMimeType)) {
1398
aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
1399
return nullptr;
1400
}
1401
1402
RefPtr<MediaRecorder> object =
1403
new MediaRecorder(aSrcAudioNode, aSrcOutput, ownerWindow);
1404
object->SetOptions(aInitDict);
1405
return object.forget();
1406
}
1407
1408
void MediaRecorder::SetOptions(const MediaRecorderOptions& aInitDict) {
1409
SetMimeType(aInitDict.mMimeType);
1410
mAudioBitsPerSecond = aInitDict.mAudioBitsPerSecond.WasPassed()
1411
? aInitDict.mAudioBitsPerSecond.Value()
1412
: 0;
1413
mVideoBitsPerSecond = aInitDict.mVideoBitsPerSecond.WasPassed()
1414
? aInitDict.mVideoBitsPerSecond.Value()
1415
: 0;
1416
mBitsPerSecond = aInitDict.mBitsPerSecond.WasPassed()
1417
? aInitDict.mBitsPerSecond.Value()
1418
: 0;
1419
// We're not handling dynamic changes yet. Eventually we'll handle
1420
// setting audio, video and/or total -- and anything that isn't set,
1421
// we'll derive. Calculated versions require querying bitrates after
1422
// the encoder is Init()ed. This happens only after data is
1423
// available and thus requires dynamic changes.
1424
//
1425
// Until dynamic changes are supported, I prefer to be safe and err
1426
// slightly high
1427
if (aInitDict.mBitsPerSecond.WasPassed() &&
1428
!aInitDict.mVideoBitsPerSecond.WasPassed()) {
1429
mVideoBitsPerSecond = mBitsPerSecond;
1430
}
1431
}
1432
1433
static char const* const gWebMVideoEncoderCodecs[4] = {
1434
"opus",
1435
"vp8",
1436
"vp8.0",
1437
// no VP9 yet
1438
nullptr,
1439
};
1440
static char const* const gWebMAudioEncoderCodecs[4] = {
1441
"opus",
1442
nullptr,
1443
};
1444
static char const* const gOggAudioEncoderCodecs[2] = {
1445
"opus",
1446
// we could support vorbis here too, but don't
1447
nullptr,
1448
};
1449
1450
template <class String>
1451
static bool CodecListContains(char const* const* aCodecs,
1452
const String& aCodec) {
1453
for (int32_t i = 0; aCodecs[i]; ++i) {
1454
if (aCodec.EqualsASCII(aCodecs[i])) return true;
1455
}
1456
return false;
1457
}
1458
1459
/* static */
1460
bool MediaRecorder::IsTypeSupported(GlobalObject& aGlobal,
1461
const nsAString& aMIMEType) {
1462
return IsTypeSupported(aMIMEType);
1463
}
1464
1465
/* static */
1466
bool MediaRecorder::IsTypeSupported(const nsAString& aMIMEType) {
1467
char const* const* codeclist = nullptr;
1468
1469
if (aMIMEType.IsEmpty()) {
1470
return true;
1471
}
1472
1473
nsContentTypeParser parser(aMIMEType);
1474
nsAutoString mimeType;
1475
nsresult rv = parser.GetType(mimeType);
1476
if (NS_FAILED(rv)) {
1477
return false;
1478
}
1479
1480
// effectively a 'switch (mimeType) {'
1481
if (mimeType.EqualsLiteral(AUDIO_OGG)) {
1482
if (MediaDecoder::IsOggEnabled() && MediaDecoder::IsOpusEnabled()) {
1483
codeclist = gOggAudioEncoderCodecs;
1484
}
1485
}
1486
#ifdef MOZ_WEBM_ENCODER
1487
else if ((mimeType.EqualsLiteral(VIDEO_WEBM) ||
1488
mimeType.EqualsLiteral(AUDIO_WEBM)) &&
1489
MediaEncoder::IsWebMEncoderEnabled()) {
1490
if (mimeType.EqualsLiteral(AUDIO_WEBM)) {
1491
codeclist = gWebMAudioEncoderCodecs;
1492
} else {
1493
codeclist = gWebMVideoEncoderCodecs;
1494
}
1495
}
1496
#endif
1497
1498
// codecs don't matter if we don't support the container
1499
if (!codeclist) {
1500
return false;
1501
}
1502
// now filter on codecs, and if needed rescind support
1503
nsAutoString codecstring;
1504
rv = parser.GetParameter("codecs", codecstring);
1505
1506
nsTArray<nsString> codecs;
1507
if (!ParseCodecsString(codecstring, codecs)) {
1508
return false;
1509
}
1510
for (const nsString& codec : codecs) {
1511
if (!CodecListContains(codeclist, codec)) {
1512
// Totally unsupported codec
1513
return false;
1514
}
1515
}
1516
1517
return true;
1518
}
1519
1520
nsresult MediaRecorder::CreateAndDispatchBlobEvent(Blob* aBlob) {
1521
MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
1522
1523
BlobEventInit init;
1524
init.mBubbles = false;
1525
init.mCancelable = false;
1526
init.mData = aBlob;
1527
1528
RefPtr<BlobEvent> event =
1529
BlobEvent::Constructor(this, NS_LITERAL_STRING("dataavailable"), init);
1530
event->SetTrusted(true);
1531
ErrorResult rv;
1532
DispatchEvent(*event, rv);
1533
return rv.StealNSResult();
1534
}
1535
1536
void MediaRecorder::DispatchSimpleEvent(const nsAString& aStr) {
1537
MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
1538
nsresult rv = CheckCurrentGlobalCorrectness();
1539
if (NS_FAILED(rv)) {
1540
return;
1541
}
1542
1543
rv = DOMEventTargetHelper::DispatchTrustedEvent(aStr);
1544
if (NS_FAILED(rv)) {
1545
LOG(LogLevel::Error,
1546
("MediaRecorder.DispatchSimpleEvent: DispatchTrustedEvent failed %p",
1547
this));
1548
NS_ERROR("Failed to dispatch the event!!!");
1549
}
1550
}
1551
1552
void MediaRecorder::NotifyError(nsresult aRv) {
1553
MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
1554
nsresult rv = CheckCurrentGlobalCorrectness();
1555
if (NS_FAILED(rv)) {
1556
return;
1557
}
1558
MediaRecorderErrorEventInit init;
1559
init.mBubbles = false;
1560
init.mCancelable = false;
1561
// These DOMExceptions have been created earlier so they can contain stack
1562
// traces. We attach the appropriate one here to be fired. We should have
1563
// exceptions here, but defensively check.
1564
switch (aRv) {
1565
case NS_ERROR_DOM_SECURITY_ERR:
1566
if (!mSecurityDomException) {
1567
LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
1568
"mSecurityDomException was not initialized"));
1569
mSecurityDomException = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR);
1570
}
1571
init.mError = mSecurityDomException.forget();
1572
break;
1573
default:
1574
if (!mUnknownDomException) {
1575
LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
1576
"mUnknownDomException was not initialized"));
1577
mUnknownDomException = DOMException::Create(NS_ERROR_DOM_UNKNOWN_ERR);
1578
}
1579
LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
1580
"mUnknownDomException being fired for aRv: %X",
1581
uint32_t(aRv)));
1582
init.mError = mUnknownDomException.forget();
1583
}
1584
1585
RefPtr<MediaRecorderErrorEvent> event = MediaRecorderErrorEvent::Constructor(
1586
this, NS_LITERAL_STRING("error"), init);
1587
event->SetTrusted(true);
1588
1589
IgnoredErrorResult res;
1590
DispatchEvent(*event, res);
1591
if (res.Failed()) {
1592
NS_ERROR("Failed to dispatch the error event!!!");
1593
}
1594
}
1595
1596
void MediaRecorder::RemoveSession(Session* aSession) {
1597
LOG(LogLevel::Debug, ("MediaRecorder.RemoveSession (%p)", aSession));
1598
mSessions.RemoveElement(aSession);
1599
}
1600
1601
void MediaRecorder::NotifyOwnerDocumentActivityChanged() {
1602
nsPIDOMWindowInner* window = GetOwner();
1603
NS_ENSURE_TRUE_VOID(window);
1604
Document* doc = window->GetExtantDoc();
1605
NS_ENSURE_TRUE_VOID(doc);
1606
1607
bool inFrameSwap = false;
1608
if (nsDocShell* docShell = static_cast<nsDocShell*>(doc->GetDocShell())) {
1609
inFrameSwap = docShell->InFrameSwap();
1610
}
1611
1612
LOG(LogLevel::Debug, ("MediaRecorder %p NotifyOwnerDocumentActivityChanged "
1613
"IsActive=%d, "
1614
"IsVisible=%d, "
1615
"InFrameSwap=%d",
1616
this, doc->IsActive(), doc->IsVisible(), inFrameSwap));
1617
if (!doc->IsActive() || !(inFrameSwap || doc->IsVisible())) {
1618
// Stop the session.
1619
ErrorResult result;
1620
Stop(result);
1621
result.SuppressException();
1622
}
1623
}
1624
1625
void MediaRecorder::ForceInactive() {
1626
LOG(LogLevel::Debug, ("MediaRecorder.ForceInactive %p", this));
1627
mState = RecordingState::Inactive;
1628
}
1629
1630
void MediaRecorder::InitializeDomExceptions() {
1631
mSecurityDomException = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR);
1632
mUnknownDomException = DOMException::Create(NS_ERROR_DOM_UNKNOWN_ERR);
1633
}
1634
1635
RefPtr<MediaRecorder::SizeOfPromise> MediaRecorder::SizeOfExcludingThis(
1636
mozilla::MallocSizeOf aMallocSizeOf) {
1637
MOZ_ASSERT(NS_IsMainThread());
1638
1639
// The return type of a chained MozPromise cannot be changed, so we create a
1640
// holder for our desired return type and resolve that from All()->Then().
1641
auto holder = MakeRefPtr<Refcountable<MozPromiseHolder<SizeOfPromise>>>();
1642
RefPtr<SizeOfPromise> promise = holder->Ensure(__func__);
1643
1644
nsTArray<RefPtr<SizeOfPromise>> promises(mSessions.Length());
1645
for (const RefPtr<Session>& session : mSessions) {
1646
promises.AppendElement(session->SizeOfExcludingThis(aMallocSizeOf));
1647
}
1648
1649
SizeOfPromise::All(GetCurrentThreadSerialEventTarget(), promises)
1650
->Then(
1651
GetCurrentThreadSerialEventTarget(), __func__,
1652
[holder](const nsTArray<size_t>& sizes) {
1653
size_t total = 0;
1654
for (const size_t& size : sizes) {
1655
total += size;
1656
}
1657
holder->Resolve(total, __func__);
1658
},
1659
[]() { MOZ_CRASH("Unexpected reject"); });
1660
1661
return promise;
1662
}
1663
1664
StaticRefPtr<MediaRecorderReporter> MediaRecorderReporter::sUniqueInstance;
1665
1666
} // namespace dom
1667
} // namespace mozilla
1668
1669
#undef LOG