MediaDevices.cpp (34152B)
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/. */ 4 5 #include "mozilla/dom/MediaDevices.h" 6 7 #include "AudioDeviceInfo.h" 8 #include "MediaEngine.h" 9 #include "MediaEngineFake.h" 10 #include "MediaTrackConstraints.h" 11 #include "mozilla/MediaManager.h" 12 #include "mozilla/StaticPrefs_media.h" 13 #include "mozilla/dom/BrowsingContext.h" 14 #include "mozilla/dom/Document.h" 15 #include "mozilla/dom/FeaturePolicyUtils.h" 16 #include "mozilla/dom/MediaDeviceInfo.h" 17 #include "mozilla/dom/MediaDevicesBinding.h" 18 #include "mozilla/dom/MediaStreamBinding.h" 19 #include "mozilla/dom/MediaTrackSupportedConstraintsBinding.h" 20 #include "mozilla/dom/NavigatorBinding.h" 21 #include "mozilla/dom/Promise.h" 22 #include "mozilla/dom/WindowContext.h" 23 #include "mozilla/intl/Localization.h" 24 #include "nsContentUtils.h" 25 #include "nsGlobalWindowInner.h" 26 #include "nsINamed.h" 27 #include "nsIScriptGlobalObject.h" 28 #include "nsPIDOMWindow.h" 29 #include "nsQueryObject.h" 30 31 namespace mozilla::dom { 32 33 using ConstDeviceSetPromise = MediaManager::ConstDeviceSetPromise; 34 using LocalDeviceSetPromise = MediaManager::LocalDeviceSetPromise; 35 using LocalMediaDeviceSetRefCnt = MediaManager::LocalMediaDeviceSetRefCnt; 36 using MediaDeviceSetRefCnt = MediaManager::MediaDeviceSetRefCnt; 37 using mozilla::intl::Localization; 38 39 MediaDevices::MediaDevices(nsPIDOMWindowInner* aWindow) 40 : DOMEventTargetHelper(aWindow), mDefaultOutputLabel(VoidString()) {} 41 42 MediaDevices::~MediaDevices() { 43 MOZ_ASSERT(NS_IsMainThread()); 44 mDeviceChangeListener.DisconnectIfExists(); 45 } 46 47 // No code needed, unless controlled by prefs, as 48 // MediaTrackSupportedConstraints members default to true. 49 void MediaDevices::GetSupportedConstraints( 50 MediaTrackSupportedConstraints& aResult) { 51 if (Preferences::GetBool("media.navigator.video.resize_mode.enabled")) { 52 aResult.mResizeMode.Construct(true); 53 } 54 } 55 56 already_AddRefed<Promise> MediaDevices::GetUserMedia( 57 const MediaStreamConstraints& aConstraints, CallerType aCallerType, 58 ErrorResult& aRv) { 59 MOZ_ASSERT(NS_IsMainThread()); 60 // Get the relevant global for the promise from the wrapper cache because 61 // DOMEventTargetHelper::GetOwnerWindow() returns null if the document is 62 // unloaded. 63 // We know the wrapper exists because it is being used for |this| from JS. 64 // See https://github.com/heycam/webidl/issues/932 for why the relevant 65 // global is used instead of the current global. 66 nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper()); 67 // global is a window because MediaDevices is exposed only to Window. 68 nsCOMPtr<nsPIDOMWindowInner> owner = do_QueryInterface(global); 69 if (Document* doc = owner->GetExtantDoc()) { 70 if (!owner->IsSecureContext()) { 71 doc->SetUseCounter(eUseCounter_custom_GetUserMediaInsec); 72 } 73 Document* topDoc = doc->GetTopLevelContentDocumentIfSameProcess(); 74 IgnoredErrorResult ignored; 75 if (topDoc && !topDoc->HasFocus(ignored)) { 76 doc->SetUseCounter(eUseCounter_custom_GetUserMediaUnfocused); 77 } 78 } 79 RefPtr<Promise> p = Promise::Create(global, aRv); 80 if (NS_WARN_IF(aRv.Failed())) { 81 return nullptr; 82 } 83 /* If requestedMediaTypes is the empty set, return a promise rejected with a 84 * TypeError. */ 85 if (!MediaManager::IsOn(aConstraints.mVideo) && 86 !MediaManager::IsOn(aConstraints.mAudio)) { 87 p->MaybeRejectWithTypeError("audio and/or video is required"); 88 return p.forget(); 89 } 90 /* If the relevant settings object's responsible document is NOT fully 91 * active, return a promise rejected with a DOMException object whose name 92 * attribute has the value "InvalidStateError". */ 93 if (!owner->IsFullyActive()) { 94 p->MaybeRejectWithInvalidStateError("The document is not fully active."); 95 return p.forget(); 96 } 97 const OwningBooleanOrMediaTrackConstraints& video = aConstraints.mVideo; 98 if (aCallerType != CallerType::System && video.IsMediaTrackConstraints()) { 99 const Optional<nsString>& mediaSource = 100 video.GetAsMediaTrackConstraints().mMediaSource; 101 if (mediaSource.WasPassed() && 102 !mediaSource.Value().EqualsLiteral("camera")) { 103 WindowContext* wc = owner->GetWindowContext(); 104 if (!wc || !wc->HasValidTransientUserGestureActivation()) { 105 p->MaybeRejectWithInvalidStateError( 106 "Display capture requires transient activation " 107 "from a user gesture."); 108 return p.forget(); 109 } 110 } 111 } 112 RefPtr<MediaDevices> self(this); 113 GetUserMedia(owner, aConstraints, aCallerType) 114 ->Then( 115 GetCurrentSerialEventTarget(), __func__, 116 [this, self, p](RefPtr<DOMMediaStream>&& aStream) { 117 if (!GetWindowIfCurrent()) { 118 return; // Leave Promise pending after navigation by design. 119 } 120 p->MaybeResolve(std::move(aStream)); 121 }, 122 [this, self, p](const RefPtr<MediaMgrError>& error) { 123 nsPIDOMWindowInner* window = GetWindowIfCurrent(); 124 if (!window) { 125 return; // Leave Promise pending after navigation by design. 126 } 127 error->Reject(p); 128 }); 129 return p.forget(); 130 } 131 132 RefPtr<MediaDevices::StreamPromise> MediaDevices::GetUserMedia( 133 nsPIDOMWindowInner* aWindow, const MediaStreamConstraints& aConstraints, 134 CallerType aCallerType) { 135 MOZ_ASSERT(NS_IsMainThread()); 136 bool haveFake = aConstraints.mFake.WasPassed() && aConstraints.mFake.Value(); 137 const OwningBooleanOrMediaTrackConstraints& video = aConstraints.mVideo; 138 const OwningBooleanOrMediaTrackConstraints& audio = aConstraints.mAudio; 139 bool isMicrophone = 140 !haveFake && 141 (audio.IsBoolean() 142 ? audio.GetAsBoolean() 143 : !audio.GetAsMediaTrackConstraints().mMediaSource.WasPassed()); 144 bool isCamera = 145 !haveFake && 146 (video.IsBoolean() 147 ? video.GetAsBoolean() 148 : !video.GetAsMediaTrackConstraints().mMediaSource.WasPassed()); 149 150 RefPtr<MediaDevices> self(this); 151 return MediaManager::Get() 152 ->GetUserMedia(aWindow, aConstraints, aCallerType) 153 ->Then( 154 GetCurrentSerialEventTarget(), __func__, 155 [this, self, isMicrophone, 156 isCamera](RefPtr<DOMMediaStream>&& aStream) { 157 if (isMicrophone) { 158 mCanExposeMicrophoneInfo = true; 159 } 160 if (isCamera) { 161 mCanExposeCameraInfo = true; 162 } 163 return StreamPromise::CreateAndResolve(std::move(aStream), 164 __func__); 165 }, 166 [](RefPtr<MediaMgrError>&& aError) { 167 return StreamPromise::CreateAndReject(std::move(aError), __func__); 168 }); 169 } 170 171 already_AddRefed<Promise> MediaDevices::EnumerateDevices(ErrorResult& aRv) { 172 MOZ_ASSERT(NS_IsMainThread()); 173 nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper()); 174 nsCOMPtr<nsPIDOMWindowInner> owner = do_QueryInterface(global); 175 if (Document* doc = owner->GetExtantDoc()) { 176 if (!owner->IsSecureContext()) { 177 doc->SetUseCounter(eUseCounter_custom_EnumerateDevicesInsec); 178 } 179 Document* topDoc = doc->GetTopLevelContentDocumentIfSameProcess(); 180 IgnoredErrorResult ignored; 181 if (topDoc && !topDoc->HasFocus(ignored)) { 182 doc->SetUseCounter(eUseCounter_custom_EnumerateDevicesUnfocused); 183 } 184 } 185 RefPtr<Promise> p = Promise::Create(global, aRv); 186 if (NS_WARN_IF(aRv.Failed())) { 187 return nullptr; 188 } 189 mPendingEnumerateDevicesPromises.AppendElement(p); 190 MaybeResumeDeviceExposure(); 191 return p.forget(); 192 } 193 194 void MediaDevices::MaybeResumeDeviceExposure() { 195 if (mPendingEnumerateDevicesPromises.IsEmpty() && 196 !mHaveUnprocessedDeviceListChange) { 197 return; 198 } 199 nsPIDOMWindowInner* window = GetOwnerWindow(); 200 if (!window || !window->IsFullyActive()) { 201 return; 202 } 203 if (!StaticPrefs::media_devices_unfocused_enabled()) { 204 // Device list changes are not exposed to unfocused contexts because the 205 // timing information would allow fingerprinting for content to identify 206 // concurrent browsing, even when pages are in different containers. 207 BrowsingContext* bc = window->GetBrowsingContext(); 208 if (!bc->IsActive() || // background tab or browser window fully obscured 209 !bc->GetIsActiveBrowserWindow()) { // browser window without focus 210 return; 211 } 212 } 213 bool shouldResistFingerprinting = 214 window->AsGlobal()->ShouldResistFingerprinting(RFPTarget::MediaDevices); 215 MediaManager::Get()->GetPhysicalDevices()->Then( 216 GetCurrentSerialEventTarget(), __func__, 217 [self = RefPtr(this), this, 218 haveDeviceListChange = mHaveUnprocessedDeviceListChange, 219 enumerateDevicesPromises = std::move(mPendingEnumerateDevicesPromises), 220 shouldResistFingerprinting]( 221 RefPtr<const MediaDeviceSetRefCnt> aAllDevices) mutable { 222 RefPtr<MediaDeviceSetRefCnt> exposedDevices = 223 FilterExposedDevices(*aAllDevices); 224 if (haveDeviceListChange && !shouldResistFingerprinting) { 225 if (ShouldQueueDeviceChange(*exposedDevices)) { 226 NS_DispatchToCurrentThread(NS_NewRunnableFunction( 227 "devicechange", [self = RefPtr(this), this] { 228 DispatchTrustedEvent(u"devicechange"_ns); 229 })); 230 } 231 mLastPhysicalDevices = std::move(aAllDevices); 232 } 233 if (!enumerateDevicesPromises.IsEmpty()) { 234 ResumeEnumerateDevices(std::move(enumerateDevicesPromises), 235 std::move(exposedDevices)); 236 } 237 }, 238 [](RefPtr<MediaMgrError>&&) { 239 MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject"); 240 }); 241 mHaveUnprocessedDeviceListChange = false; 242 } 243 244 static bool IsLegacyMode(nsPIDOMWindowInner* window) { 245 if (StaticPrefs::media_devices_enumerate_legacy_enabled()) { 246 return true; 247 } 248 if (window->GetDocumentURI()) { 249 nsAutoCString host; 250 window->GetDocumentURI()->GetAsciiHost(host); 251 if (media::HostnameInPref("media.devices.enumerate.legacy.allowlist", 252 host)) { 253 return true; 254 } 255 } 256 return false; 257 } 258 259 RefPtr<MediaDeviceSetRefCnt> MediaDevices::FilterExposedDevices( 260 const MediaDeviceSet& aDevices) const { 261 nsPIDOMWindowInner* window = GetOwnerWindow(); 262 RefPtr exposed = new MediaDeviceSetRefCnt(); 263 if (!window) { 264 return exposed; // Promises will be left pending 265 } 266 Document* doc = window->GetExtantDoc(); 267 if (!doc) { 268 return exposed; 269 } 270 // Only expose devices which are allowed to use: 271 // https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices 272 bool dropMics = !FeaturePolicyUtils::IsFeatureAllowed(doc, u"microphone"_ns); 273 bool dropCams = !FeaturePolicyUtils::IsFeatureAllowed(doc, u"camera"_ns); 274 bool dropSpeakers = 275 !Preferences::GetBool("media.setsinkid.enabled") || 276 !FeaturePolicyUtils::IsFeatureAllowed(doc, u"speaker-selection"_ns); 277 bool shouldResistFingerprinting = 278 window->AsGlobal()->ShouldResistFingerprinting(RFPTarget::MediaDevices); 279 bool legacy = IsLegacyMode(window); 280 bool outputIsDefault = true; // First output is the default. 281 bool haveDefaultOutput = false; 282 nsTHashSet<nsString> exposedMicrophoneGroupIds; 283 for (const auto& device : aDevices) { 284 switch (device->mKind) { 285 case MediaDeviceKind::Audioinput: 286 if (dropMics) { 287 continue; 288 } 289 if (mCanExposeMicrophoneInfo) { 290 exposedMicrophoneGroupIds.Insert(device->mRawGroupID); 291 } 292 if (!mCanExposeMicrophoneInfo && !legacy) { 293 dropMics = true; 294 } 295 break; 296 case MediaDeviceKind::Videoinput: 297 if (dropCams) { 298 continue; 299 } 300 if (!mCanExposeCameraInfo && !legacy) { 301 dropCams = true; 302 } 303 break; 304 case MediaDeviceKind::Audiooutput: 305 if (dropSpeakers || 306 (!mExplicitlyGrantedAudioOutputRawIds.Contains(device->mRawID) && 307 (!mCanExposeMicrophoneInfo || 308 (shouldResistFingerprinting && 309 // Assumes aDevices order has microphones before speakers. 310 !exposedMicrophoneGroupIds.Contains(device->mRawGroupID))))) { 311 outputIsDefault = false; 312 continue; 313 } 314 if (!haveDefaultOutput && !outputIsDefault) { 315 // Insert a virtual default device so that the first enumerated 316 // device is the default output. 317 if (mDefaultOutputLabel.IsVoid()) { 318 mDefaultOutputLabel.SetIsVoid(false); 319 AutoTArray<nsCString, 1> resourceIds{"dom/media.ftl"_ns}; 320 RefPtr l10n = Localization::Create(resourceIds, /*sync*/ true); 321 nsAutoCString translation; 322 IgnoredErrorResult rv; 323 l10n->FormatValueSync("default-audio-output-device-label"_ns, {}, 324 translation, rv); 325 if (!rv.Failed()) { 326 AppendUTF8toUTF16(translation, mDefaultOutputLabel); 327 } 328 } 329 RefPtr info = new AudioDeviceInfo( 330 nullptr, mDefaultOutputLabel, u""_ns, u""_ns, 331 CUBEB_DEVICE_TYPE_OUTPUT, CUBEB_DEVICE_STATE_ENABLED, 332 CUBEB_DEVICE_PREF_ALL, CUBEB_DEVICE_FMT_ALL, 333 CUBEB_DEVICE_FMT_S16NE, 2, 44100, 44100, 44100, 128, 128); 334 exposed->AppendElement( 335 new MediaDevice(new MediaEngineFake(), info, u""_ns)); 336 } 337 haveDefaultOutput = true; 338 break; 339 // Avoid `default:` so that `-Wswitch` catches missing 340 // enumerators at compile time. 341 } 342 exposed->AppendElement(device); 343 } 344 345 if (doc->ShouldResistFingerprinting(RFPTarget::MediaDevices)) { 346 // We expose a single device of each kind. 347 // Legacy mode also achieves the same thing, except for speakers. 348 nsTHashSet<MediaDeviceKind> seenKinds; 349 350 for (uint32_t i = 0; i < exposed->Length(); i++) { 351 RefPtr<mozilla::MediaDevice> device = exposed->ElementAt(i); 352 if (seenKinds.Contains(device->mKind)) { 353 exposed->RemoveElementAt(i); 354 i--; 355 continue; 356 } 357 seenKinds.Insert(device->mKind); 358 } 359 360 // We haven't seen at least one of each kind of device. 361 // Audioinput, Videoinput, Audiooutput. 362 // Insert fake devices. 363 if (seenKinds.Count() != 3) { 364 RefPtr fakeEngine = new MediaEngineFake(); 365 RefPtr fakeDevices = new MediaDeviceSetRefCnt(); 366 // The order in which we insert the fake devices is important. 367 // Microphone is inserted first, then camera, then speaker. 368 // If we haven't seen a microphone, insert a fake one. 369 if (!seenKinds.Contains(MediaDeviceKind::Audioinput)) { 370 fakeEngine->EnumerateDevices(MediaSourceEnum::Microphone, 371 MediaSinkEnum::Other, fakeDevices); 372 exposed->InsertElementAt(0, fakeDevices->LastElement()); 373 } 374 // If we haven't seen a camera, insert a fake one. 375 if (!seenKinds.Contains(MediaDeviceKind::Videoinput)) { 376 fakeEngine->EnumerateDevices(MediaSourceEnum::Camera, 377 MediaSinkEnum::Other, fakeDevices); 378 exposed->InsertElementAt(1, fakeDevices->LastElement()); 379 } 380 // If we haven't seen a speaker, insert a fake one. 381 if (!seenKinds.Contains(MediaDeviceKind::Audiooutput) && 382 mCanExposeMicrophoneInfo) { 383 RefPtr info = new AudioDeviceInfo( 384 nullptr, u""_ns, u""_ns, u""_ns, CUBEB_DEVICE_TYPE_OUTPUT, 385 CUBEB_DEVICE_STATE_ENABLED, CUBEB_DEVICE_PREF_ALL, 386 CUBEB_DEVICE_FMT_ALL, CUBEB_DEVICE_FMT_S16NE, 2, 44100, 44100, 387 44100, 128, 128); 388 exposed->AppendElement( 389 new MediaDevice(new MediaEngineFake(), info, u""_ns)); 390 } 391 } 392 } 393 394 return exposed; 395 } 396 397 bool MediaDevices::CanExposeInfo(MediaDeviceKind aKind) const { 398 switch (aKind) { 399 case MediaDeviceKind::Audioinput: 400 return mCanExposeMicrophoneInfo; 401 case MediaDeviceKind::Videoinput: 402 return mCanExposeCameraInfo; 403 case MediaDeviceKind::Audiooutput: 404 // Assumes caller has used FilterExposedDevices() 405 return true; 406 // Avoid `default:` so that `-Wswitch` catches missing enumerators at 407 // compile time. 408 } 409 MOZ_ASSERT_UNREACHABLE("unexpected MediaDeviceKind"); 410 return false; 411 } 412 413 bool MediaDevices::ShouldQueueDeviceChange( 414 const MediaDeviceSet& aExposedDevices) const { 415 if (!mLastPhysicalDevices) { // SetupDeviceChangeListener not complete 416 return false; 417 } 418 RefPtr<MediaDeviceSetRefCnt> lastExposedDevices = 419 FilterExposedDevices(*mLastPhysicalDevices); 420 auto exposed = aExposedDevices.begin(); 421 auto exposedEnd = aExposedDevices.end(); 422 auto last = lastExposedDevices->begin(); 423 auto lastEnd = lastExposedDevices->end(); 424 // Lists from FilterExposedDevices may have multiple devices of the same 425 // kind even when only a single anonymous device of that kind should be 426 // exposed by enumerateDevices() (but multiple devices are currently exposed 427 // - bug 1528042). "devicechange" events are not queued when the number 428 // of such devices changes but remains non-zero. 429 while (exposed < exposedEnd && last < lastEnd) { 430 // First determine whether there is at least one device of the same kind 431 // in both `aExposedDevices` and `lastExposedDevices`. 432 // A change between zero and non-zero numbers of microphone or camera 433 // devices triggers a devicechange event even if that kind of device is 434 // not yet exposed. 435 MediaDeviceKind kind = (*exposed)->mKind; 436 if (kind != (*last)->mKind) { 437 return true; 438 } 439 // `exposed` and `last` have matching kind. 440 if (CanExposeInfo(kind)) { 441 // Queue "devicechange" if there has been any change in devices of this 442 // exposed kind. ID and kind uniquely identify a device. 443 if ((*exposed)->mRawID != (*last)->mRawID) { 444 return true; 445 } 446 ++exposed; 447 ++last; 448 continue; 449 } 450 // `aExposedDevices` and `lastExposedDevices` both have non-zero numbers 451 // of devices of this unexposed kind. 452 // Skip remaining devices of this kind because all devices of this kind 453 // should be exposed as a single anonymous device. 454 do { 455 ++exposed; 456 } while (exposed != exposedEnd && (*exposed)->mKind == kind); 457 do { 458 ++last; 459 } while (last != lastEnd && (*last)->mKind == kind); 460 } 461 // Queue "devicechange" if the number of exposed devices differs. 462 return exposed < exposedEnd || last < lastEnd; 463 } 464 465 void MediaDevices::ResumeEnumerateDevices( 466 nsTArray<RefPtr<Promise>>&& aPromises, 467 RefPtr<const MediaDeviceSetRefCnt> aExposedDevices) const { 468 nsCOMPtr<nsPIDOMWindowInner> window = GetOwnerWindow(); 469 if (!window) { 470 return; // Leave Promise pending after navigation by design. 471 } 472 MediaManager::Get() 473 ->AnonymizeDevices(window, std::move(aExposedDevices)) 474 ->Then(GetCurrentSerialEventTarget(), __func__, 475 [self = RefPtr(this), this, promises = std::move(aPromises)]( 476 const LocalDeviceSetPromise::ResolveOrRejectValue& 477 aLocalDevices) { 478 nsPIDOMWindowInner* window = GetWindowIfCurrent(); 479 if (!window) { 480 return; // Leave Promises pending after navigation by design. 481 } 482 for (const RefPtr<Promise>& promise : promises) { 483 if (aLocalDevices.IsReject()) { 484 aLocalDevices.RejectValue()->Reject(promise); 485 } else { 486 ResolveEnumerateDevicesPromise( 487 promise, *aLocalDevices.ResolveValue()); 488 } 489 } 490 }); 491 } 492 493 void MediaDevices::ResolveEnumerateDevicesPromise( 494 Promise* aPromise, const LocalMediaDeviceSet& aDevices) const { 495 nsCOMPtr<nsPIDOMWindowInner> window = GetOwnerWindow(); 496 auto windowId = window->WindowID(); 497 nsTArray<RefPtr<MediaDeviceInfo>> infos; 498 bool legacy = IsLegacyMode(window); 499 bool capturePermitted = 500 legacy && 501 MediaManager::Get()->IsActivelyCapturingOrHasAPermission(windowId); 502 503 for (const RefPtr<LocalMediaDevice>& device : aDevices) { 504 bool exposeInfo = CanExposeInfo(device->Kind()) || legacy; 505 bool exposeLabel = legacy ? capturePermitted : exposeInfo; 506 infos.AppendElement(MakeRefPtr<MediaDeviceInfo>( 507 exposeInfo ? device->mID : u""_ns, device->Kind(), 508 exposeLabel ? device->mName : u""_ns, 509 exposeInfo ? device->mGroupID : u""_ns)); 510 } 511 aPromise->MaybeResolve(std::move(infos)); 512 } 513 514 already_AddRefed<Promise> MediaDevices::GetDisplayMedia( 515 const DisplayMediaStreamConstraints& aConstraints, CallerType aCallerType, 516 ErrorResult& aRv) { 517 nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper()); 518 RefPtr<Promise> p = Promise::Create(global, aRv); 519 if (NS_WARN_IF(aRv.Failed())) { 520 return nullptr; 521 } 522 nsCOMPtr<nsPIDOMWindowInner> owner = do_QueryInterface(global); 523 /* If the relevant global object of this does not have transient activation, 524 * return a promise rejected with a DOMException object whose name attribute 525 * has the value InvalidStateError. */ 526 WindowContext* wc = owner->GetWindowContext(); 527 if (!wc || !wc->HasValidTransientUserGestureActivation()) { 528 p->MaybeRejectWithInvalidStateError( 529 "getDisplayMedia requires transient activation from a user gesture."); 530 return p.forget(); 531 } 532 /* If constraints.video is false, return a promise rejected with a newly 533 * created TypeError. */ 534 if (!MediaManager::IsOn(aConstraints.mVideo)) { 535 p->MaybeRejectWithTypeError("video is required"); 536 return p.forget(); 537 } 538 MediaStreamConstraints c; 539 auto& vc = c.mVideo.SetAsMediaTrackConstraints(); 540 541 if (aConstraints.mVideo.IsMediaTrackConstraints()) { 542 vc = aConstraints.mVideo.GetAsMediaTrackConstraints(); 543 /* If CS contains a member named advanced, return a promise rejected with 544 * a newly created TypeError. */ 545 if (vc.mAdvanced.WasPassed()) { 546 p->MaybeRejectWithTypeError("advanced not allowed"); 547 return p.forget(); 548 } 549 auto getCLR = [](const auto& aCon) -> const ConstrainLongRange& { 550 static ConstrainLongRange empty; 551 return (aCon.WasPassed() && !aCon.Value().IsLong()) 552 ? aCon.Value().GetAsConstrainLongRange() 553 : empty; 554 }; 555 auto getCDR = [](auto&& aCon) -> const ConstrainDoubleRange& { 556 static ConstrainDoubleRange empty; 557 return (aCon.WasPassed() && !aCon.Value().IsDouble()) 558 ? aCon.Value().GetAsConstrainDoubleRange() 559 : empty; 560 }; 561 const auto& w = getCLR(vc.mWidth); 562 const auto& h = getCLR(vc.mHeight); 563 const auto& f = getCDR(vc.mFrameRate); 564 /* If CS contains a member whose name specifies a constrainable property 565 * applicable to display surfaces, and whose value in turn is a dictionary 566 * containing a member named either min or exact, return a promise 567 * rejected with a newly created TypeError. */ 568 if (w.mMin.WasPassed() || h.mMin.WasPassed() || f.mMin.WasPassed()) { 569 p->MaybeRejectWithTypeError("min not allowed"); 570 return p.forget(); 571 } 572 if (w.mExact.WasPassed() || h.mExact.WasPassed() || f.mExact.WasPassed()) { 573 p->MaybeRejectWithTypeError("exact not allowed"); 574 return p.forget(); 575 } 576 /* If CS contains a member whose name, failedConstraint specifies a 577 * constrainable property, constraint, applicable to display surfaces, and 578 * whose value in turn is a dictionary containing a member named max, and 579 * that member's value in turn is less than the constrainable property's 580 * floor value, then let failedConstraint be the name of the constraint, 581 * let message be either undefined or an informative human-readable 582 * message, and return a promise rejected with a new OverconstrainedError 583 * created by calling OverconstrainedError(failedConstraint, message). */ 584 // We fail early without incurring a prompt, on known-to-fail constraint 585 // values that don't reveal anything about the user's system. 586 const char* badConstraint = nullptr; 587 if (w.mMax.WasPassed() && w.mMax.Value() < 1) { 588 badConstraint = "width"; 589 } 590 if (h.mMax.WasPassed() && h.mMax.Value() < 1) { 591 badConstraint = "height"; 592 } 593 if (f.mMax.WasPassed() && f.mMax.Value() < 1) { 594 badConstraint = "frameRate"; 595 } 596 if (badConstraint) { 597 p->MaybeReject(MakeRefPtr<dom::MediaStreamError>( 598 owner, *MakeRefPtr<MediaMgrError>( 599 MediaMgrError::Name::OverconstrainedError, "", 600 NS_ConvertASCIItoUTF16(badConstraint)))); 601 return p.forget(); 602 } 603 } 604 /* If the relevant settings object's responsible document is NOT fully 605 * active, return a promise rejected with a DOMException object whose name 606 * attribute has the value "InvalidStateError". */ 607 if (!owner->IsFullyActive()) { 608 p->MaybeRejectWithInvalidStateError("The document is not fully active."); 609 return p.forget(); 610 } 611 // We ask for "screen" sharing. 612 // 613 // If this is a privileged call or permission is disabled, this gives us full 614 // screen sharing by default, which is useful for internal testing. 615 // 616 // If this is a non-priviliged call, GetUserMedia() will change it to "window" 617 // for us. 618 vc.mMediaSource.Reset(); 619 vc.mMediaSource.Construct().AssignASCII( 620 dom::GetEnumString(MediaSourceEnum::Screen)); 621 622 RefPtr<MediaDevices> self(this); 623 MediaManager::Get() 624 ->GetUserMedia(owner, c, aCallerType) 625 ->Then( 626 GetCurrentSerialEventTarget(), __func__, 627 [this, self, p](RefPtr<DOMMediaStream>&& aStream) { 628 if (!GetWindowIfCurrent()) { 629 return; // leave promise pending after navigation. 630 } 631 p->MaybeResolve(std::move(aStream)); 632 }, 633 [this, self, p](RefPtr<MediaMgrError>&& error) { 634 nsPIDOMWindowInner* window = GetWindowIfCurrent(); 635 if (!window) { 636 return; // leave promise pending after navigation. 637 } 638 error->Reject(p); 639 }); 640 return p.forget(); 641 } 642 643 already_AddRefed<Promise> MediaDevices::SelectAudioOutput( 644 const AudioOutputOptions& aOptions, CallerType aCallerType, 645 ErrorResult& aRv) { 646 nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper()); 647 RefPtr<Promise> p = Promise::Create(global, aRv); 648 if (NS_WARN_IF(aRv.Failed())) { 649 return nullptr; 650 } 651 /* (This includes the expected user activation update of 652 * https://github.com/w3c/mediacapture-output/issues/107) 653 * If the relevant global object of this does not have transient activation, 654 * return a promise rejected with a DOMException object whose name attribute 655 * has the value InvalidStateError. */ 656 nsCOMPtr<nsPIDOMWindowInner> owner = do_QueryInterface(global); 657 WindowContext* wc = owner->GetWindowContext(); 658 if (!wc || !wc->HasValidTransientUserGestureActivation()) { 659 p->MaybeRejectWithInvalidStateError( 660 "selectAudioOutput requires transient user activation."); 661 return p.forget(); 662 } 663 RefPtr<MediaDevices> self(this); 664 MediaManager::Get() 665 ->SelectAudioOutput(owner, aOptions, aCallerType) 666 ->Then( 667 GetCurrentSerialEventTarget(), __func__, 668 [this, self, p](RefPtr<LocalMediaDevice> aDevice) { 669 nsPIDOMWindowInner* window = GetWindowIfCurrent(); 670 if (!window) { 671 return; // Leave Promise pending after navigation by design. 672 } 673 MOZ_ASSERT(aDevice->Kind() == dom::MediaDeviceKind::Audiooutput); 674 mExplicitlyGrantedAudioOutputRawIds.Insert(aDevice->RawID()); 675 p->MaybeResolve( 676 MakeRefPtr<MediaDeviceInfo>(aDevice->mID, aDevice->Kind(), 677 aDevice->mName, aDevice->mGroupID)); 678 }, 679 [this, self, p](const RefPtr<MediaMgrError>& error) { 680 nsPIDOMWindowInner* window = GetWindowIfCurrent(); 681 if (!window) { 682 return; // Leave Promise pending after navigation by design. 683 } 684 error->Reject(p); 685 }); 686 return p.forget(); 687 } 688 689 static RefPtr<AudioDeviceInfo> CopyWithNullDeviceId( 690 AudioDeviceInfo* aDeviceInfo) { 691 MOZ_ASSERT(aDeviceInfo->Preferred()); 692 693 nsString vendor; 694 aDeviceInfo->GetVendor(vendor); 695 uint16_t type; 696 aDeviceInfo->GetType(&type); 697 uint16_t state; 698 aDeviceInfo->GetState(&state); 699 uint16_t pref; 700 aDeviceInfo->GetPreferred(&pref); 701 uint16_t supportedFormat; 702 aDeviceInfo->GetSupportedFormat(&supportedFormat); 703 uint16_t defaultFormat; 704 aDeviceInfo->GetDefaultFormat(&defaultFormat); 705 uint32_t maxChannels; 706 aDeviceInfo->GetMaxChannels(&maxChannels); 707 uint32_t defaultRate; 708 aDeviceInfo->GetDefaultRate(&defaultRate); 709 uint32_t maxRate; 710 aDeviceInfo->GetMaxRate(&maxRate); 711 uint32_t minRate; 712 aDeviceInfo->GetMinRate(&minRate); 713 uint32_t maxLatency; 714 aDeviceInfo->GetMaxLatency(&maxLatency); 715 uint32_t minLatency; 716 aDeviceInfo->GetMinLatency(&minLatency); 717 718 return MakeRefPtr<AudioDeviceInfo>( 719 nullptr, aDeviceInfo->Name(), aDeviceInfo->GroupID(), vendor, type, state, 720 pref, supportedFormat, defaultFormat, maxChannels, defaultRate, maxRate, 721 minRate, maxLatency, minLatency); 722 } 723 724 RefPtr<MediaDevices::SinkInfoPromise> MediaDevices::GetSinkDevice( 725 const nsString& aDeviceId) { 726 MOZ_ASSERT(NS_IsMainThread()); 727 return MediaManager::Get() 728 ->GetPhysicalDevices() 729 ->Then( 730 GetCurrentSerialEventTarget(), __func__, 731 [self = RefPtr(this), this, 732 aDeviceId](RefPtr<const MediaDeviceSetRefCnt> aRawDevices) { 733 nsCOMPtr<nsPIDOMWindowInner> window = GetOwnerWindow(); 734 if (!window) { 735 return LocalDeviceSetPromise::CreateAndReject( 736 new MediaMgrError(MediaMgrError::Name::AbortError), __func__); 737 } 738 // Don't filter if matching the preferred device, because that may 739 // not be exposed. 740 RefPtr devices = aDeviceId.IsEmpty() 741 ? std::move(aRawDevices) 742 : FilterExposedDevices(*aRawDevices); 743 return MediaManager::Get()->AnonymizeDevices(window, 744 std::move(devices)); 745 }, 746 [](RefPtr<MediaMgrError>&& reason) { 747 MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject"); 748 return RefPtr<LocalDeviceSetPromise>(); 749 }) 750 ->Then( 751 GetCurrentSerialEventTarget(), __func__, 752 [aDeviceId](RefPtr<LocalMediaDeviceSetRefCnt> aDevices) { 753 RefPtr<AudioDeviceInfo> outputInfo; 754 // Check for a matching device. 755 for (const RefPtr<LocalMediaDevice>& device : *aDevices) { 756 if (device->Kind() != dom::MediaDeviceKind::Audiooutput) { 757 continue; 758 } 759 if (aDeviceId.IsEmpty()) { 760 MOZ_ASSERT(device->GetAudioDeviceInfo()->Preferred(), 761 "First Audiooutput should be preferred"); 762 return SinkInfoPromise::CreateAndResolve( 763 CopyWithNullDeviceId(device->GetAudioDeviceInfo()), 764 __func__); 765 } else if (aDeviceId.Equals(device->mID)) { 766 return SinkInfoPromise::CreateAndResolve( 767 device->GetAudioDeviceInfo(), __func__); 768 } 769 } 770 /* If sinkId is not the empty string and does not match any audio 771 * output device identified by the result that would be provided 772 * by enumerateDevices(), reject p with a new DOMException whose 773 * name is NotFoundError and abort these substeps. */ 774 return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, 775 __func__); 776 }, 777 // aRejectMethod = 778 [](RefPtr<MediaMgrError>&& aError) { 779 return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, 780 __func__); 781 }); 782 } 783 784 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(MediaDevices, 785 DOMEventTargetHelper) 786 NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaDevices, DOMEventTargetHelper, 787 mPendingEnumerateDevicesPromises) 788 789 void MediaDevices::OnDeviceChange() { 790 MOZ_ASSERT(NS_IsMainThread()); 791 if (NS_FAILED(CheckCurrentGlobalCorrectness())) { 792 // This is a ghost window, don't do anything. 793 return; 794 } 795 796 mHaveUnprocessedDeviceListChange = true; 797 MaybeResumeDeviceExposure(); 798 } 799 800 mozilla::dom::EventHandlerNonNull* MediaDevices::GetOndevicechange() { 801 return GetEventHandler(nsGkAtoms::ondevicechange); 802 } 803 804 void MediaDevices::SetupDeviceChangeListener() { 805 if (mIsDeviceChangeListenerSetUp) { 806 return; 807 } 808 809 nsPIDOMWindowInner* window = GetOwnerWindow(); 810 if (!window) { 811 return; 812 } 813 814 mDeviceChangeListener = MediaManager::Get()->DeviceListChangeEvent().Connect( 815 GetMainThreadSerialEventTarget(), this, &MediaDevices::OnDeviceChange); 816 mIsDeviceChangeListenerSetUp = true; 817 818 MediaManager::Get()->GetPhysicalDevices()->Then( 819 GetCurrentSerialEventTarget(), __func__, 820 [self = RefPtr(this), this](RefPtr<const MediaDeviceSetRefCnt> aDevices) { 821 mLastPhysicalDevices = std::move(aDevices); 822 }, 823 [](RefPtr<MediaMgrError>&& reason) { 824 MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject"); 825 }); 826 } 827 828 void MediaDevices::SetOndevicechange( 829 mozilla::dom::EventHandlerNonNull* aCallback) { 830 SetEventHandler(nsGkAtoms::ondevicechange, aCallback); 831 } 832 833 void MediaDevices::EventListenerAdded(nsAtom* aType) { 834 DOMEventTargetHelper::EventListenerAdded(aType); 835 SetupDeviceChangeListener(); 836 } 837 838 JSObject* MediaDevices::WrapObject(JSContext* aCx, 839 JS::Handle<JSObject*> aGivenProto) { 840 return MediaDevices_Binding::Wrap(aCx, this, aGivenProto); 841 } 842 843 } // namespace mozilla::dom