/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/dom/MediaDevices.h" #include "AudioDeviceInfo.h" #include "MediaEngine.h" #include "MediaEngineFake.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/FeaturePolicyUtils.h" #include "mozilla/dom/MediaStreamBinding.h" #include "mozilla/dom/MediaDeviceInfo.h" #include "mozilla/dom/MediaDevicesBinding.h" #include "mozilla/dom/NavigatorBinding.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/WindowContext.h" #include "mozilla/intl/Localization.h" #include "mozilla/MediaManager.h" #include "mozilla/StaticPrefs_media.h" #include "MediaTrackConstraints.h" #include "nsContentUtils.h" #include "nsINamed.h" #include "nsIScriptGlobalObject.h" #include "nsPIDOMWindow.h" #include "nsGlobalWindowInner.h" #include "nsQueryObject.h" namespace mozilla::dom { using ConstDeviceSetPromise = MediaManager::ConstDeviceSetPromise; using LocalDeviceSetPromise = MediaManager::LocalDeviceSetPromise; using LocalMediaDeviceSetRefCnt = MediaManager::LocalMediaDeviceSetRefCnt; using MediaDeviceSetRefCnt = MediaManager::MediaDeviceSetRefCnt; using mozilla::intl::Localization; MediaDevices::MediaDevices(nsPIDOMWindowInner* aWindow) : DOMEventTargetHelper(aWindow), mDefaultOutputLabel(VoidString()) {} MediaDevices::~MediaDevices() { MOZ_ASSERT(NS_IsMainThread()); mDeviceChangeListener.DisconnectIfExists(); } already_AddRefed MediaDevices::GetUserMedia( const MediaStreamConstraints& aConstraints, CallerType aCallerType, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); // Get the relevant global for the promise from the wrapper cache because // DOMEventTargetHelper::GetOwner() returns null if the document is unloaded. // We know the wrapper exists because it is being used for |this| from JS. // See https://github.com/heycam/webidl/issues/932 for why the relevant // global is used instead of the current global. nsCOMPtr global = xpc::NativeGlobal(GetWrapper()); // global is a window because MediaDevices is exposed only to Window. nsCOMPtr owner = do_QueryInterface(global); if (Document* doc = owner->GetExtantDoc()) { if (!owner->IsSecureContext()) { doc->SetUseCounter(eUseCounter_custom_GetUserMediaInsec); } Document* topDoc = doc->GetTopLevelContentDocumentIfSameProcess(); IgnoredErrorResult ignored; if (topDoc && !topDoc->HasFocus(ignored)) { doc->SetUseCounter(eUseCounter_custom_GetUserMediaUnfocused); } } RefPtr p = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } /* If requestedMediaTypes is the empty set, return a promise rejected with a * TypeError. */ if (!MediaManager::IsOn(aConstraints.mVideo) && !MediaManager::IsOn(aConstraints.mAudio)) { p->MaybeRejectWithTypeError("audio and/or video is required"); return p.forget(); } /* If the relevant settings object's responsible document is NOT fully * active, return a promise rejected with a DOMException object whose name * attribute has the value "InvalidStateError". */ if (!owner->IsFullyActive()) { p->MaybeRejectWithInvalidStateError("The document is not fully active."); return p.forget(); } const OwningBooleanOrMediaTrackConstraints& video = aConstraints.mVideo; if (aCallerType != CallerType::System && video.IsMediaTrackConstraints()) { const Optional& mediaSource = video.GetAsMediaTrackConstraints().mMediaSource; if (mediaSource.WasPassed() && !mediaSource.Value().EqualsLiteral("camera")) { WindowContext* wc = owner->GetWindowContext(); if (!wc || !wc->HasValidTransientUserGestureActivation()) { p->MaybeRejectWithInvalidStateError( "Display capture requires transient activation " "from a user gesture."); return p.forget(); } } } RefPtr self(this); GetUserMedia(owner, aConstraints, aCallerType) ->Then( GetCurrentSerialEventTarget(), __func__, [this, self, p](RefPtr&& aStream) { if (!GetWindowIfCurrent()) { return; // Leave Promise pending after navigation by design. } p->MaybeResolve(std::move(aStream)); }, [this, self, p](const RefPtr& error) { nsPIDOMWindowInner* window = GetWindowIfCurrent(); if (!window) { return; // Leave Promise pending after navigation by design. } error->Reject(p); }); return p.forget(); } RefPtr MediaDevices::GetUserMedia( nsPIDOMWindowInner* aWindow, const MediaStreamConstraints& aConstraints, CallerType aCallerType) { MOZ_ASSERT(NS_IsMainThread()); bool haveFake = aConstraints.mFake.WasPassed() && aConstraints.mFake.Value(); const OwningBooleanOrMediaTrackConstraints& video = aConstraints.mVideo; const OwningBooleanOrMediaTrackConstraints& audio = aConstraints.mAudio; bool isMicrophone = !haveFake && (audio.IsBoolean() ? audio.GetAsBoolean() : !audio.GetAsMediaTrackConstraints().mMediaSource.WasPassed()); bool isCamera = !haveFake && (video.IsBoolean() ? video.GetAsBoolean() : !video.GetAsMediaTrackConstraints().mMediaSource.WasPassed()); RefPtr self(this); return MediaManager::Get() ->GetUserMedia(aWindow, aConstraints, aCallerType) ->Then( GetCurrentSerialEventTarget(), __func__, [this, self, isMicrophone, isCamera](RefPtr&& aStream) { if (isMicrophone) { mCanExposeMicrophoneInfo = true; } if (isCamera) { mCanExposeCameraInfo = true; } return StreamPromise::CreateAndResolve(std::move(aStream), __func__); }, [](RefPtr&& aError) { return StreamPromise::CreateAndReject(std::move(aError), __func__); }); } already_AddRefed MediaDevices::EnumerateDevices(ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr global = xpc::NativeGlobal(GetWrapper()); nsCOMPtr owner = do_QueryInterface(global); if (Document* doc = owner->GetExtantDoc()) { if (!owner->IsSecureContext()) { doc->SetUseCounter(eUseCounter_custom_EnumerateDevicesInsec); } Document* topDoc = doc->GetTopLevelContentDocumentIfSameProcess(); IgnoredErrorResult ignored; if (topDoc && !topDoc->HasFocus(ignored)) { doc->SetUseCounter(eUseCounter_custom_EnumerateDevicesUnfocused); } } RefPtr p = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } mPendingEnumerateDevicesPromises.AppendElement(p); MaybeResumeDeviceExposure(); return p.forget(); } void MediaDevices::MaybeResumeDeviceExposure() { if (mPendingEnumerateDevicesPromises.IsEmpty() && !mHaveUnprocessedDeviceListChange) { return; } nsPIDOMWindowInner* window = GetOwner(); if (!window || !window->IsFullyActive()) { return; } if (!StaticPrefs::media_devices_unfocused_enabled()) { // Device list changes are not exposed to unfocused contexts because the // timing information would allow fingerprinting for content to identify // concurrent browsing, even when pages are in different containers. BrowsingContext* bc = window->GetBrowsingContext(); if (!bc->IsActive() || // background tab or browser window fully obscured !bc->GetIsActiveBrowserWindow()) { // browser window without focus return; } } MediaManager::Get()->GetPhysicalDevices()->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr(this), this, haveDeviceListChange = mHaveUnprocessedDeviceListChange, enumerateDevicesPromises = std::move(mPendingEnumerateDevicesPromises)]( RefPtr aAllDevices) mutable { RefPtr exposedDevices = FilterExposedDevices(*aAllDevices); if (haveDeviceListChange) { if (ShouldQueueDeviceChange(*exposedDevices)) { NS_DispatchToCurrentThread(NS_NewRunnableFunction( "devicechange", [self = RefPtr(this), this] { DispatchTrustedEvent(u"devicechange"_ns); })); } mLastPhysicalDevices = std::move(aAllDevices); } if (!enumerateDevicesPromises.IsEmpty()) { ResumeEnumerateDevices(std::move(enumerateDevicesPromises), std::move(exposedDevices)); } }, [](RefPtr&&) { MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject"); }); mHaveUnprocessedDeviceListChange = false; } RefPtr MediaDevices::FilterExposedDevices( const MediaDeviceSet& aDevices) const { nsPIDOMWindowInner* window = GetOwner(); RefPtr exposed = new MediaDeviceSetRefCnt(); if (!window) { return exposed; // Promises will be left pending } Document* doc = window->GetExtantDoc(); if (!doc) { return exposed; } // Only expose devices which are allowed to use: // https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices bool dropMics = !FeaturePolicyUtils::IsFeatureAllowed(doc, u"microphone"_ns); bool dropCams = !FeaturePolicyUtils::IsFeatureAllowed(doc, u"camera"_ns); bool dropSpeakers = !Preferences::GetBool("media.setsinkid.enabled") || !FeaturePolicyUtils::IsFeatureAllowed(doc, u"speaker-selection"_ns); if (doc->ShouldResistFingerprinting(RFPTarget::MediaDevices)) { RefPtr fakeEngine = new MediaEngineFake(); fakeEngine->EnumerateDevices(MediaSourceEnum::Microphone, MediaSinkEnum::Other, exposed); fakeEngine->EnumerateDevices(MediaSourceEnum::Camera, MediaSinkEnum::Other, exposed); dropMics = dropCams = true; // Speakers are not handled specially with resistFingerprinting because // they are exposed only when explicitly and individually allowed by the // user. } bool legacy = StaticPrefs::media_devices_enumerate_legacy_enabled(); bool outputIsDefault = true; // First output is the default. bool haveDefaultOutput = false; nsTHashSet exposedMicrophoneGroupIds; for (const auto& device : aDevices) { switch (device->mKind) { case MediaDeviceKind::Audioinput: if (dropMics) { continue; } if (mCanExposeMicrophoneInfo) { exposedMicrophoneGroupIds.Insert(device->mRawGroupID); } if (!mCanExposeMicrophoneInfo && !legacy) { dropMics = true; } break; case MediaDeviceKind::Videoinput: if (dropCams) { continue; } if (!mCanExposeCameraInfo && !legacy) { dropCams = true; } break; case MediaDeviceKind::Audiooutput: if (dropSpeakers || (!mExplicitlyGrantedAudioOutputRawIds.Contains(device->mRawID) && // Assumes aDevices order has microphones before speakers. !exposedMicrophoneGroupIds.Contains(device->mRawGroupID))) { outputIsDefault = false; continue; } if (!haveDefaultOutput && !outputIsDefault) { // Insert a virtual default device so that the first enumerated // device is the default output. if (mDefaultOutputLabel.IsVoid()) { mDefaultOutputLabel.SetIsVoid(false); AutoTArray resourceIds{"dom/media.ftl"_ns}; RefPtr l10n = Localization::Create(resourceIds, /*sync*/ true); nsAutoCString translation; IgnoredErrorResult rv; l10n->FormatValueSync("default-audio-output-device-label"_ns, {}, translation, rv); if (!rv.Failed()) { AppendUTF8toUTF16(translation, mDefaultOutputLabel); } } RefPtr info = new AudioDeviceInfo( nullptr, mDefaultOutputLabel, u""_ns, u""_ns, CUBEB_DEVICE_TYPE_OUTPUT, CUBEB_DEVICE_STATE_ENABLED, CUBEB_DEVICE_PREF_ALL, CUBEB_DEVICE_FMT_ALL, CUBEB_DEVICE_FMT_S16NE, 2, 44100, 44100, 44100, 128, 128); exposed->AppendElement( new MediaDevice(new MediaEngineFake(), info, u""_ns)); } haveDefaultOutput = true; break; // Avoid `default:` so that `-Wswitch` catches missing // enumerators at compile time. } exposed->AppendElement(device); } return exposed; } bool MediaDevices::CanExposeInfo(MediaDeviceKind aKind) const { switch (aKind) { case MediaDeviceKind::Audioinput: return mCanExposeMicrophoneInfo; case MediaDeviceKind::Videoinput: return mCanExposeCameraInfo; case MediaDeviceKind::Audiooutput: // Assumes caller has used FilterExposedDevices() return true; // Avoid `default:` so that `-Wswitch` catches missing enumerators at // compile time. } MOZ_ASSERT_UNREACHABLE("unexpected MediaDeviceKind"); return false; } bool MediaDevices::ShouldQueueDeviceChange( const MediaDeviceSet& aExposedDevices) const { if (!mLastPhysicalDevices) { // SetupDeviceChangeListener not complete return false; } RefPtr lastExposedDevices = FilterExposedDevices(*mLastPhysicalDevices); auto exposed = aExposedDevices.begin(); auto exposedEnd = aExposedDevices.end(); auto last = lastExposedDevices->begin(); auto lastEnd = lastExposedDevices->end(); // Lists from FilterExposedDevices may have multiple devices of the same // kind even when only a single anonymous device of that kind should be // exposed by enumerateDevices() (but multiple devices are currently exposed // - bug 1528042). "devicechange" events are not queued when the number // of such devices changes but remains non-zero. while (exposed < exposedEnd && last < lastEnd) { // First determine whether there is at least one device of the same kind // in both `aExposedDevices` and `lastExposedDevices`. // A change between zero and non-zero numbers of microphone or camera // devices triggers a devicechange event even if that kind of device is // not yet exposed. MediaDeviceKind kind = (*exposed)->mKind; if (kind != (*last)->mKind) { return true; } // `exposed` and `last` have matching kind. if (CanExposeInfo(kind)) { // Queue "devicechange" if there has been any change in devices of this // exposed kind. ID and kind uniquely identify a device. if ((*exposed)->mRawID != (*last)->mRawID) { return true; } ++exposed; ++last; continue; } // `aExposedDevices` and `lastExposedDevices` both have non-zero numbers // of devices of this unexposed kind. // Skip remaining devices of this kind because all devices of this kind // should be exposed as a single anonymous device. do { ++exposed; } while (exposed != exposedEnd && (*exposed)->mKind == kind); do { ++last; } while (last != lastEnd && (*last)->mKind == kind); } // Queue "devicechange" if the number of exposed devices differs. return exposed < exposedEnd || last < lastEnd; } void MediaDevices::ResumeEnumerateDevices( nsTArray>&& aPromises, RefPtr aExposedDevices) const { nsCOMPtr window = GetOwner(); if (!window) { return; // Leave Promise pending after navigation by design. } MediaManager::Get() ->AnonymizeDevices(window, std::move(aExposedDevices)) ->Then(GetCurrentSerialEventTarget(), __func__, [self = RefPtr(this), this, promises = std::move(aPromises)]( const LocalDeviceSetPromise::ResolveOrRejectValue& aLocalDevices) { nsPIDOMWindowInner* window = GetWindowIfCurrent(); if (!window) { return; // Leave Promises pending after navigation by design. } for (const RefPtr& promise : promises) { if (aLocalDevices.IsReject()) { aLocalDevices.RejectValue()->Reject(promise); } else { ResolveEnumerateDevicesPromise( promise, *aLocalDevices.ResolveValue()); } } }); } void MediaDevices::ResolveEnumerateDevicesPromise( Promise* aPromise, const LocalMediaDeviceSet& aDevices) const { nsCOMPtr window = GetOwner(); auto windowId = window->WindowID(); nsTArray> infos; bool legacy = StaticPrefs::media_devices_enumerate_legacy_enabled(); bool capturePermitted = legacy && MediaManager::Get()->IsActivelyCapturingOrHasAPermission(windowId); for (const RefPtr& device : aDevices) { bool exposeInfo = CanExposeInfo(device->Kind()) || legacy; bool exposeLabel = legacy ? capturePermitted : exposeInfo; infos.AppendElement(MakeRefPtr( exposeInfo ? device->mID : u""_ns, device->Kind(), exposeLabel ? device->mName : u""_ns, exposeInfo ? device->mGroupID : u""_ns)); } aPromise->MaybeResolve(std::move(infos)); } already_AddRefed MediaDevices::GetDisplayMedia( const DisplayMediaStreamConstraints& aConstraints, CallerType aCallerType, ErrorResult& aRv) { nsCOMPtr global = xpc::NativeGlobal(GetWrapper()); RefPtr p = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } nsCOMPtr owner = do_QueryInterface(global); /* If the relevant global object of this does not have transient activation, * return a promise rejected with a DOMException object whose name attribute * has the value InvalidStateError. */ WindowContext* wc = owner->GetWindowContext(); if (!wc || !wc->HasValidTransientUserGestureActivation()) { p->MaybeRejectWithInvalidStateError( "getDisplayMedia requires transient activation from a user gesture."); return p.forget(); } /* If constraints.video is false, return a promise rejected with a newly * created TypeError. */ if (!MediaManager::IsOn(aConstraints.mVideo)) { p->MaybeRejectWithTypeError("video is required"); return p.forget(); } MediaStreamConstraints c; auto& vc = c.mVideo.SetAsMediaTrackConstraints(); if (aConstraints.mVideo.IsMediaTrackConstraints()) { vc = aConstraints.mVideo.GetAsMediaTrackConstraints(); /* If CS contains a member named advanced, return a promise rejected with * a newly created TypeError. */ if (vc.mAdvanced.WasPassed()) { p->MaybeRejectWithTypeError("advanced not allowed"); return p.forget(); } auto getCLR = [](const auto& aCon) -> const ConstrainLongRange& { static ConstrainLongRange empty; return (aCon.WasPassed() && !aCon.Value().IsLong()) ? aCon.Value().GetAsConstrainLongRange() : empty; }; auto getCDR = [](auto&& aCon) -> const ConstrainDoubleRange& { static ConstrainDoubleRange empty; return (aCon.WasPassed() && !aCon.Value().IsDouble()) ? aCon.Value().GetAsConstrainDoubleRange() : empty; }; const auto& w = getCLR(vc.mWidth); const auto& h = getCLR(vc.mHeight); const auto& f = getCDR(vc.mFrameRate); /* If CS contains a member whose name specifies a constrainable property * applicable to display surfaces, and whose value in turn is a dictionary * containing a member named either min or exact, return a promise * rejected with a newly created TypeError. */ if (w.mMin.WasPassed() || h.mMin.WasPassed() || f.mMin.WasPassed()) { p->MaybeRejectWithTypeError("min not allowed"); return p.forget(); } if (w.mExact.WasPassed() || h.mExact.WasPassed() || f.mExact.WasPassed()) { p->MaybeRejectWithTypeError("exact not allowed"); return p.forget(); } /* If CS contains a member whose name, failedConstraint specifies a * constrainable property, constraint, applicable to display surfaces, and * whose value in turn is a dictionary containing a member named max, and * that member's value in turn is less than the constrainable property's * floor value, then let failedConstraint be the name of the constraint, * let message be either undefined or an informative human-readable * message, and return a promise rejected with a new OverconstrainedError * created by calling OverconstrainedError(failedConstraint, message). */ // We fail early without incurring a prompt, on known-to-fail constraint // values that don't reveal anything about the user's system. const char* badConstraint = nullptr; if (w.mMax.WasPassed() && w.mMax.Value() < 1) { badConstraint = "width"; } if (h.mMax.WasPassed() && h.mMax.Value() < 1) { badConstraint = "height"; } if (f.mMax.WasPassed() && f.mMax.Value() < 1) { badConstraint = "frameRate"; } if (badConstraint) { p->MaybeReject(MakeRefPtr( owner, *MakeRefPtr( MediaMgrError::Name::OverconstrainedError, "", NS_ConvertASCIItoUTF16(badConstraint)))); return p.forget(); } } /* If the relevant settings object's responsible document is NOT fully * active, return a promise rejected with a DOMException object whose name * attribute has the value "InvalidStateError". */ if (!owner->IsFullyActive()) { p->MaybeRejectWithInvalidStateError("The document is not fully active."); return p.forget(); } // We ask for "screen" sharing. // // If this is a privileged call or permission is disabled, this gives us full // screen sharing by default, which is useful for internal testing. // // If this is a non-priviliged call, GetUserMedia() will change it to "window" // for us. vc.mMediaSource.Reset(); vc.mMediaSource.Construct().AssignASCII( dom::GetEnumString(MediaSourceEnum::Screen)); RefPtr self(this); MediaManager::Get() ->GetUserMedia(owner, c, aCallerType) ->Then( GetCurrentSerialEventTarget(), __func__, [this, self, p](RefPtr&& aStream) { if (!GetWindowIfCurrent()) { return; // leave promise pending after navigation. } p->MaybeResolve(std::move(aStream)); }, [this, self, p](RefPtr&& error) { nsPIDOMWindowInner* window = GetWindowIfCurrent(); if (!window) { return; // leave promise pending after navigation. } error->Reject(p); }); return p.forget(); } already_AddRefed MediaDevices::SelectAudioOutput( const AudioOutputOptions& aOptions, CallerType aCallerType, ErrorResult& aRv) { nsCOMPtr global = xpc::NativeGlobal(GetWrapper()); RefPtr p = Promise::Create(global, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } /* (This includes the expected user activation update of * https://github.com/w3c/mediacapture-output/issues/107) * If the relevant global object of this does not have transient activation, * return a promise rejected with a DOMException object whose name attribute * has the value InvalidStateError. */ nsCOMPtr owner = do_QueryInterface(global); WindowContext* wc = owner->GetWindowContext(); if (!wc || !wc->HasValidTransientUserGestureActivation()) { p->MaybeRejectWithInvalidStateError( "selectAudioOutput requires transient user activation."); return p.forget(); } RefPtr self(this); MediaManager::Get() ->SelectAudioOutput(owner, aOptions, aCallerType) ->Then( GetCurrentSerialEventTarget(), __func__, [this, self, p](RefPtr aDevice) { nsPIDOMWindowInner* window = GetWindowIfCurrent(); if (!window) { return; // Leave Promise pending after navigation by design. } MOZ_ASSERT(aDevice->Kind() == dom::MediaDeviceKind::Audiooutput); mExplicitlyGrantedAudioOutputRawIds.Insert(aDevice->RawID()); p->MaybeResolve( MakeRefPtr(aDevice->mID, aDevice->Kind(), aDevice->mName, aDevice->mGroupID)); }, [this, self, p](const RefPtr& error) { nsPIDOMWindowInner* window = GetWindowIfCurrent(); if (!window) { return; // Leave Promise pending after navigation by design. } error->Reject(p); }); return p.forget(); } static RefPtr CopyWithNullDeviceId( AudioDeviceInfo* aDeviceInfo) { MOZ_ASSERT(aDeviceInfo->Preferred()); nsString vendor; aDeviceInfo->GetVendor(vendor); uint16_t type; aDeviceInfo->GetType(&type); uint16_t state; aDeviceInfo->GetState(&state); uint16_t pref; aDeviceInfo->GetPreferred(&pref); uint16_t supportedFormat; aDeviceInfo->GetSupportedFormat(&supportedFormat); uint16_t defaultFormat; aDeviceInfo->GetDefaultFormat(&defaultFormat); uint32_t maxChannels; aDeviceInfo->GetMaxChannels(&maxChannels); uint32_t defaultRate; aDeviceInfo->GetDefaultRate(&defaultRate); uint32_t maxRate; aDeviceInfo->GetMaxRate(&maxRate); uint32_t minRate; aDeviceInfo->GetMinRate(&minRate); uint32_t maxLatency; aDeviceInfo->GetMaxLatency(&maxLatency); uint32_t minLatency; aDeviceInfo->GetMinLatency(&minLatency); return MakeRefPtr( nullptr, aDeviceInfo->Name(), aDeviceInfo->GroupID(), vendor, type, state, pref, supportedFormat, defaultFormat, maxChannels, defaultRate, maxRate, minRate, maxLatency, minLatency); } RefPtr MediaDevices::GetSinkDevice( const nsString& aDeviceId) { MOZ_ASSERT(NS_IsMainThread()); return MediaManager::Get() ->GetPhysicalDevices() ->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr(this), this, aDeviceId](RefPtr aRawDevices) { nsCOMPtr window = GetOwner(); if (!window) { return LocalDeviceSetPromise::CreateAndReject( new MediaMgrError(MediaMgrError::Name::AbortError), __func__); } // Don't filter if matching the preferred device, because that may // not be exposed. RefPtr devices = aDeviceId.IsEmpty() ? std::move(aRawDevices) : FilterExposedDevices(*aRawDevices); return MediaManager::Get()->AnonymizeDevices(window, std::move(devices)); }, [](RefPtr&& reason) { MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject"); return RefPtr(); }) ->Then( GetCurrentSerialEventTarget(), __func__, [aDeviceId](RefPtr aDevices) { RefPtr outputInfo; // Check for a matching device. for (const RefPtr& device : *aDevices) { if (device->Kind() != dom::MediaDeviceKind::Audiooutput) { continue; } if (aDeviceId.IsEmpty()) { MOZ_ASSERT(device->GetAudioDeviceInfo()->Preferred(), "First Audiooutput should be preferred"); return SinkInfoPromise::CreateAndResolve( CopyWithNullDeviceId(device->GetAudioDeviceInfo()), __func__); } else if (aDeviceId.Equals(device->mID)) { return SinkInfoPromise::CreateAndResolve( device->GetAudioDeviceInfo(), __func__); } } /* If sinkId is not the empty string and does not match any audio * output device identified by the result that would be provided * by enumerateDevices(), reject p with a new DOMException whose * name is NotFoundError and abort these substeps. */ return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); }, // aRejectMethod = [](RefPtr&& aError) { return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__); }); } NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(MediaDevices, DOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaDevices, DOMEventTargetHelper, mPendingEnumerateDevicesPromises) void MediaDevices::OnDeviceChange() { MOZ_ASSERT(NS_IsMainThread()); if (NS_FAILED(CheckCurrentGlobalCorrectness())) { // This is a ghost window, don't do anything. return; } // Do not fire event to content script when // privacy.resistFingerprinting is true. if (nsContentUtils::ShouldResistFingerprinting( "Guarding the more expensive RFP check with a simple one", RFPTarget::MediaDevices)) { nsCOMPtr window = GetOwner(); auto* wrapper = GetWrapper(); if (!window && wrapper) { nsCOMPtr global = xpc::NativeGlobal(wrapper); window = do_QueryInterface(global); } if (!window) { return; } if (nsGlobalWindowInner::Cast(window)->ShouldResistFingerprinting( RFPTarget::MediaDevices)) { return; } } mHaveUnprocessedDeviceListChange = true; MaybeResumeDeviceExposure(); } mozilla::dom::EventHandlerNonNull* MediaDevices::GetOndevicechange() { return GetEventHandler(nsGkAtoms::ondevicechange); } void MediaDevices::SetupDeviceChangeListener() { if (mIsDeviceChangeListenerSetUp) { return; } nsPIDOMWindowInner* window = GetOwner(); if (!window) { return; } mDeviceChangeListener = MediaManager::Get()->DeviceListChangeEvent().Connect( GetMainThreadSerialEventTarget(), this, &MediaDevices::OnDeviceChange); mIsDeviceChangeListenerSetUp = true; MediaManager::Get()->GetPhysicalDevices()->Then( GetCurrentSerialEventTarget(), __func__, [self = RefPtr(this), this](RefPtr aDevices) { mLastPhysicalDevices = std::move(aDevices); }, [](RefPtr&& reason) { MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject"); }); } void MediaDevices::SetOndevicechange( mozilla::dom::EventHandlerNonNull* aCallback) { SetEventHandler(nsGkAtoms::ondevicechange, aCallback); } void MediaDevices::EventListenerAdded(nsAtom* aType) { DOMEventTargetHelper::EventListenerAdded(aType); SetupDeviceChangeListener(); } JSObject* MediaDevices::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return MediaDevices_Binding::Wrap(aCx, this, aGivenProto); } } // namespace mozilla::dom