Skip to content

Commit

Permalink
Bug 1753131 - Dispatch devicechange events even without an actively c…
Browse files Browse the repository at this point in the history
…apturing 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().
w3c/mediacapture-main#641
w3c/mediacapture-main#773

The "media.navigator.permission.disabled" pref is no longer involved in "devicechange" dispatch decisions.

Differential Revision: https://phabricator.services.mozilla.com/D132908
  • Loading branch information
karlt committed Feb 2, 2022
1 parent 559abbb commit ae8f8e7
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 204 deletions.
393 changes: 276 additions & 117 deletions dom/media/MediaDevices.cpp

Large diffs are not rendered by default.

31 changes: 25 additions & 6 deletions dom/media/MediaDevices.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ class AudioDeviceInfo;

namespace mozilla {

class LocalMediaDevice;
class MediaDevice;
class MediaMgrError;
template <typename ResolveValueT, typename RejectValueT, bool IsExclusive>
class MozPromise;

namespace media {
template <typename T>
class Refcountable;
} // namespace media

namespace dom {

class Promise;
Expand Down Expand Up @@ -84,22 +92,33 @@ class MediaDevices final : public DOMEventTargetHelper {
void BrowserWindowBecameActive() { MaybeResumeDeviceExposure(); }

private:
class GumResolver;
class EnumDevResolver;
class GumRejecter;
using MediaDeviceSet = nsTArray<RefPtr<MediaDevice>>;
using MediaDeviceSetRefCnt = media::Refcountable<MediaDeviceSet>;
using LocalMediaDeviceSet = nsTArray<RefPtr<LocalMediaDevice>>;

virtual ~MediaDevices();
void MaybeResumeDeviceExposure();
void ResumeEnumerateDevices(RefPtr<Promise> aPromise);

nsTHashSet<nsString> mExplicitlyGrantedAudioOutputIds;
void ResumeEnumerateDevices(
nsTArray<RefPtr<Promise>>&& aPromises,
RefPtr<const MediaDeviceSetRefCnt> aExposedDevices) const;
RefPtr<MediaDeviceSetRefCnt> FilterExposedDevices(
const MediaDeviceSet& aDevices) const;
bool ShouldQueueDeviceChange(const MediaDeviceSet& aExposedDevices) const;
void ResolveEnumerateDevicesPromise(
Promise* aPromise, const LocalMediaDeviceSet& aDevices) const;

nsTHashSet<nsString> mExplicitlyGrantedAudioOutputRawIds;
nsTArray<RefPtr<Promise>> mPendingEnumerateDevicesPromises;

// Connect/Disconnect on main thread only
MediaEventListener mDeviceChangeListener;
// Ordered set of the system physical devices when devicechange event
// decisions were last performed.
RefPtr<const MediaDeviceSetRefCnt> mLastPhysicalDevices;
bool mIsDeviceChangeListenerSetUp = false;
bool mHaveUnprocessedDeviceListChange = false;
bool mCanExposeMicrophoneInfo = false;
bool mCanExposeCameraInfo = false;

void RecordAccessTelemetry(const UseCounter counter) const;
};
Expand Down
42 changes: 0 additions & 42 deletions dom/media/MediaManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2997,48 +2997,6 @@ RefPtr<LocalDeviceSetPromise> MediaManager::EnumerateDevicesImpl(
});
}

RefPtr<LocalDeviceSetPromise> MediaManager::EnumerateDevices(
nsPIDOMWindowInner* aWindow) {
MOZ_ASSERT(NS_IsMainThread());
if (sHasShutdown) {
return LocalDeviceSetPromise::CreateAndReject(
MakeRefPtr<MediaMgrError>(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<LocalDevicePromise> MediaManager::SelectAudioOutput(
nsPIDOMWindowInner* aWindow, const dom::AudioOutputOptions& aOptions,
CallerType aCallerType) {
Expand Down
22 changes: 10 additions & 12 deletions dom/media/MediaManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -278,18 +278,6 @@ class MediaManager final : public nsIMediaManagerService,
const dom::MediaStreamConstraints& aConstraints,
dom::CallerType aCallerType);

RefPtr<LocalDeviceSetPromise> EnumerateDevices(nsPIDOMWindowInner* aWindow);

enum class EnumerationFlag {
AllowPermissionRequest,
EnumerateAudioOutputs,
ForceFakes,
};
using EnumerationFlags = EnumSet<EnumerationFlag>;
RefPtr<LocalDeviceSetPromise> EnumerateDevicesImpl(
nsPIDOMWindowInner* aWindow, dom::MediaSourceEnum aVideoInputType,
dom::MediaSourceEnum aAudioInputType, EnumerationFlags aFlags);

RefPtr<LocalDevicePromise> SelectAudioOutput(
nsPIDOMWindowInner* aWindow, const dom::AudioOutputOptions& aOptions,
dom::CallerType aCallerType);
Expand Down Expand Up @@ -330,6 +318,16 @@ class MediaManager final : public nsIMediaManagerService,
const MediaDeviceSet& aAudios);

private:
enum class EnumerationFlag {
AllowPermissionRequest,
EnumerateAudioOutputs,
ForceFakes,
};
using EnumerationFlags = EnumSet<EnumerationFlag>;
RefPtr<LocalDeviceSetPromise> EnumerateDevicesImpl(
nsPIDOMWindowInner* aWindow, dom::MediaSourceEnum aVideoInputType,
dom::MediaSourceEnum aAudioInputType, EnumerationFlags aFlags);

RefPtr<DeviceSetPromise> EnumerateRawDevices(
dom::MediaSourceEnum aVideoInputType,
dom::MediaSourceEnum aAudioInputType, EnumerationFlags aFlags);
Expand Down
2 changes: 1 addition & 1 deletion dom/media/MediaStreamError.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion dom/media/MediaStreamError.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
39 changes: 14 additions & 25 deletions dom/media/webrtc/tests/mochitests/test_ondevicechange.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,29 @@
// 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");
Expand All @@ -95,10 +93,8 @@
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");
Expand All @@ -107,13 +103,6 @@
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");
});

</script>
Expand Down

0 comments on commit ae8f8e7

Please sign in to comment.