зеркало из https://github.com/mozilla/gecko-dev.git
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
This commit is contained in:
Родитель
559abbbdc0
Коммит
ae8f8e7132
|
@ -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<Promise> MediaDevices::GetUserMedia(
|
|||
(audio.IsBoolean()
|
||||
? audio.GetAsBoolean()
|
||||
: !audio.GetAsMediaTrackConstraints().mMediaSource.WasPassed());
|
||||
bool isCamera =
|
||||
!haveFake &&
|
||||
(video.IsBoolean()
|
||||
? video.GetAsBoolean()
|
||||
: !video.GetAsMediaTrackConstraints().mMediaSource.WasPassed());
|
||||
RefPtr<MediaDevices> self(this);
|
||||
MediaManager::Get()
|
||||
->GetUserMedia(owner, aConstraints, aCallerType)
|
||||
->Then(
|
||||
GetCurrentSerialEventTarget(), __func__,
|
||||
[this, self, p, isMicrophone](RefPtr<DOMMediaStream>&& aStream) {
|
||||
[this, self, p, isMicrophone,
|
||||
isCamera](RefPtr<DOMMediaStream>&& 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<MediaMgrError>& 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<const MediaDeviceSetRefCnt> aAllDevices) mutable {
|
||||
RefPtr<MediaDeviceSetRefCnt> 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<MediaMgrError>&&) {
|
||||
MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject");
|
||||
});
|
||||
mHaveUnprocessedDeviceListChange = false;
|
||||
}
|
||||
|
||||
void MediaDevices::ResumeEnumerateDevices(RefPtr<Promise> aPromise) {
|
||||
RefPtr<MediaDeviceSetRefCnt> 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<nsString> 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<MediaDeviceSetRefCnt> 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<RefPtr<Promise>>&& aPromises,
|
||||
RefPtr<const MediaDeviceSetRefCnt> aExposedDevices) const {
|
||||
nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
|
||||
MOZ_ASSERT(window, "Fully active document should have window");
|
||||
RefPtr<MediaDevices> self(this);
|
||||
MediaManager::Get()->EnumerateDevices(window)->Then(
|
||||
GetCurrentSerialEventTarget(), __func__,
|
||||
[this, self, aPromise](RefPtr<LocalMediaDeviceSetRefCnt>&& 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>& promise : promises) {
|
||||
if (aLocalDevices.IsReject()) {
|
||||
aLocalDevices.RejectValue()->Reject(promise);
|
||||
} else {
|
||||
ResolveEnumerateDevicesPromise(
|
||||
promise, *aLocalDevices.ResolveValue());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MediaDevices::ResolveEnumerateDevicesPromise(
|
||||
Promise* aPromise, const LocalMediaDeviceSet& aDevices) const {
|
||||
nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
|
||||
auto windowId = window->WindowID();
|
||||
nsTArray<RefPtr<MediaDeviceInfo>> infos;
|
||||
bool allowLabel =
|
||||
aDevices.Length() == 0 ||
|
||||
MediaManager::Get()->IsActivelyCapturingOrHasAPermission(windowId);
|
||||
for (const RefPtr<LocalMediaDevice>& 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<RefPtr<MediaDeviceInfo>> infos;
|
||||
bool allowLabel =
|
||||
aDevices->Length() == 0 ||
|
||||
MediaManager::Get()->IsActivelyCapturingOrHasAPermission(windowId);
|
||||
nsTHashSet<nsString> 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<MediaDeviceInfo>(
|
||||
device->mID, device->Kind(), label, device->mGroupID));
|
||||
}
|
||||
aPromise->MaybeResolve(std::move(infos));
|
||||
},
|
||||
[this, self, aPromise](const RefPtr<MediaMgrError>& 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<MediaDeviceInfo>(device->mID, device->Kind(),
|
||||
label, device->mGroupID));
|
||||
}
|
||||
aPromise->MaybeResolve(std::move(infos));
|
||||
}
|
||||
|
||||
already_AddRefed<Promise> MediaDevices::GetDisplayMedia(
|
||||
|
@ -399,7 +562,7 @@ already_AddRefed<Promise> 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<MediaDeviceInfo>(aDevice->mID, aDevice->Kind(),
|
||||
aDevice->mName, aDevice->mGroupID));
|
||||
|
@ -452,62 +615,55 @@ static RefPtr<AudioDeviceInfo> CopyWithNullDeviceId(
|
|||
RefPtr<MediaDevices::SinkInfoPromise> 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<LocalMediaDeviceSetRefCnt> aDevices) mutable {
|
||||
[self = RefPtr(this), this,
|
||||
aDeviceId](RefPtr<const MediaDeviceSetRefCnt> aRawDevices) {
|
||||
nsCOMPtr<nsPIDOMWindowInner> 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<MediaMgrError>&& reason) {
|
||||
MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject");
|
||||
return RefPtr<LocalDeviceSetPromise>();
|
||||
})
|
||||
->Then(
|
||||
GetCurrentSerialEventTarget(), __func__,
|
||||
[aDeviceId](RefPtr<LocalMediaDeviceSetRefCnt> aDevices) {
|
||||
RefPtr<AudioDeviceInfo> outputInfo;
|
||||
nsString groupId;
|
||||
// Check for a matching device.
|
||||
for (const RefPtr<LocalMediaDevice>& 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<LocalMediaDevice>& 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<MediaMgrError>&& 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<const MediaDeviceSetRefCnt> aDevices) {
|
||||
mLastPhysicalDevices = std::move(aDevices);
|
||||
},
|
||||
[](RefPtr<MediaMgrError>&& reason) {
|
||||
MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject");
|
||||
});
|
||||
}
|
||||
|
||||
void MediaDevices::SetOndevicechange(
|
||||
|
|
|
@ -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;
|
||||
|
@ -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);
|
||||
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> mExplicitlyGrantedAudioOutputIds;
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
||||
</script>
|
||||
|
|
Загрузка…
Ссылка в новой задаче