From ae8f8e7132f90147ea865e05ba62bdd35d9cae52 Mon Sep 17 00:00:00 2001 From: Karl Tomlinson Date: Wed, 2 Feb 2022 19:46:19 +0000 Subject: [PATCH] Bug 1753131 - Dispatch devicechange events even without an actively capturing MediaStreamTrack r=jib "fully active and has focus" is now a sufficient condition for dispatching "devicechange" events if the change in devices should be visible from enumerateDevices(). https://github.com/w3c/mediacapture-main/pull/574/files#diff-1217ca1c44ff30a33dd50c49d03b5cadc9633c789df8ff9370ed4a42859e1211R3146 Permissions checks are replaced with [[canExposeCameraInfo]] and [[canExposeMicrophoneInfo]] slots set by getUserMedia(). https://github.com/w3c/mediacapture-main/pull/641 https://github.com/w3c/mediacapture-main/pull/773 The "media.navigator.permission.disabled" pref is no longer involved in "devicechange" dispatch decisions. Differential Revision: https://phabricator.services.mozilla.com/D132908 --- dom/media/MediaDevices.cpp | 399 ++++++++++++------ dom/media/MediaDevices.h | 29 +- dom/media/MediaManager.cpp | 42 -- dom/media/MediaManager.h | 22 +- dom/media/MediaStreamError.cpp | 2 +- dom/media/MediaStreamError.h | 2 +- .../tests/mochitests/test_ondevicechange.html | 39 +- 7 files changed, 329 insertions(+), 206 deletions(-) diff --git a/dom/media/MediaDevices.cpp b/dom/media/MediaDevices.cpp index 7634d7da0e7f..fbbdb272f06c 100644 --- a/dom/media/MediaDevices.cpp +++ b/dom/media/MediaDevices.cpp @@ -6,8 +6,10 @@ #include "AudioDeviceInfo.h" #include "MediaEngine.h" +#include "MediaEngineDefault.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" @@ -24,8 +26,10 @@ namespace mozilla::dom { -using EnumerationFlag = MediaManager::EnumerationFlag; +using ConstDeviceSetPromise = MediaManager::ConstDeviceSetPromise; +using LocalDeviceSetPromise = MediaManager::LocalDeviceSetPromise; using LocalMediaDeviceSetRefCnt = MediaManager::LocalMediaDeviceSetRefCnt; +using MediaDeviceSetRefCnt = MediaManager::MediaDeviceSetRefCnt; MediaDevices::MediaDevices(nsPIDOMWindowInner* aWindow) : DOMEventTargetHelper(aWindow) {} @@ -97,18 +101,27 @@ already_AddRefed MediaDevices::GetUserMedia( (audio.IsBoolean() ? audio.GetAsBoolean() : !audio.GetAsMediaTrackConstraints().mMediaSource.WasPassed()); + bool isCamera = + !haveFake && + (video.IsBoolean() + ? video.GetAsBoolean() + : !video.GetAsMediaTrackConstraints().mMediaSource.WasPassed()); RefPtr self(this); MediaManager::Get() ->GetUserMedia(owner, aConstraints, aCallerType) ->Then( GetCurrentSerialEventTarget(), __func__, - [this, self, p, isMicrophone](RefPtr&& aStream) { + [this, self, p, isMicrophone, + isCamera](RefPtr&& aStream) { if (!GetWindowIfCurrent()) { return; // Leave Promise pending after navigation by design. } if (isMicrophone) { mCanExposeMicrophoneInfo = true; } + if (isCamera) { + mCanExposeCameraInfo = true; + } p->MaybeResolve(std::move(aStream)); }, [this, self, p](const RefPtr& error) { @@ -161,82 +174,232 @@ void MediaDevices::MaybeResumeDeviceExposure() { !bc->GetIsActiveBrowserWindow()) { // browser window does not have focus return; } - if (mHaveUnprocessedDeviceListChange) { - NS_DispatchToCurrentThread( - NS_NewRunnableFunction("devicechange", [self = RefPtr(this), this] { - DispatchTrustedEvent(u"devicechange"_ns); - })); - mHaveUnprocessedDeviceListChange = false; - } - - auto pending = std::move(mPendingEnumerateDevicesPromises); - for (auto& promise : pending) { - ResumeEnumerateDevices(std::move(promise)); - } + 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; } -void MediaDevices::ResumeEnumerateDevices(RefPtr aPromise) { +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); + + bool resistFingerprinting = nsContentUtils::ShouldResistFingerprinting(doc); + if (resistFingerprinting) { + RefPtr fakeEngine = new MediaEngineDefault(); + 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. + } + nsTHashSet exposedMicrophoneGroupIds; + for (const auto& device : aDevices) { + switch (device->mKind) { + case MediaDeviceKind::Audioinput: + if (dropMics) { + continue; + } + if (mCanExposeMicrophoneInfo) { + exposedMicrophoneGroupIds.Insert(device->mRawGroupID); + } + // Reducing to one mic or cam device when not mCanExposeMicrophoneInfo + // or not mCanExposeCameraInfo is bug 1528042. + break; + case MediaDeviceKind::Videoinput: + if (dropCams) { + continue; + } + break; + case MediaDeviceKind::Audiooutput: + if (dropSpeakers || + (!mExplicitlyGrantedAudioOutputRawIds.Contains(device->mRawID) && + // Assumes aDevices order has microphones before speakers. + !exposedMicrophoneGroupIds.Contains(device->mRawGroupID))) { + continue; + } + break; + case MediaDeviceKind::EndGuard_: + continue; + // Avoid `default:` so that `-Wswitch` catches missing + // enumerators at compile time. + } + exposed->AppendElement(device); + } + return exposed; +} + +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. + auto CanExposeNonZeroChanges = [this](MediaDeviceKind aKind) { + switch (aKind) { + case MediaDeviceKind::Audioinput: + return mCanExposeMicrophoneInfo; + case MediaDeviceKind::Videoinput: + return mCanExposeCameraInfo; + case MediaDeviceKind::Audiooutput: + return true; + case MediaDeviceKind::EndGuard_: + break; + // Avoid `default:` so that `-Wswitch` catches missing enumerators at + // compile time. + } + MOZ_ASSERT_UNREACHABLE("unexpected MediaDeviceKind"); + return false; + }; + 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 (CanExposeNonZeroChanges(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(); - MOZ_ASSERT(window, "Fully active document should have window"); - RefPtr self(this); - MediaManager::Get()->EnumerateDevices(window)->Then( - GetCurrentSerialEventTarget(), __func__, - [this, self, aPromise](RefPtr&& aDevices) { - nsPIDOMWindowInner* window = GetWindowIfCurrent(); - if (!window) { - return; // Leave Promise pending after navigation by design. + 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 allowLabel = + aDevices.Length() == 0 || + MediaManager::Get()->IsActivelyCapturingOrHasAPermission(windowId); + for (const RefPtr& device : aDevices) { + nsString label; + MOZ_ASSERT(device->Kind() < MediaDeviceKind::EndGuard_); + switch (device->Kind()) { + case MediaDeviceKind::Audioinput: + case MediaDeviceKind::Videoinput: + // Include name only if page currently has a gUM stream + // active or persistent permissions (audio or video) have + // been granted. See bug 1528042 for using + // mCanExposeMicrophoneInfo. + if (allowLabel || Preferences::GetBool( + "media.navigator.permission.disabled", false)) { + label = device->mName; } - auto windowId = window->WindowID(); - nsTArray> infos; - bool allowLabel = - aDevices->Length() == 0 || - MediaManager::Get()->IsActivelyCapturingOrHasAPermission(windowId); - nsTHashSet exposedMicrophoneGroupIds; - for (auto& device : *aDevices) { - nsString label; - MOZ_ASSERT(device->Kind() < MediaDeviceKind::EndGuard_); - switch (device->Kind()) { - case MediaDeviceKind::Audioinput: - if (mCanExposeMicrophoneInfo) { - exposedMicrophoneGroupIds.Insert(device->mGroupID); - } - [[fallthrough]]; - case MediaDeviceKind::Videoinput: - // Include name only if page currently has a gUM stream - // active or persistent permissions (audio or video) have - // been granted. See bug 1528042 for using - // mCanExposeMicrophoneInfo. - if (allowLabel || - Preferences::GetBool("media.navigator.permission.disabled", - false)) { - label = device->mName; - } - break; - case MediaDeviceKind::Audiooutput: - if (!mExplicitlyGrantedAudioOutputIds.Contains(device->mID) && - // Assumes aDevices order has microphones before speakers. - !exposedMicrophoneGroupIds.Contains(device->mGroupID)) { - continue; - } - label = device->mName; - break; - case MediaDeviceKind::EndGuard_: - break; - // Avoid `default:` so that `-Wswitch` catches missing - // enumerators at compile time. - } - infos.AppendElement(MakeRefPtr( - device->mID, device->Kind(), label, device->mGroupID)); - } - aPromise->MaybeResolve(std::move(infos)); - }, - [this, self, aPromise](const RefPtr& error) { - nsPIDOMWindowInner* window = GetWindowIfCurrent(); - if (!window) { - return; // Leave Promise pending after navigation by design. - } - error->Reject(aPromise); - }); + break; + case MediaDeviceKind::Audiooutput: + label = device->mName; + break; + case MediaDeviceKind::EndGuard_: + break; + // Avoid `default:` so that `-Wswitch` catches missing + // enumerators at compile time. + } + infos.AppendElement(MakeRefPtr(device->mID, device->Kind(), + label, device->mGroupID)); + } + aPromise->MaybeResolve(std::move(infos)); } already_AddRefed MediaDevices::GetDisplayMedia( @@ -399,7 +562,7 @@ already_AddRefed MediaDevices::SelectAudioOutput( return; // Leave Promise pending after navigation by design. } MOZ_ASSERT(aDevice->Kind() == dom::MediaDeviceKind::Audiooutput); - mExplicitlyGrantedAudioOutputIds.Insert(aDevice->mID); + mExplicitlyGrantedAudioOutputRawIds.Insert(aDevice->RawID()); p->MaybeResolve( MakeRefPtr(aDevice->mID, aDevice->Kind(), aDevice->mName, aDevice->mGroupID)); @@ -452,62 +615,55 @@ static RefPtr CopyWithNullDeviceId( RefPtr MediaDevices::GetSinkDevice( const nsString& aDeviceId) { MOZ_ASSERT(NS_IsMainThread()); - - bool isExposed = aDeviceId.IsEmpty() || - mExplicitlyGrantedAudioOutputIds.Contains(aDeviceId); - // If the device id is not exposed, then check microphone groupIds. - MediaSourceEnum audioInputType = isExposed || !mCanExposeMicrophoneInfo - ? MediaSourceEnum::Other - : MediaSourceEnum::Microphone; - return MediaManager::Get() - ->EnumerateDevicesImpl(GetOwner(), MediaSourceEnum::Other, audioInputType, - EnumerationFlag::EnumerateAudioOutputs) + ->GetPhysicalDevices() ->Then( GetCurrentSerialEventTarget(), __func__, - [aDeviceId, - isExposed](RefPtr aDevices) mutable { + [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; - nsString groupId; // Check for a matching device. for (const RefPtr& device : *aDevices) { if (device->Kind() != dom::MediaDeviceKind::Audiooutput) { continue; } if (aDeviceId.IsEmpty()) { - if (device->GetAudioDeviceInfo()->Preferred()) { - outputInfo = - CopyWithNullDeviceId(device->GetAudioDeviceInfo()); - break; - } + MOZ_ASSERT(device->GetAudioDeviceInfo()->Preferred(), + "First Audiooutput should be preferred"); + return SinkInfoPromise::CreateAndResolve( + CopyWithNullDeviceId(device->GetAudioDeviceInfo()), + __func__); } else if (aDeviceId.Equals(device->mID)) { - outputInfo = device->GetAudioDeviceInfo(); - groupId = device->mGroupID; - break; - } - } - if (outputInfo && !isExposed) { - // Check microphone groups. - MOZ_ASSERT(!groupId.IsEmpty()); - for (const RefPtr& device : *aDevices) { - if (device->Kind() != dom::MediaDeviceKind::Audioinput) { - continue; - } - if (groupId.Equals(device->mGroupID)) { - isExposed = true; - break; - } + 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. */ - if (!outputInfo || !isExposed) { - return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, - __func__); - } - return SinkInfoPromise::CreateAndResolve(outputInfo, __func__); + return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, + __func__); }, // aRejectMethod = [](RefPtr&& aError) { @@ -528,12 +684,6 @@ void MediaDevices::OnDeviceChange() { return; } - if (!(MediaManager::Get()->IsActivelyCapturingOrHasAPermission( - GetOwner()->WindowID()) || - Preferences::GetBool("media.navigator.permission.disabled", false))) { - return; - } - // Do not fire event to content script when // privacy.resistFingerprinting is true. if (nsContentUtils::ShouldResistFingerprinting()) { @@ -567,6 +717,15 @@ void MediaDevices::SetupDeviceChangeListener() { mDeviceChangeListener = MediaManager::Get()->DeviceListChangeEvent().Connect( mainThread, 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( diff --git a/dom/media/MediaDevices.h b/dom/media/MediaDevices.h index 240c75ba1ce4..988ffd2906a0 100644 --- a/dom/media/MediaDevices.h +++ b/dom/media/MediaDevices.h @@ -20,9 +20,17 @@ class AudioDeviceInfo; namespace mozilla { +class LocalMediaDevice; +class MediaDevice; +class MediaMgrError; template class MozPromise; +namespace media { +template +class Refcountable; +} // namespace media + namespace dom { class Promise; @@ -84,22 +92,33 @@ class MediaDevices final : public DOMEventTargetHelper { void BrowserWindowBecameActive() { MaybeResumeDeviceExposure(); } private: - class GumResolver; - class EnumDevResolver; - class GumRejecter; + using MediaDeviceSet = nsTArray>; + using MediaDeviceSetRefCnt = media::Refcountable; + using LocalMediaDeviceSet = nsTArray>; virtual ~MediaDevices(); void MaybeResumeDeviceExposure(); - void ResumeEnumerateDevices(RefPtr aPromise); + void ResumeEnumerateDevices( + nsTArray>&& aPromises, + RefPtr aExposedDevices) const; + RefPtr FilterExposedDevices( + const MediaDeviceSet& aDevices) const; + bool ShouldQueueDeviceChange(const MediaDeviceSet& aExposedDevices) const; + void ResolveEnumerateDevicesPromise( + Promise* aPromise, const LocalMediaDeviceSet& aDevices) const; - nsTHashSet mExplicitlyGrantedAudioOutputIds; + nsTHashSet mExplicitlyGrantedAudioOutputRawIds; nsTArray> mPendingEnumerateDevicesPromises; // Connect/Disconnect on main thread only MediaEventListener mDeviceChangeListener; + // Ordered set of the system physical devices when devicechange event + // decisions were last performed. + RefPtr mLastPhysicalDevices; bool mIsDeviceChangeListenerSetUp = false; bool mHaveUnprocessedDeviceListChange = false; bool mCanExposeMicrophoneInfo = false; + bool mCanExposeCameraInfo = false; void RecordAccessTelemetry(const UseCounter counter) const; }; diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp index c7f52fd8e02c..8167c1c58ab5 100644 --- a/dom/media/MediaManager.cpp +++ b/dom/media/MediaManager.cpp @@ -2997,48 +2997,6 @@ RefPtr MediaManager::EnumerateDevicesImpl( }); } -RefPtr MediaManager::EnumerateDevices( - nsPIDOMWindowInner* aWindow) { - MOZ_ASSERT(NS_IsMainThread()); - if (sHasShutdown) { - return LocalDeviceSetPromise::CreateAndReject( - MakeRefPtr(MediaMgrError::Name::AbortError, - "In shutdown"), - __func__); - } - Document* doc = aWindow->GetExtantDoc(); - MOZ_ASSERT(doc); - - // Only expose devices which are allowed to use: - // https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices - MediaSourceEnum videoType = - FeaturePolicyUtils::IsFeatureAllowed(doc, u"camera"_ns) - ? MediaSourceEnum::Camera - : MediaSourceEnum::Other; - MediaSourceEnum audioType = - FeaturePolicyUtils::IsFeatureAllowed(doc, u"microphone"_ns) - ? MediaSourceEnum::Microphone - : MediaSourceEnum::Other; - EnumerationFlags flags; - if (Preferences::GetBool("media.setsinkid.enabled") && - FeaturePolicyUtils::IsFeatureAllowed(doc, u"speaker-selection"_ns)) { - flags += EnumerationFlag::EnumerateAudioOutputs; - } - - if (audioType == MediaSourceEnum::Other && - videoType == MediaSourceEnum::Other && flags.isEmpty()) { - return LocalDeviceSetPromise::CreateAndResolve( - new LocalMediaDeviceSetRefCnt(), __func__); - } - - bool resistFingerprinting = nsContentUtils::ShouldResistFingerprinting(doc); - if (resistFingerprinting) { - flags += EnumerationFlag::ForceFakes; - } - - return EnumerateDevicesImpl(aWindow, videoType, audioType, flags); -} - RefPtr MediaManager::SelectAudioOutput( nsPIDOMWindowInner* aWindow, const dom::AudioOutputOptions& aOptions, CallerType aCallerType) { diff --git a/dom/media/MediaManager.h b/dom/media/MediaManager.h index 00fc127c0c86..2ca86203134c 100644 --- a/dom/media/MediaManager.h +++ b/dom/media/MediaManager.h @@ -278,18 +278,6 @@ class MediaManager final : public nsIMediaManagerService, const dom::MediaStreamConstraints& aConstraints, dom::CallerType aCallerType); - RefPtr EnumerateDevices(nsPIDOMWindowInner* aWindow); - - enum class EnumerationFlag { - AllowPermissionRequest, - EnumerateAudioOutputs, - ForceFakes, - }; - using EnumerationFlags = EnumSet; - RefPtr EnumerateDevicesImpl( - nsPIDOMWindowInner* aWindow, dom::MediaSourceEnum aVideoInputType, - dom::MediaSourceEnum aAudioInputType, EnumerationFlags aFlags); - RefPtr SelectAudioOutput( nsPIDOMWindowInner* aWindow, const dom::AudioOutputOptions& aOptions, dom::CallerType aCallerType); @@ -330,6 +318,16 @@ class MediaManager final : public nsIMediaManagerService, const MediaDeviceSet& aAudios); private: + enum class EnumerationFlag { + AllowPermissionRequest, + EnumerateAudioOutputs, + ForceFakes, + }; + using EnumerationFlags = EnumSet; + RefPtr EnumerateDevicesImpl( + nsPIDOMWindowInner* aWindow, dom::MediaSourceEnum aVideoInputType, + dom::MediaSourceEnum aAudioInputType, EnumerationFlags aFlags); + RefPtr EnumerateRawDevices( dom::MediaSourceEnum aVideoInputType, dom::MediaSourceEnum aAudioInputType, EnumerationFlags aFlags); diff --git a/dom/media/MediaStreamError.cpp b/dom/media/MediaStreamError.cpp index 2ca3751bab95..27f12012615e 100644 --- a/dom/media/MediaStreamError.cpp +++ b/dom/media/MediaStreamError.cpp @@ -47,7 +47,7 @@ BaseMediaMgrError::BaseMediaMgrError(Name aName, const nsACString& aMessage, NS_IMPL_ISUPPORTS0(MediaMgrError) -void MediaMgrError::Reject(dom::Promise* aPromise) { +void MediaMgrError::Reject(dom::Promise* aPromise) const { switch (mName) { case Name::AbortError: aPromise->MaybeRejectWithAbortError(mMessage); diff --git a/dom/media/MediaStreamError.h b/dom/media/MediaStreamError.h index 68c35b53e59d..5ac0bf7f4b5e 100644 --- a/dom/media/MediaStreamError.h +++ b/dom/media/MediaStreamError.h @@ -71,7 +71,7 @@ class MediaMgrError final : public nsISupports, public BaseMediaMgrError { NS_DECL_THREADSAFE_ISUPPORTS - void Reject(dom::Promise* aPromise); + void Reject(dom::Promise* aPromise) const; private: ~MediaMgrError() = default; diff --git a/dom/media/webrtc/tests/mochitests/test_ondevicechange.html b/dom/media/webrtc/tests/mochitests/test_ondevicechange.html index 94cfc0c57eae..8881b4c0f35e 100644 --- a/dom/media/webrtc/tests/mochitests/test_ondevicechange.html +++ b/dom/media/webrtc/tests/mochitests/test_ondevicechange.html @@ -47,31 +47,29 @@ runTest(async () => { // Make fake devices count as real, permission-wise, or devicechange // events won't be exposed ["media.navigator.permission.fake", true], - // Initial state for gUM. + // For gUM. ["media.navigator.permission.disabled", true] ), ]); const topDevices = navigator.mediaDevices; const frameDevices = iframe.contentWindow.navigator.mediaDevices; - const streams = await Promise.all( - [topDevices, frameDevices].map(d => d.getUserMedia({video: true})) - ); - const [topTrack, frameTrack] = streams.map(s => s.getVideoTracks()[0]); - topTrack.stop(); - // permission.disabled would expose devicechange events without gUM - await pushPrefs(["media.navigator.permission.disabled", false]); - + // Initialization of MediaDevices::mLastPhysicalDevices is triggered when + // ondevicechange is set but tests "media.getusermedia.fake-camera-name" + // asynchronously. Wait for getUserMedia() completion to ensure that the + // pref has been read before doDevicechanges() changes it. + frameDevices.ondevicechange = () => {}; const topEventPromise = resolveOnEvent(topDevices, "devicechange"); + const frameStream = await frameDevices.getUserMedia({video: true}); + frameStream.getVideoTracks()[0].stop(); + await doDevicechanges(frameDevices); ok(true, - "devicechange event is fired when gUM is in use without permanent " + - "permission granted"); + "devicechange event is fired when gUM has been in use"); // Race a settled Promise to check that the event has not been received in // the toplevel Window. - const racer = {type: "racer"}; + const racer = {}; is(await Promise.race([topEventPromise, racer]), racer, - "devicechange event is NOT fired when gUM is NO LONGER in use and " + - "permanent permission is NOT granted"); + "devicechange event is NOT fired when gUM has NOT been in use"); if (navigator.userAgent.includes("Android")) { todo(false, "test assumes Firefox-for-Desktop specific API and behavior"); @@ -95,10 +93,8 @@ runTest(async () => { is(document.visibilityState, 'hidden', 'visibilityState') const frameEventPromise = resolveOnEvent(frameDevices, "devicechange"); const tabDevices = tab.navigator.mediaDevices; - const tabStream = await withPrefs( - [["media.navigator.permission.disabled", true]], - () => tabDevices.getUserMedia({video: true}) - ); + tabDevices.ondevicechange = () => {}; + const tabStream = await tabDevices.getUserMedia({video: true}); await doDevicechanges(tabDevices); is(await Promise.race([frameEventPromise, racer]), racer, "devicechange event is NOT fired while tab is in background"); @@ -107,13 +103,6 @@ runTest(async () => { is(document.visibilityState, 'visible', 'visibilityState') await frameEventPromise; ok(true, "devicechange event IS fired when tab returns to foreground"); - frameTrack.stop(); - - await pushPrefs(["media.navigator.permission.disabled", true]); - await doDevicechanges(frameDevices); - is((await Promise.race([topEventPromise, racer])).type, "devicechange", - "devicechange event IS fired when gUM is NO LONGER in use and " + - "permanent permission IS granted"); });