Bug 1850460 - Removed file build/build-clang/revert-llvmorg-18-init-3787-gb6a1473f97d...
[gecko.git] / dom / media / MediaDevices.cpp
blob5849fb2759bc5e41a37b13ee233d7ffdf82d5ceb
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"
24 #include "nsINamed.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,
48 ErrorResult& aRv) {
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())) {
70 return nullptr;
72 /* If requestedMediaTypes is the empty set, return a promise rejected with a
73 * TypeError. */
74 if (!MediaManager::IsOn(aConstraints.mVideo) &&
75 !MediaManager::IsOn(aConstraints.mAudio)) {
76 p->MaybeRejectWithTypeError("audio and/or video is required");
77 return p.forget();
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.");
84 return p.forget();
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.");
97 return p.forget();
101 RefPtr<MediaDevices> self(this);
102 GetUserMedia(owner, aConstraints, aCallerType)
103 ->Then(
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();
113 if (!window) {
114 return; // Leave Promise pending after navigation by design.
116 error->Reject(p);
118 return p.forget();
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;
128 bool isMicrophone =
129 !haveFake &&
130 (audio.IsBoolean()
131 ? audio.GetAsBoolean()
132 : !audio.GetAsMediaTrackConstraints().mMediaSource.WasPassed());
133 bool isCamera =
134 !haveFake &&
135 (video.IsBoolean()
136 ? video.GetAsBoolean()
137 : !video.GetAsMediaTrackConstraints().mMediaSource.WasPassed());
139 RefPtr<MediaDevices> self(this);
140 return MediaManager::Get()
141 ->GetUserMedia(aWindow, aConstraints, aCallerType)
142 ->Then(
143 GetCurrentSerialEventTarget(), __func__,
144 [this, self, isMicrophone,
145 isCamera](RefPtr<DOMMediaStream>&& aStream) {
146 if (isMicrophone) {
147 mCanExposeMicrophoneInfo = true;
149 if (isCamera) {
150 mCanExposeCameraInfo = true;
152 return StreamPromise::CreateAndResolve(std::move(aStream),
153 __func__);
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())) {
176 return nullptr;
178 mPendingEnumerateDevicesPromises.AppendElement(p);
179 MaybeResumeDeviceExposure();
180 return p.forget();
183 void MediaDevices::MaybeResumeDeviceExposure() {
184 if (mPendingEnumerateDevicesPromises.IsEmpty() &&
185 !mHaveUnprocessedDeviceListChange) {
186 return;
188 nsPIDOMWindowInner* window = GetOwner();
189 if (!window || !window->IsFullyActive()) {
190 return;
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
199 return;
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);
215 }));
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();
234 if (!window) {
235 return exposed; // Promises will be left pending
237 Document* doc = window->GetExtantDoc();
238 if (!doc) {
239 return exposed;
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);
245 bool dropSpeakers =
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,
254 exposed);
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
258 // user.
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:
267 if (dropMics) {
268 continue;
270 if (mCanExposeMicrophoneInfo) {
271 exposedMicrophoneGroupIds.Insert(device->mRawGroupID);
273 if (!mCanExposeMicrophoneInfo && !legacy) {
274 dropMics = true;
276 break;
277 case MediaDeviceKind::Videoinput:
278 if (dropCams) {
279 continue;
281 if (!mCanExposeCameraInfo && !legacy) {
282 dropCams = true;
284 break;
285 case MediaDeviceKind::Audiooutput:
286 if (dropSpeakers ||
287 (!mExplicitlyGrantedAudioOutputRawIds.Contains(device->mRawID) &&
288 // Assumes aDevices order has microphones before speakers.
289 !exposedMicrophoneGroupIds.Contains(device->mRawGroupID))) {
290 outputIsDefault = false;
291 continue;
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, {},
303 translation, rv);
304 if (!rv.Failed()) {
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;
317 break;
318 case MediaDeviceKind::EndGuard_:
319 continue;
320 // Avoid `default:` so that `-Wswitch` catches missing
321 // enumerators at compile time.
323 exposed->AppendElement(device);
325 return exposed;
328 bool MediaDevices::CanExposeInfo(MediaDeviceKind aKind) const {
329 switch (aKind) {
330 case MediaDeviceKind::Audioinput:
331 return mCanExposeMicrophoneInfo;
332 case MediaDeviceKind::Videoinput:
333 return mCanExposeCameraInfo;
334 case MediaDeviceKind::Audiooutput:
335 // Assumes caller has used FilterExposedDevices()
336 return true;
337 case MediaDeviceKind::EndGuard_:
338 break;
339 // Avoid `default:` so that `-Wswitch` catches missing enumerators at
340 // compile time.
342 MOZ_ASSERT_UNREACHABLE("unexpected MediaDeviceKind");
343 return false;
346 bool MediaDevices::ShouldQueueDeviceChange(
347 const MediaDeviceSet& aExposedDevices) const {
348 if (!mLastPhysicalDevices) { // SetupDeviceChangeListener not complete
349 return false;
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
367 // not yet exposed.
368 MediaDeviceKind kind = (*exposed)->mKind;
369 if (kind != (*last)->mKind) {
370 return true;
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) {
377 return true;
379 ++exposed;
380 ++last;
381 continue;
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.
387 do {
388 ++exposed;
389 } while (exposed != exposedEnd && (*exposed)->mKind == kind);
390 do {
391 ++last;
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();
402 if (!window) {
403 return; // Leave Promise pending after navigation by design.
405 MediaManager::Get()
406 ->AnonymizeDevices(window, std::move(aExposedDevices))
407 ->Then(GetCurrentSerialEventTarget(), __func__,
408 [self = RefPtr(this), this, promises = std::move(aPromises)](
409 const LocalDeviceSetPromise::ResolveOrRejectValue&
410 aLocalDevices) {
411 nsPIDOMWindowInner* window = GetWindowIfCurrent();
412 if (!window) {
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);
418 } else {
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 =
433 legacy &&
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,
449 ErrorResult& aRv) {
450 nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper());
451 RefPtr<Promise> p = Promise::Create(global, aRv);
452 if (NS_WARN_IF(aRv.Failed())) {
453 return nullptr;
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.");
463 return p.forget();
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");
469 return p.forget();
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");
480 return p.forget();
482 auto getCLR = [](const auto& aCon) -> const ConstrainLongRange& {
483 static ConstrainLongRange empty;
484 return (aCon.WasPassed() && !aCon.Value().IsLong())
485 ? aCon.Value().GetAsConstrainLongRange()
486 : empty;
488 auto getCDR = [](auto&& aCon) -> const ConstrainDoubleRange& {
489 static ConstrainDoubleRange empty;
490 return (aCon.WasPassed() && !aCon.Value().IsDouble())
491 ? aCon.Value().GetAsConstrainDoubleRange()
492 : empty;
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");
503 return p.forget();
505 if (w.mExact.WasPassed() || h.mExact.WasPassed() || f.mExact.WasPassed()) {
506 p->MaybeRejectWithTypeError("exact not allowed");
507 return p.forget();
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";
529 if (badConstraint) {
530 p->MaybeReject(MakeRefPtr<dom::MediaStreamError>(
531 owner, *MakeRefPtr<MediaMgrError>(
532 MediaMgrError::Name::OverconstrainedError, "",
533 NS_ConvertASCIItoUTF16(badConstraint))));
534 return p.forget();
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.");
542 return p.forget();
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"
550 // for us.
551 vc.mMediaSource.Reset();
552 vc.mMediaSource.Construct().AssignASCII(
553 dom::MediaSourceEnumValues::GetString(MediaSourceEnum::Screen));
555 RefPtr<MediaDevices> self(this);
556 MediaManager::Get()
557 ->GetUserMedia(owner, c, aCallerType)
558 ->Then(
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();
568 if (!window) {
569 return; // leave promise pending after navigation.
571 error->Reject(p);
573 return p.forget();
576 already_AddRefed<Promise> MediaDevices::SelectAudioOutput(
577 const AudioOutputOptions& aOptions, CallerType aCallerType,
578 ErrorResult& aRv) {
579 nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper());
580 RefPtr<Promise> p = Promise::Create(global, aRv);
581 if (NS_WARN_IF(aRv.Failed())) {
582 return nullptr;
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.");
594 return p.forget();
596 RefPtr<MediaDevices> self(this);
597 MediaManager::Get()
598 ->SelectAudioOutput(owner, aOptions, aCallerType)
599 ->Then(
600 GetCurrentSerialEventTarget(), __func__,
601 [this, self, p](RefPtr<LocalMediaDevice> aDevice) {
602 nsPIDOMWindowInner* window = GetWindowIfCurrent();
603 if (!window) {
604 return; // Leave Promise pending after navigation by design.
606 MOZ_ASSERT(aDevice->Kind() == dom::MediaDeviceKind::Audiooutput);
607 mExplicitlyGrantedAudioOutputRawIds.Insert(aDevice->RawID());
608 p->MaybeResolve(
609 MakeRefPtr<MediaDeviceInfo>(aDevice->mID, aDevice->Kind(),
610 aDevice->mName, aDevice->mGroupID));
612 [this, self, p](const RefPtr<MediaMgrError>& error) {
613 nsPIDOMWindowInner* window = GetWindowIfCurrent();
614 if (!window) {
615 return; // Leave Promise pending after navigation by design.
617 error->Reject(p);
619 return p.forget();
622 static RefPtr<AudioDeviceInfo> CopyWithNullDeviceId(
623 AudioDeviceInfo* aDeviceInfo) {
624 MOZ_ASSERT(aDeviceInfo->Preferred());
626 nsString vendor;
627 aDeviceInfo->GetVendor(vendor);
628 uint16_t type;
629 aDeviceInfo->GetType(&type);
630 uint16_t state;
631 aDeviceInfo->GetState(&state);
632 uint16_t pref;
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);
642 uint32_t maxRate;
643 aDeviceInfo->GetMaxRate(&maxRate);
644 uint32_t minRate;
645 aDeviceInfo->GetMinRate(&minRate);
646 uint32_t maxLatency;
647 aDeviceInfo->GetMaxLatency(&maxLatency);
648 uint32_t minLatency;
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()
662 ->Then(
663 GetCurrentSerialEventTarget(), __func__,
664 [self = RefPtr(this), this,
665 aDeviceId](RefPtr<const MediaDeviceSetRefCnt> aRawDevices) {
666 nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
667 if (!window) {
668 return LocalDeviceSetPromise::CreateAndReject(
669 new MediaMgrError(MediaMgrError::Name::AbortError), __func__);
671 // Don't filter if matching the preferred device, because that may
672 // not be exposed.
673 RefPtr devices = aDeviceId.IsEmpty()
674 ? std::move(aRawDevices)
675 : FilterExposedDevices(*aRawDevices);
676 return MediaManager::Get()->AnonymizeDevices(window,
677 std::move(devices));
679 [](RefPtr<MediaMgrError>&& reason) {
680 MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject");
681 return RefPtr<LocalDeviceSetPromise>();
683 ->Then(
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) {
690 continue;
692 if (aDeviceId.IsEmpty()) {
693 MOZ_ASSERT(device->GetAudioDeviceInfo()->Preferred(),
694 "First Audiooutput should be preferred");
695 return SinkInfoPromise::CreateAndResolve(
696 CopyWithNullDeviceId(device->GetAudioDeviceInfo()),
697 __func__);
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,
708 __func__);
710 // aRejectMethod =
711 [](RefPtr<MediaMgrError>&& aError) {
712 return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE,
713 __func__);
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.
726 return;
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);
741 if (!window) {
742 return;
745 if (nsGlobalWindowInner::Cast(window)->ShouldResistFingerprinting(
746 RFPTarget::MediaDevices)) {
747 return;
751 mHaveUnprocessedDeviceListChange = true;
752 MaybeResumeDeviceExposure();
755 mozilla::dom::EventHandlerNonNull* MediaDevices::GetOndevicechange() {
756 return GetEventHandler(nsGkAtoms::ondevicechange);
759 void MediaDevices::SetupDeviceChangeListener() {
760 if (mIsDeviceChangeListenerSetUp) {
761 return;
764 nsPIDOMWindowInner* window = GetOwner();
765 if (!window) {
766 return;
769 nsISerialEventTarget* mainThread =
770 window->EventTargetFor(TaskCategory::Other);
771 if (!mainThread) {
772 return;
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