1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 #include "mozilla/dom/MediaDevices.h"
7 #include "AudioDeviceInfo.h"
8 #include "MediaEngine.h"
9 #include "MediaEngineFake.h"
10 #include "mozilla/dom/BrowsingContext.h"
11 #include "mozilla/dom/Document.h"
12 #include "mozilla/dom/FeaturePolicyUtils.h"
13 #include "mozilla/dom/MediaStreamBinding.h"
14 #include "mozilla/dom/MediaDeviceInfo.h"
15 #include "mozilla/dom/MediaDevicesBinding.h"
16 #include "mozilla/dom/NavigatorBinding.h"
17 #include "mozilla/dom/Promise.h"
18 #include "mozilla/dom/WindowContext.h"
19 #include "mozilla/intl/Localization.h"
20 #include "mozilla/MediaManager.h"
21 #include "mozilla/StaticPrefs_media.h"
22 #include "MediaTrackConstraints.h"
23 #include "nsContentUtils.h"
25 #include "nsIScriptGlobalObject.h"
26 #include "nsPIDOMWindow.h"
27 #include "nsGlobalWindowInner.h"
28 #include "nsQueryObject.h"
30 namespace mozilla::dom
{
32 using ConstDeviceSetPromise
= MediaManager::ConstDeviceSetPromise
;
33 using LocalDeviceSetPromise
= MediaManager::LocalDeviceSetPromise
;
34 using LocalMediaDeviceSetRefCnt
= MediaManager::LocalMediaDeviceSetRefCnt
;
35 using MediaDeviceSetRefCnt
= MediaManager::MediaDeviceSetRefCnt
;
36 using mozilla::intl::Localization
;
38 MediaDevices::MediaDevices(nsPIDOMWindowInner
* aWindow
)
39 : DOMEventTargetHelper(aWindow
), mDefaultOutputLabel(VoidString()) {}
41 MediaDevices::~MediaDevices() {
42 MOZ_ASSERT(NS_IsMainThread());
43 mDeviceChangeListener
.DisconnectIfExists();
46 already_AddRefed
<Promise
> MediaDevices::GetUserMedia(
47 const MediaStreamConstraints
& aConstraints
, CallerType aCallerType
,
49 MOZ_ASSERT(NS_IsMainThread());
50 // Get the relevant global for the promise from the wrapper cache because
51 // DOMEventTargetHelper::GetOwner() returns null if the document is unloaded.
52 // We know the wrapper exists because it is being used for |this| from JS.
53 // See https://github.com/heycam/webidl/issues/932 for why the relevant
54 // global is used instead of the current global.
55 nsCOMPtr
<nsIGlobalObject
> global
= xpc::NativeGlobal(GetWrapper());
56 // global is a window because MediaDevices is exposed only to Window.
57 nsCOMPtr
<nsPIDOMWindowInner
> owner
= do_QueryInterface(global
);
58 if (Document
* doc
= owner
->GetExtantDoc()) {
59 if (!owner
->IsSecureContext()) {
60 doc
->SetUseCounter(eUseCounter_custom_GetUserMediaInsec
);
62 Document
* topDoc
= doc
->GetTopLevelContentDocumentIfSameProcess();
63 IgnoredErrorResult ignored
;
64 if (topDoc
&& !topDoc
->HasFocus(ignored
)) {
65 doc
->SetUseCounter(eUseCounter_custom_GetUserMediaUnfocused
);
68 RefPtr
<Promise
> p
= Promise::Create(global
, aRv
);
69 if (NS_WARN_IF(aRv
.Failed())) {
72 /* If requestedMediaTypes is the empty set, return a promise rejected with a
74 if (!MediaManager::IsOn(aConstraints
.mVideo
) &&
75 !MediaManager::IsOn(aConstraints
.mAudio
)) {
76 p
->MaybeRejectWithTypeError("audio and/or video is required");
79 /* If the relevant settings object's responsible document is NOT fully
80 * active, return a promise rejected with a DOMException object whose name
81 * attribute has the value "InvalidStateError". */
82 if (!owner
->IsFullyActive()) {
83 p
->MaybeRejectWithInvalidStateError("The document is not fully active.");
86 const OwningBooleanOrMediaTrackConstraints
& video
= aConstraints
.mVideo
;
87 if (aCallerType
!= CallerType::System
&& video
.IsMediaTrackConstraints()) {
88 const Optional
<nsString
>& mediaSource
=
89 video
.GetAsMediaTrackConstraints().mMediaSource
;
90 if (mediaSource
.WasPassed() &&
91 !mediaSource
.Value().EqualsLiteral("camera")) {
92 WindowContext
* wc
= owner
->GetWindowContext();
93 if (!wc
|| !wc
->HasValidTransientUserGestureActivation()) {
94 p
->MaybeRejectWithInvalidStateError(
95 "Display capture requires transient activation "
96 "from a user gesture.");
101 RefPtr
<MediaDevices
> self(this);
102 GetUserMedia(owner
, aConstraints
, aCallerType
)
104 GetCurrentSerialEventTarget(), __func__
,
105 [this, self
, p
](RefPtr
<DOMMediaStream
>&& aStream
) {
106 if (!GetWindowIfCurrent()) {
107 return; // Leave Promise pending after navigation by design.
109 p
->MaybeResolve(std::move(aStream
));
111 [this, self
, p
](const RefPtr
<MediaMgrError
>& error
) {
112 nsPIDOMWindowInner
* window
= GetWindowIfCurrent();
114 return; // Leave Promise pending after navigation by design.
121 RefPtr
<MediaDevices::StreamPromise
> MediaDevices::GetUserMedia(
122 nsPIDOMWindowInner
* aWindow
, const MediaStreamConstraints
& aConstraints
,
123 CallerType aCallerType
) {
124 MOZ_ASSERT(NS_IsMainThread());
125 bool haveFake
= aConstraints
.mFake
.WasPassed() && aConstraints
.mFake
.Value();
126 const OwningBooleanOrMediaTrackConstraints
& video
= aConstraints
.mVideo
;
127 const OwningBooleanOrMediaTrackConstraints
& audio
= aConstraints
.mAudio
;
131 ? audio
.GetAsBoolean()
132 : !audio
.GetAsMediaTrackConstraints().mMediaSource
.WasPassed());
136 ? video
.GetAsBoolean()
137 : !video
.GetAsMediaTrackConstraints().mMediaSource
.WasPassed());
139 RefPtr
<MediaDevices
> self(this);
140 return MediaManager::Get()
141 ->GetUserMedia(aWindow
, aConstraints
, aCallerType
)
143 GetCurrentSerialEventTarget(), __func__
,
144 [this, self
, isMicrophone
,
145 isCamera
](RefPtr
<DOMMediaStream
>&& aStream
) {
147 mCanExposeMicrophoneInfo
= true;
150 mCanExposeCameraInfo
= true;
152 return StreamPromise::CreateAndResolve(std::move(aStream
),
155 [](RefPtr
<MediaMgrError
>&& aError
) {
156 return StreamPromise::CreateAndReject(std::move(aError
), __func__
);
160 already_AddRefed
<Promise
> MediaDevices::EnumerateDevices(ErrorResult
& aRv
) {
161 MOZ_ASSERT(NS_IsMainThread());
162 nsCOMPtr
<nsIGlobalObject
> global
= xpc::NativeGlobal(GetWrapper());
163 nsCOMPtr
<nsPIDOMWindowInner
> owner
= do_QueryInterface(global
);
164 if (Document
* doc
= owner
->GetExtantDoc()) {
165 if (!owner
->IsSecureContext()) {
166 doc
->SetUseCounter(eUseCounter_custom_EnumerateDevicesInsec
);
168 Document
* topDoc
= doc
->GetTopLevelContentDocumentIfSameProcess();
169 IgnoredErrorResult ignored
;
170 if (topDoc
&& !topDoc
->HasFocus(ignored
)) {
171 doc
->SetUseCounter(eUseCounter_custom_EnumerateDevicesUnfocused
);
174 RefPtr
<Promise
> p
= Promise::Create(global
, aRv
);
175 if (NS_WARN_IF(aRv
.Failed())) {
178 mPendingEnumerateDevicesPromises
.AppendElement(p
);
179 MaybeResumeDeviceExposure();
183 void MediaDevices::MaybeResumeDeviceExposure() {
184 if (mPendingEnumerateDevicesPromises
.IsEmpty() &&
185 !mHaveUnprocessedDeviceListChange
) {
188 nsPIDOMWindowInner
* window
= GetOwner();
189 if (!window
|| !window
->IsFullyActive()) {
192 if (!StaticPrefs::media_devices_unfocused_enabled()) {
193 // Device list changes are not exposed to unfocused contexts because the
194 // timing information would allow fingerprinting for content to identify
195 // concurrent browsing, even when pages are in different containers.
196 BrowsingContext
* bc
= window
->GetBrowsingContext();
197 if (!bc
->IsActive() || // background tab or browser window fully obscured
198 !bc
->GetIsActiveBrowserWindow()) { // browser window without focus
202 MediaManager::Get()->GetPhysicalDevices()->Then(
203 GetCurrentSerialEventTarget(), __func__
,
204 [self
= RefPtr(this), this,
205 haveDeviceListChange
= mHaveUnprocessedDeviceListChange
,
206 enumerateDevicesPromises
= std::move(mPendingEnumerateDevicesPromises
)](
207 RefPtr
<const MediaDeviceSetRefCnt
> aAllDevices
) mutable {
208 RefPtr
<MediaDeviceSetRefCnt
> exposedDevices
=
209 FilterExposedDevices(*aAllDevices
);
210 if (haveDeviceListChange
) {
211 if (ShouldQueueDeviceChange(*exposedDevices
)) {
212 NS_DispatchToCurrentThread(NS_NewRunnableFunction(
213 "devicechange", [self
= RefPtr(this), this] {
214 DispatchTrustedEvent(u
"devicechange"_ns
);
217 mLastPhysicalDevices
= std::move(aAllDevices
);
219 if (!enumerateDevicesPromises
.IsEmpty()) {
220 ResumeEnumerateDevices(std::move(enumerateDevicesPromises
),
221 std::move(exposedDevices
));
224 [](RefPtr
<MediaMgrError
>&&) {
225 MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject");
227 mHaveUnprocessedDeviceListChange
= false;
230 RefPtr
<MediaDeviceSetRefCnt
> MediaDevices::FilterExposedDevices(
231 const MediaDeviceSet
& aDevices
) const {
232 nsPIDOMWindowInner
* window
= GetOwner();
233 RefPtr exposed
= new MediaDeviceSetRefCnt();
235 return exposed
; // Promises will be left pending
237 Document
* doc
= window
->GetExtantDoc();
241 // Only expose devices which are allowed to use:
242 // https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices
243 bool dropMics
= !FeaturePolicyUtils::IsFeatureAllowed(doc
, u
"microphone"_ns
);
244 bool dropCams
= !FeaturePolicyUtils::IsFeatureAllowed(doc
, u
"camera"_ns
);
246 !Preferences::GetBool("media.setsinkid.enabled") ||
247 !FeaturePolicyUtils::IsFeatureAllowed(doc
, u
"speaker-selection"_ns
);
249 if (doc
->ShouldResistFingerprinting(RFPTarget::MediaDevices
)) {
250 RefPtr fakeEngine
= new MediaEngineFake();
251 fakeEngine
->EnumerateDevices(MediaSourceEnum::Microphone
,
252 MediaSinkEnum::Other
, exposed
);
253 fakeEngine
->EnumerateDevices(MediaSourceEnum::Camera
, MediaSinkEnum::Other
,
255 dropMics
= dropCams
= true;
256 // Speakers are not handled specially with resistFingerprinting because
257 // they are exposed only when explicitly and individually allowed by the
260 bool legacy
= StaticPrefs::media_devices_enumerate_legacy_enabled();
261 bool outputIsDefault
= true; // First output is the default.
262 bool haveDefaultOutput
= false;
263 nsTHashSet
<nsString
> exposedMicrophoneGroupIds
;
264 for (const auto& device
: aDevices
) {
265 switch (device
->mKind
) {
266 case MediaDeviceKind::Audioinput
:
270 if (mCanExposeMicrophoneInfo
) {
271 exposedMicrophoneGroupIds
.Insert(device
->mRawGroupID
);
273 if (!mCanExposeMicrophoneInfo
&& !legacy
) {
277 case MediaDeviceKind::Videoinput
:
281 if (!mCanExposeCameraInfo
&& !legacy
) {
285 case MediaDeviceKind::Audiooutput
:
287 (!mExplicitlyGrantedAudioOutputRawIds
.Contains(device
->mRawID
) &&
288 // Assumes aDevices order has microphones before speakers.
289 !exposedMicrophoneGroupIds
.Contains(device
->mRawGroupID
))) {
290 outputIsDefault
= false;
293 if (!haveDefaultOutput
&& !outputIsDefault
) {
294 // Insert a virtual default device so that the first enumerated
295 // device is the default output.
296 if (mDefaultOutputLabel
.IsVoid()) {
297 mDefaultOutputLabel
.SetIsVoid(false);
298 AutoTArray
<nsCString
, 1> resourceIds
{"dom/media.ftl"_ns
};
299 RefPtr l10n
= Localization::Create(resourceIds
, /*sync*/ true);
300 nsAutoCString translation
;
301 IgnoredErrorResult rv
;
302 l10n
->FormatValueSync("default-audio-output-device-label"_ns
, {},
305 AppendUTF8toUTF16(translation
, mDefaultOutputLabel
);
308 RefPtr info
= new AudioDeviceInfo(
309 nullptr, mDefaultOutputLabel
, u
""_ns
, u
""_ns
,
310 CUBEB_DEVICE_TYPE_OUTPUT
, CUBEB_DEVICE_STATE_ENABLED
,
311 CUBEB_DEVICE_PREF_ALL
, CUBEB_DEVICE_FMT_ALL
,
312 CUBEB_DEVICE_FMT_S16NE
, 2, 44100, 44100, 44100, 128, 128);
313 exposed
->AppendElement(
314 new MediaDevice(new MediaEngineFake(), info
, u
""_ns
));
316 haveDefaultOutput
= true;
318 case MediaDeviceKind::EndGuard_
:
320 // Avoid `default:` so that `-Wswitch` catches missing
321 // enumerators at compile time.
323 exposed
->AppendElement(device
);
328 bool MediaDevices::CanExposeInfo(MediaDeviceKind aKind
) const {
330 case MediaDeviceKind::Audioinput
:
331 return mCanExposeMicrophoneInfo
;
332 case MediaDeviceKind::Videoinput
:
333 return mCanExposeCameraInfo
;
334 case MediaDeviceKind::Audiooutput
:
335 // Assumes caller has used FilterExposedDevices()
337 case MediaDeviceKind::EndGuard_
:
339 // Avoid `default:` so that `-Wswitch` catches missing enumerators at
342 MOZ_ASSERT_UNREACHABLE("unexpected MediaDeviceKind");
346 bool MediaDevices::ShouldQueueDeviceChange(
347 const MediaDeviceSet
& aExposedDevices
) const {
348 if (!mLastPhysicalDevices
) { // SetupDeviceChangeListener not complete
351 RefPtr
<MediaDeviceSetRefCnt
> lastExposedDevices
=
352 FilterExposedDevices(*mLastPhysicalDevices
);
353 auto exposed
= aExposedDevices
.begin();
354 auto exposedEnd
= aExposedDevices
.end();
355 auto last
= lastExposedDevices
->begin();
356 auto lastEnd
= lastExposedDevices
->end();
357 // Lists from FilterExposedDevices may have multiple devices of the same
358 // kind even when only a single anonymous device of that kind should be
359 // exposed by enumerateDevices() (but multiple devices are currently exposed
360 // - bug 1528042). "devicechange" events are not queued when the number
361 // of such devices changes but remains non-zero.
362 while (exposed
< exposedEnd
&& last
< lastEnd
) {
363 // First determine whether there is at least one device of the same kind
364 // in both `aExposedDevices` and `lastExposedDevices`.
365 // A change between zero and non-zero numbers of microphone or camera
366 // devices triggers a devicechange event even if that kind of device is
368 MediaDeviceKind kind
= (*exposed
)->mKind
;
369 if (kind
!= (*last
)->mKind
) {
372 // `exposed` and `last` have matching kind.
373 if (CanExposeInfo(kind
)) {
374 // Queue "devicechange" if there has been any change in devices of this
375 // exposed kind. ID and kind uniquely identify a device.
376 if ((*exposed
)->mRawID
!= (*last
)->mRawID
) {
383 // `aExposedDevices` and `lastExposedDevices` both have non-zero numbers
384 // of devices of this unexposed kind.
385 // Skip remaining devices of this kind because all devices of this kind
386 // should be exposed as a single anonymous device.
389 } while (exposed
!= exposedEnd
&& (*exposed
)->mKind
== kind
);
392 } while (last
!= lastEnd
&& (*last
)->mKind
== kind
);
394 // Queue "devicechange" if the number of exposed devices differs.
395 return exposed
< exposedEnd
|| last
< lastEnd
;
398 void MediaDevices::ResumeEnumerateDevices(
399 nsTArray
<RefPtr
<Promise
>>&& aPromises
,
400 RefPtr
<const MediaDeviceSetRefCnt
> aExposedDevices
) const {
401 nsCOMPtr
<nsPIDOMWindowInner
> window
= GetOwner();
403 return; // Leave Promise pending after navigation by design.
406 ->AnonymizeDevices(window
, std::move(aExposedDevices
))
407 ->Then(GetCurrentSerialEventTarget(), __func__
,
408 [self
= RefPtr(this), this, promises
= std::move(aPromises
)](
409 const LocalDeviceSetPromise::ResolveOrRejectValue
&
411 nsPIDOMWindowInner
* window
= GetWindowIfCurrent();
413 return; // Leave Promises pending after navigation by design.
415 for (const RefPtr
<Promise
>& promise
: promises
) {
416 if (aLocalDevices
.IsReject()) {
417 aLocalDevices
.RejectValue()->Reject(promise
);
419 ResolveEnumerateDevicesPromise(
420 promise
, *aLocalDevices
.ResolveValue());
426 void MediaDevices::ResolveEnumerateDevicesPromise(
427 Promise
* aPromise
, const LocalMediaDeviceSet
& aDevices
) const {
428 nsCOMPtr
<nsPIDOMWindowInner
> window
= GetOwner();
429 auto windowId
= window
->WindowID();
430 nsTArray
<RefPtr
<MediaDeviceInfo
>> infos
;
431 bool legacy
= StaticPrefs::media_devices_enumerate_legacy_enabled();
432 bool capturePermitted
=
434 MediaManager::Get()->IsActivelyCapturingOrHasAPermission(windowId
);
436 for (const RefPtr
<LocalMediaDevice
>& device
: aDevices
) {
437 bool exposeInfo
= CanExposeInfo(device
->Kind()) || legacy
;
438 bool exposeLabel
= legacy
? capturePermitted
: exposeInfo
;
439 infos
.AppendElement(MakeRefPtr
<MediaDeviceInfo
>(
440 exposeInfo
? device
->mID
: u
""_ns
, device
->Kind(),
441 exposeLabel
? device
->mName
: u
""_ns
,
442 exposeInfo
? device
->mGroupID
: u
""_ns
));
444 aPromise
->MaybeResolve(std::move(infos
));
447 already_AddRefed
<Promise
> MediaDevices::GetDisplayMedia(
448 const DisplayMediaStreamConstraints
& aConstraints
, CallerType aCallerType
,
450 nsCOMPtr
<nsIGlobalObject
> global
= xpc::NativeGlobal(GetWrapper());
451 RefPtr
<Promise
> p
= Promise::Create(global
, aRv
);
452 if (NS_WARN_IF(aRv
.Failed())) {
455 nsCOMPtr
<nsPIDOMWindowInner
> owner
= do_QueryInterface(global
);
456 /* If the relevant global object of this does not have transient activation,
457 * return a promise rejected with a DOMException object whose name attribute
458 * has the value InvalidStateError. */
459 WindowContext
* wc
= owner
->GetWindowContext();
460 if (!wc
|| !wc
->HasValidTransientUserGestureActivation()) {
461 p
->MaybeRejectWithInvalidStateError(
462 "getDisplayMedia requires transient activation from a user gesture.");
465 /* If constraints.video is false, return a promise rejected with a newly
466 * created TypeError. */
467 if (!MediaManager::IsOn(aConstraints
.mVideo
)) {
468 p
->MaybeRejectWithTypeError("video is required");
471 MediaStreamConstraints c
;
472 auto& vc
= c
.mVideo
.SetAsMediaTrackConstraints();
474 if (aConstraints
.mVideo
.IsMediaTrackConstraints()) {
475 vc
= aConstraints
.mVideo
.GetAsMediaTrackConstraints();
476 /* If CS contains a member named advanced, return a promise rejected with
477 * a newly created TypeError. */
478 if (vc
.mAdvanced
.WasPassed()) {
479 p
->MaybeRejectWithTypeError("advanced not allowed");
482 auto getCLR
= [](const auto& aCon
) -> const ConstrainLongRange
& {
483 static ConstrainLongRange empty
;
484 return (aCon
.WasPassed() && !aCon
.Value().IsLong())
485 ? aCon
.Value().GetAsConstrainLongRange()
488 auto getCDR
= [](auto&& aCon
) -> const ConstrainDoubleRange
& {
489 static ConstrainDoubleRange empty
;
490 return (aCon
.WasPassed() && !aCon
.Value().IsDouble())
491 ? aCon
.Value().GetAsConstrainDoubleRange()
494 const auto& w
= getCLR(vc
.mWidth
);
495 const auto& h
= getCLR(vc
.mHeight
);
496 const auto& f
= getCDR(vc
.mFrameRate
);
497 /* If CS contains a member whose name specifies a constrainable property
498 * applicable to display surfaces, and whose value in turn is a dictionary
499 * containing a member named either min or exact, return a promise
500 * rejected with a newly created TypeError. */
501 if (w
.mMin
.WasPassed() || h
.mMin
.WasPassed() || f
.mMin
.WasPassed()) {
502 p
->MaybeRejectWithTypeError("min not allowed");
505 if (w
.mExact
.WasPassed() || h
.mExact
.WasPassed() || f
.mExact
.WasPassed()) {
506 p
->MaybeRejectWithTypeError("exact not allowed");
509 /* If CS contains a member whose name, failedConstraint specifies a
510 * constrainable property, constraint, applicable to display surfaces, and
511 * whose value in turn is a dictionary containing a member named max, and
512 * that member's value in turn is less than the constrainable property's
513 * floor value, then let failedConstraint be the name of the constraint,
514 * let message be either undefined or an informative human-readable
515 * message, and return a promise rejected with a new OverconstrainedError
516 * created by calling OverconstrainedError(failedConstraint, message). */
517 // We fail early without incurring a prompt, on known-to-fail constraint
518 // values that don't reveal anything about the user's system.
519 const char* badConstraint
= nullptr;
520 if (w
.mMax
.WasPassed() && w
.mMax
.Value() < 1) {
521 badConstraint
= "width";
523 if (h
.mMax
.WasPassed() && h
.mMax
.Value() < 1) {
524 badConstraint
= "height";
526 if (f
.mMax
.WasPassed() && f
.mMax
.Value() < 1) {
527 badConstraint
= "frameRate";
530 p
->MaybeReject(MakeRefPtr
<dom::MediaStreamError
>(
531 owner
, *MakeRefPtr
<MediaMgrError
>(
532 MediaMgrError::Name::OverconstrainedError
, "",
533 NS_ConvertASCIItoUTF16(badConstraint
))));
537 /* If the relevant settings object's responsible document is NOT fully
538 * active, return a promise rejected with a DOMException object whose name
539 * attribute has the value "InvalidStateError". */
540 if (!owner
->IsFullyActive()) {
541 p
->MaybeRejectWithInvalidStateError("The document is not fully active.");
544 // We ask for "screen" sharing.
546 // If this is a privileged call or permission is disabled, this gives us full
547 // screen sharing by default, which is useful for internal testing.
549 // If this is a non-priviliged call, GetUserMedia() will change it to "window"
551 vc
.mMediaSource
.Reset();
552 vc
.mMediaSource
.Construct().AssignASCII(
553 dom::MediaSourceEnumValues::GetString(MediaSourceEnum::Screen
));
555 RefPtr
<MediaDevices
> self(this);
557 ->GetUserMedia(owner
, c
, aCallerType
)
559 GetCurrentSerialEventTarget(), __func__
,
560 [this, self
, p
](RefPtr
<DOMMediaStream
>&& aStream
) {
561 if (!GetWindowIfCurrent()) {
562 return; // leave promise pending after navigation.
564 p
->MaybeResolve(std::move(aStream
));
566 [this, self
, p
](RefPtr
<MediaMgrError
>&& error
) {
567 nsPIDOMWindowInner
* window
= GetWindowIfCurrent();
569 return; // leave promise pending after navigation.
576 already_AddRefed
<Promise
> MediaDevices::SelectAudioOutput(
577 const AudioOutputOptions
& aOptions
, CallerType aCallerType
,
579 nsCOMPtr
<nsIGlobalObject
> global
= xpc::NativeGlobal(GetWrapper());
580 RefPtr
<Promise
> p
= Promise::Create(global
, aRv
);
581 if (NS_WARN_IF(aRv
.Failed())) {
584 /* (This includes the expected user activation update of
585 * https://github.com/w3c/mediacapture-output/issues/107)
586 * If the relevant global object of this does not have transient activation,
587 * return a promise rejected with a DOMException object whose name attribute
588 * has the value InvalidStateError. */
589 nsCOMPtr
<nsPIDOMWindowInner
> owner
= do_QueryInterface(global
);
590 WindowContext
* wc
= owner
->GetWindowContext();
591 if (!wc
|| !wc
->HasValidTransientUserGestureActivation()) {
592 p
->MaybeRejectWithInvalidStateError(
593 "selectAudioOutput requires transient user activation.");
596 RefPtr
<MediaDevices
> self(this);
598 ->SelectAudioOutput(owner
, aOptions
, aCallerType
)
600 GetCurrentSerialEventTarget(), __func__
,
601 [this, self
, p
](RefPtr
<LocalMediaDevice
> aDevice
) {
602 nsPIDOMWindowInner
* window
= GetWindowIfCurrent();
604 return; // Leave Promise pending after navigation by design.
606 MOZ_ASSERT(aDevice
->Kind() == dom::MediaDeviceKind::Audiooutput
);
607 mExplicitlyGrantedAudioOutputRawIds
.Insert(aDevice
->RawID());
609 MakeRefPtr
<MediaDeviceInfo
>(aDevice
->mID
, aDevice
->Kind(),
610 aDevice
->mName
, aDevice
->mGroupID
));
612 [this, self
, p
](const RefPtr
<MediaMgrError
>& error
) {
613 nsPIDOMWindowInner
* window
= GetWindowIfCurrent();
615 return; // Leave Promise pending after navigation by design.
622 static RefPtr
<AudioDeviceInfo
> CopyWithNullDeviceId(
623 AudioDeviceInfo
* aDeviceInfo
) {
624 MOZ_ASSERT(aDeviceInfo
->Preferred());
627 aDeviceInfo
->GetVendor(vendor
);
629 aDeviceInfo
->GetType(&type
);
631 aDeviceInfo
->GetState(&state
);
633 aDeviceInfo
->GetPreferred(&pref
);
634 uint16_t supportedFormat
;
635 aDeviceInfo
->GetSupportedFormat(&supportedFormat
);
636 uint16_t defaultFormat
;
637 aDeviceInfo
->GetDefaultFormat(&defaultFormat
);
638 uint32_t maxChannels
;
639 aDeviceInfo
->GetMaxChannels(&maxChannels
);
640 uint32_t defaultRate
;
641 aDeviceInfo
->GetDefaultRate(&defaultRate
);
643 aDeviceInfo
->GetMaxRate(&maxRate
);
645 aDeviceInfo
->GetMinRate(&minRate
);
647 aDeviceInfo
->GetMaxLatency(&maxLatency
);
649 aDeviceInfo
->GetMinLatency(&minLatency
);
651 return MakeRefPtr
<AudioDeviceInfo
>(
652 nullptr, aDeviceInfo
->Name(), aDeviceInfo
->GroupID(), vendor
, type
, state
,
653 pref
, supportedFormat
, defaultFormat
, maxChannels
, defaultRate
, maxRate
,
654 minRate
, maxLatency
, minLatency
);
657 RefPtr
<MediaDevices::SinkInfoPromise
> MediaDevices::GetSinkDevice(
658 const nsString
& aDeviceId
) {
659 MOZ_ASSERT(NS_IsMainThread());
660 return MediaManager::Get()
661 ->GetPhysicalDevices()
663 GetCurrentSerialEventTarget(), __func__
,
664 [self
= RefPtr(this), this,
665 aDeviceId
](RefPtr
<const MediaDeviceSetRefCnt
> aRawDevices
) {
666 nsCOMPtr
<nsPIDOMWindowInner
> window
= GetOwner();
668 return LocalDeviceSetPromise::CreateAndReject(
669 new MediaMgrError(MediaMgrError::Name::AbortError
), __func__
);
671 // Don't filter if matching the preferred device, because that may
673 RefPtr devices
= aDeviceId
.IsEmpty()
674 ? std::move(aRawDevices
)
675 : FilterExposedDevices(*aRawDevices
);
676 return MediaManager::Get()->AnonymizeDevices(window
,
679 [](RefPtr
<MediaMgrError
>&& reason
) {
680 MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject");
681 return RefPtr
<LocalDeviceSetPromise
>();
684 GetCurrentSerialEventTarget(), __func__
,
685 [aDeviceId
](RefPtr
<LocalMediaDeviceSetRefCnt
> aDevices
) {
686 RefPtr
<AudioDeviceInfo
> outputInfo
;
687 // Check for a matching device.
688 for (const RefPtr
<LocalMediaDevice
>& device
: *aDevices
) {
689 if (device
->Kind() != dom::MediaDeviceKind::Audiooutput
) {
692 if (aDeviceId
.IsEmpty()) {
693 MOZ_ASSERT(device
->GetAudioDeviceInfo()->Preferred(),
694 "First Audiooutput should be preferred");
695 return SinkInfoPromise::CreateAndResolve(
696 CopyWithNullDeviceId(device
->GetAudioDeviceInfo()),
698 } else if (aDeviceId
.Equals(device
->mID
)) {
699 return SinkInfoPromise::CreateAndResolve(
700 device
->GetAudioDeviceInfo(), __func__
);
703 /* If sinkId is not the empty string and does not match any audio
704 * output device identified by the result that would be provided
705 * by enumerateDevices(), reject p with a new DOMException whose
706 * name is NotFoundError and abort these substeps. */
707 return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE
,
711 [](RefPtr
<MediaMgrError
>&& aError
) {
712 return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE
,
717 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(MediaDevices
,
718 DOMEventTargetHelper
)
719 NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaDevices
, DOMEventTargetHelper
,
720 mPendingEnumerateDevicesPromises
)
722 void MediaDevices::OnDeviceChange() {
723 MOZ_ASSERT(NS_IsMainThread());
724 if (NS_FAILED(CheckCurrentGlobalCorrectness())) {
725 // This is a ghost window, don't do anything.
729 // Do not fire event to content script when
730 // privacy.resistFingerprinting is true.
732 if (nsContentUtils::ShouldResistFingerprinting(
733 "Guarding the more expensive RFP check with a simple one",
734 RFPTarget::MediaDevices
)) {
735 nsCOMPtr
<nsPIDOMWindowInner
> window
= GetOwner();
736 auto* wrapper
= GetWrapper();
737 if (!window
&& wrapper
) {
738 nsCOMPtr
<nsIGlobalObject
> global
= xpc::NativeGlobal(wrapper
);
739 window
= do_QueryInterface(global
);
745 if (nsGlobalWindowInner::Cast(window
)->ShouldResistFingerprinting(
746 RFPTarget::MediaDevices
)) {
751 mHaveUnprocessedDeviceListChange
= true;
752 MaybeResumeDeviceExposure();
755 mozilla::dom::EventHandlerNonNull
* MediaDevices::GetOndevicechange() {
756 return GetEventHandler(nsGkAtoms::ondevicechange
);
759 void MediaDevices::SetupDeviceChangeListener() {
760 if (mIsDeviceChangeListenerSetUp
) {
764 nsPIDOMWindowInner
* window
= GetOwner();
769 nsISerialEventTarget
* mainThread
=
770 window
->EventTargetFor(TaskCategory::Other
);
775 mDeviceChangeListener
= MediaManager::Get()->DeviceListChangeEvent().Connect(
776 mainThread
, this, &MediaDevices::OnDeviceChange
);
777 mIsDeviceChangeListenerSetUp
= true;
779 MediaManager::Get()->GetPhysicalDevices()->Then(
780 GetCurrentSerialEventTarget(), __func__
,
781 [self
= RefPtr(this), this](RefPtr
<const MediaDeviceSetRefCnt
> aDevices
) {
782 mLastPhysicalDevices
= std::move(aDevices
);
784 [](RefPtr
<MediaMgrError
>&& reason
) {
785 MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject");
789 void MediaDevices::SetOndevicechange(
790 mozilla::dom::EventHandlerNonNull
* aCallback
) {
791 SetEventHandler(nsGkAtoms::ondevicechange
, aCallback
);
794 void MediaDevices::EventListenerAdded(nsAtom
* aType
) {
795 DOMEventTargetHelper::EventListenerAdded(aType
);
796 SetupDeviceChangeListener();
799 JSObject
* MediaDevices::WrapObject(JSContext
* aCx
,
800 JS::Handle
<JSObject
*> aGivenProto
) {
801 return MediaDevices_Binding::Wrap(aCx
, this, aGivenProto
);
804 } // namespace mozilla::dom