Skip to content

Commit

Permalink
[MediaStream] enumerateDevices should not exposed devices that can no…
Browse files Browse the repository at this point in the history
…t be used

https://bugs.webkit.org/show_bug.cgi?id=258993
rdar://110210394

Reviewed by Jer Noble and Youenn Fablet.

`enumerateDevices` should only include devices that are available for capture, so
make sure each device can be instantiated before including it.

* LayoutTests/fast/mediastream/camera-invalid-device-expected.txt: Added.
* LayoutTests/fast/mediastream/camera-invalid-device.html: Added.

* Source/WebCore/platform/mediastream/RealtimeMediaSourceCenter.cpp:
(WebCore::RealtimeMediaSourceCenter::getCapabilities): Change to return a std::optional<>
so it can signal failure.
* Source/WebCore/platform/mediastream/RealtimeMediaSourceCenter.h:

* Source/WebCore/platform/mock/MockMediaDevice.h:
(WebCore::MockMediaDevice::captureDevice const): Combine the existing `IsEphemeral` bool
with the new `Invalid` flag into a "Flags" bitfield.
(WebCore::MockMediaDevice::encode const): Encode the new flags variable.
(WebCore::MockMediaDevice::decodeMockMediaDevice): Decode it.
(WebCore::MockMediaDevice::decode): Ditto.

* Source/WebCore/platform/mock/MockRealtimeMediaSourceCenter.cpp:
(WebCore::defaultDevices): Update for the struct change.
(WebCore::MockRealtimeMediaSourceCenter::setDeviceIsEphemeral): Ditto.

* Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in: Define the new bitfield.

* Source/WebKit/UIProcess/API/C/WKMockMediaDevice.cpp:
(WKAddMockMediaDevice): Both mock camera and microphone can now have properties.

* Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp:
(WebKit::UserMediaPermissionRequestManagerProxy::platformGetMediaStreamDevices): Don't include
a device if `RealtimeMediaSourceCenter::getCapabilities` returns null.

* Tools/WebKitTestRunner/InjectedBundle/Bindings/TestRunner.idl:
* Tools/WebKitTestRunner/InjectedBundle/TestRunner.cpp:
(WTR::captureDeviceProperties): Convert the properties JS object to a WKDictionaryRef.
(WTR::TestRunner::addMockCameraDevice): Call captureDeviceProperties.
(WTR::TestRunner::addMockMicrophoneDevice): Add a properties parameter, call
captureDeviceProperties to parse it.
* Tools/WebKitTestRunner/InjectedBundle/TestRunner.h:

Canonical link: https://commits.webkit.org/265923@main
  • Loading branch information
eric-carlson committed Jul 10, 2023
1 parent d88306b commit f453768
Show file tree
Hide file tree
Showing 13 changed files with 184 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@


PASS Invalid cameras and microphones should not be exposed by enumerateDevices

51 changes: 51 additions & 0 deletions LayoutTests/fast/mediastream/camera-invalid-device.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Test that invalid capture devices are not exposed by enumerateDevices.</title>
<script src='../../resources/testharness.js'></script>
<script src='../../resources/testharnessreport.js'></script>
</head>
<body>
<video id='video'></video>
<script>
let setup = async (test) => {
if (!window.testRunner)
return Promise.reject('test requires internal API');

test.add_cleanup(() => { testRunner.resetMockMediaDevices(); });
}

async function getDeviceWithLabel(label)
{
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
const devices = await navigator.mediaDevices.enumerateDevices();
let deviceId = undefined;
devices.forEach(device => {
if (device.label === label)
deviceId = device.deviceId;
});

stream.getTracks().forEach(track => {
track.stop();
});

return deviceId;
}

promise_test(async (test) => {
await setup(test);

testRunner.addMockCameraDevice('BogusCamera', 'invalid camera', { invalid: 'true' });
let invalidDevice = await getDeviceWithLabel('invalid camera')
assert_equals(invalidDevice, undefined);

testRunner.resetMockMediaDevices();
testRunner.addMockMicrophoneDevice('BogusMicrophone', 'invalid microphone', { invalid: 'true' });
invalidDevice = await getDeviceWithLabel('invalid microphone')
assert_equals(invalidDevice, undefined);

}, 'Invalid cameras and microphones should not be exposed by enumerateDevices');
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -140,21 +140,22 @@ void RealtimeMediaSourceCenter::getMediaStreamDevices(CompletionHandler<void(Vec
});
}

RealtimeMediaSourceCapabilities RealtimeMediaSourceCenter::getCapabilities(const CaptureDevice& device)
std::optional<RealtimeMediaSourceCapabilities> RealtimeMediaSourceCenter::getCapabilities(const CaptureDevice& device)
{
if (device.type() == CaptureDevice::DeviceType::Camera) {
auto source = videoCaptureFactory().createVideoCaptureSource({ device }, { "fake"_s, "fake"_s }, nullptr, { });
if (!source)
return { };
return std::nullopt;
return source.source()->capabilities();
}
if (device.type() == CaptureDevice::DeviceType::Microphone) {
if (device.type() == CaptureDevice::DeviceType::Microphone || device.type() == CaptureDevice::DeviceType::Speaker) {
auto source = audioCaptureFactory().createAudioCaptureSource({ device }, { "fake"_s, "fake"_s }, nullptr, { });
if (!source)
return { };
return std::nullopt;
return source.source()->capabilities();
}
return { };

return std::nullopt;
}

static void addStringToSHA1(SHA1& sha1, const String& string)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class WEBCORE_EXPORT RealtimeMediaSourceCenter : public ThreadSafeRefCounted<Rea
void createMediaStream(Ref<const Logger>&&, NewMediaStreamHandler&&, MediaDeviceHashSalts&&, CaptureDevice&& audioDevice, CaptureDevice&& videoDevice, const MediaStreamRequest&);

WEBCORE_EXPORT void getMediaStreamDevices(CompletionHandler<void(Vector<CaptureDevice>&&)>&&);
WEBCORE_EXPORT RealtimeMediaSourceCapabilities getCapabilities(const CaptureDevice&);
WEBCORE_EXPORT std::optional<RealtimeMediaSourceCapabilities> getCapabilities(const CaptureDevice&);

const RealtimeMediaSourceSupportedConstraints& supportedConstraints() { return m_supportedConstraints; }

Expand Down
48 changes: 33 additions & 15 deletions Source/WebCore/platform/mock/MockMediaDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,19 +165,25 @@ struct MockMediaDevice {
bool isCamera() const { return std::holds_alternative<MockCameraProperties>(properties); }
bool isDisplay() const { return std::holds_alternative<MockDisplayProperties>(properties); }

enum Flag : uint8_t {
Ephemeral = 1 << 0,
Invalid = 1 << 1,
};
using Flags = OptionSet<Flag>;

CaptureDevice captureDevice() const
{
if (isMicrophone())
return CaptureDevice { persistentId, CaptureDevice::DeviceType::Microphone, label, persistentId, true, false, true, isEphemeral };
return CaptureDevice { persistentId, CaptureDevice::DeviceType::Microphone, label, persistentId, true, false, true, flags.contains(Ephemeral) };

if (isSpeaker())
return CaptureDevice { persistentId, CaptureDevice::DeviceType::Speaker, label, speakerProperties()->relatedMicrophoneId, true, false, true, isEphemeral };
return CaptureDevice { persistentId, CaptureDevice::DeviceType::Speaker, label, speakerProperties()->relatedMicrophoneId, true, false, true, flags.contains(Ephemeral) };

if (isCamera())
return CaptureDevice { persistentId, CaptureDevice::DeviceType::Camera, label, persistentId, true, false, true, isEphemeral };
return CaptureDevice { persistentId, CaptureDevice::DeviceType::Camera, label, persistentId, true, false, true, flags.contains(Ephemeral) };

ASSERT(isDisplay());
return CaptureDevice { persistentId, std::get<MockDisplayProperties>(properties).type, label, emptyString(), true, false, true, isEphemeral };
return CaptureDevice { persistentId, std::get<MockDisplayProperties>(properties).type, label, emptyString(), true, false, true, flags.contains(Ephemeral) };
}

CaptureDevice::DeviceType type() const
Expand Down Expand Up @@ -208,7 +214,7 @@ struct MockMediaDevice {
{
encoder << persistentId;
encoder << label;
encoder << isEphemeral;
encoder << flags;
WTF::switchOn(properties, [&](const MockMicrophoneProperties& properties) {
encoder << (uint8_t)1;
encoder << properties;
Expand All @@ -225,13 +231,13 @@ struct MockMediaDevice {
}

template <typename Properties, typename Decoder>
static std::optional<MockMediaDevice> decodeMockMediaDevice(Decoder& decoder, String&& persistentId, String&& label, bool isEphemeral)
static std::optional<MockMediaDevice> decodeMockMediaDevice(Decoder& decoder, String&& persistentId, String&& label, Flags flags)
{
std::optional<Properties> properties;
decoder >> properties;
if (!properties)
return std::nullopt;
return MockMediaDevice { WTFMove(persistentId), WTFMove(label), isEphemeral, WTFMove(*properties) };
return MockMediaDevice { WTFMove(persistentId), WTFMove(label), flags, WTFMove(*properties) };
}

template <class Decoder>
Expand All @@ -247,9 +253,9 @@ struct MockMediaDevice {
if (!label)
return std::nullopt;

std::optional<bool> isEphemeral;
decoder >> isEphemeral;
if (!isEphemeral)
std::optional<Flags> flags;
decoder >> flags;
if (!flags)
return std::nullopt;

std::optional<uint8_t> index;
Expand All @@ -259,23 +265,35 @@ struct MockMediaDevice {

switch (*index) {
case 1:
return decodeMockMediaDevice<MockMicrophoneProperties>(decoder, WTFMove(*persistentId), WTFMove(*label), *isEphemeral);
return decodeMockMediaDevice<MockMicrophoneProperties>(decoder, WTFMove(*persistentId), WTFMove(*label), *flags);
case 2:
return decodeMockMediaDevice<MockSpeakerProperties>(decoder, WTFMove(*persistentId), WTFMove(*label), *isEphemeral);
return decodeMockMediaDevice<MockSpeakerProperties>(decoder, WTFMove(*persistentId), WTFMove(*label), *flags);
case 3:
return decodeMockMediaDevice<MockCameraProperties>(decoder, WTFMove(*persistentId), WTFMove(*label), *isEphemeral);
return decodeMockMediaDevice<MockCameraProperties>(decoder, WTFMove(*persistentId), WTFMove(*label), *flags);
case 4:
return decodeMockMediaDevice<MockDisplayProperties>(decoder, WTFMove(*persistentId), WTFMove(*label), *isEphemeral);
return decodeMockMediaDevice<MockDisplayProperties>(decoder, WTFMove(*persistentId), WTFMove(*label), *flags);
}
return std::nullopt;
}

String persistentId;
String label;
bool isEphemeral;
Flags flags;
std::variant<MockMicrophoneProperties, MockSpeakerProperties, MockCameraProperties, MockDisplayProperties> properties;
};

} // namespace WebCore

namespace WTF {

template<> struct EnumTraits<WebCore::MockMediaDevice::Flag> {
using values = EnumValues<
WebCore::MockMediaDevice::Flag,
WebCore::MockMediaDevice::Flag::Ephemeral,
WebCore::MockMediaDevice::Flag::Invalid
>;
};

} // namespace WTF

#endif // ENABLE(MEDIA_STREAM)
43 changes: 28 additions & 15 deletions Source/WebCore/platform/mock/MockRealtimeMediaSourceCenter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ namespace WebCore {
static inline Vector<MockMediaDevice> defaultDevices()
{
return Vector<MockMediaDevice> {
MockMediaDevice { "239c24b0-2b15-11e3-8224-0800200c9a66"_s, "Mock audio device 1"_s, false, MockMicrophoneProperties { 44100 } },
MockMediaDevice { "239c24b1-2b15-11e3-8224-0800200c9a66"_s, "Mock audio device 2"_s, false, MockMicrophoneProperties { 48000 } },
MockMediaDevice { "239c24b0-2b15-11e3-8224-0800200c9a66"_s, "Mock audio device 1"_s, { }, MockMicrophoneProperties { 44100 } },
MockMediaDevice { "239c24b1-2b15-11e3-8224-0800200c9a66"_s, "Mock audio device 2"_s, { }, MockMicrophoneProperties { 48000 } },

MockMediaDevice { "239c24b0-2b15-11e3-8224-0800200c9a67"_s, "Mock speaker device 1"_s, false, MockSpeakerProperties { "239c24b0-2b15-11e3-8224-0800200c9a66"_s, 44100 } },
MockMediaDevice { "239c24b1-2b15-11e3-8224-0800200c9a67"_s, "Mock speaker device 2"_s, false, MockSpeakerProperties { "239c24b1-2b15-11e3-8224-0800200c9a66"_s, 48000 } },
MockMediaDevice { "239c24b2-2b15-11e3-8224-0800200c9a67"_s, "Mock speaker device 3"_s, false, MockSpeakerProperties { String { }, 48000 } },
MockMediaDevice { "239c24b0-2b15-11e3-8224-0800200c9a67"_s, "Mock speaker device 1"_s, { }, MockSpeakerProperties { "239c24b0-2b15-11e3-8224-0800200c9a66"_s, 44100 } },
MockMediaDevice { "239c24b1-2b15-11e3-8224-0800200c9a67"_s, "Mock speaker device 2"_s, { }, MockSpeakerProperties { "239c24b1-2b15-11e3-8224-0800200c9a66"_s, 48000 } },
MockMediaDevice { "239c24b2-2b15-11e3-8224-0800200c9a67"_s, "Mock speaker device 3"_s, { }, MockSpeakerProperties { String { }, 48000 } },

MockMediaDevice { "239c24b2-2b15-11e3-8224-0800200c9a66"_s, "Mock video device 1"_s, false,
MockMediaDevice { "239c24b2-2b15-11e3-8224-0800200c9a66"_s, "Mock video device 1"_s, { },
MockCameraProperties {
30,
VideoFacingMode::User, {
Expand All @@ -77,7 +77,7 @@ static inline Vector<MockMediaDevice> defaultDevices()
Color::black,
} },

MockMediaDevice { "239c24b3-2b15-11e3-8224-0800200c9a66"_s, "Mock video device 2"_s, false,
MockMediaDevice { "239c24b3-2b15-11e3-8224-0800200c9a66"_s, "Mock video device 2"_s, { },
MockCameraProperties {
15,
VideoFacingMode::Environment, {
Expand All @@ -93,11 +93,11 @@ static inline Vector<MockMediaDevice> defaultDevices()
Color::darkGray,
} },

MockMediaDevice { "SCREEN-1"_s, "Mock screen device 1"_s, false, MockDisplayProperties { CaptureDevice::DeviceType::Screen, Color::lightGray, { 1920, 1080 } } },
MockMediaDevice { "SCREEN-2"_s, "Mock screen device 2"_s, false, MockDisplayProperties { CaptureDevice::DeviceType::Screen, Color::yellow, { 3840, 2160 } } },
MockMediaDevice { "SCREEN-1"_s, "Mock screen device 1"_s, { }, MockDisplayProperties { CaptureDevice::DeviceType::Screen, Color::lightGray, { 1920, 1080 } } },
MockMediaDevice { "SCREEN-2"_s, "Mock screen device 2"_s, { }, MockDisplayProperties { CaptureDevice::DeviceType::Screen, Color::yellow, { 3840, 2160 } } },

MockMediaDevice { "WINDOW-1"_s, "Mock window device 1"_s, false, MockDisplayProperties { CaptureDevice::DeviceType::Window, SRGBA<uint8_t> { 255, 241, 181 }, { 640, 480 } } },
MockMediaDevice { "WINDOW-2"_s, "Mock window device 2"_s, false, MockDisplayProperties { CaptureDevice::DeviceType::Window, SRGBA<uint8_t> { 255, 208, 181 }, { 1280, 600 } } },
MockMediaDevice { "WINDOW-1"_s, "Mock window device 1"_s, { }, MockDisplayProperties { CaptureDevice::DeviceType::Window, SRGBA<uint8_t> { 255, 241, 181 }, { 640, 480 } } },
MockMediaDevice { "WINDOW-2"_s, "Mock window device 2"_s, { }, MockDisplayProperties { CaptureDevice::DeviceType::Window, SRGBA<uint8_t> { 255, 208, 181 }, { 1280, 600 } } },
};
}

Expand All @@ -109,6 +109,11 @@ class MockRealtimeVideoSourceFactory : public VideoCaptureFactory {
if (!MockRealtimeMediaSourceCenter::captureDeviceWithPersistentID(CaptureDevice::DeviceType::Camera, device.persistentId()))
return { "Unable to find mock camera device with given persistentID"_s };

auto mock = MockRealtimeMediaSourceCenter::mockDeviceWithPersistentID(device.persistentId());
ASSERT(mock);
if (mock->flags.contains(MockMediaDevice::Flag::Invalid))
return { "Invalid mock camera device"_s };

return MockRealtimeVideoSource::create(String { device.persistentId() }, AtomString { device.label() }, WTFMove(hashSalts), constraints, pageIdentifier);
}

Expand Down Expand Up @@ -215,9 +220,14 @@ class MockRealtimeAudioSourceFactory final : public AudioCaptureFactory {
public:
CaptureSourceOrError createAudioCaptureSource(const CaptureDevice& device, MediaDeviceHashSalts&& hashSalts, const MediaConstraints* constraints, PageIdentifier pageIdentifier) final
{
ASSERT(device.type() == CaptureDevice::DeviceType::Microphone);
if (!MockRealtimeMediaSourceCenter::captureDeviceWithPersistentID(CaptureDevice::DeviceType::Microphone, device.persistentId()))
return { "Unable to find mock microphone device with given persistentID"_s };
ASSERT(device.type() == CaptureDevice::DeviceType::Microphone || device.type() == CaptureDevice::DeviceType::Speaker);
if (!MockRealtimeMediaSourceCenter::captureDeviceWithPersistentID(device.type(), device.persistentId()))
return { "Unable to find mock microphone or speaker device with given persistentID"_s };

auto mock = MockRealtimeMediaSourceCenter::mockDeviceWithPersistentID(device.persistentId());
ASSERT(mock);
if (mock->flags.contains(MockMediaDevice::Flag::Invalid))
return { "Invalid mock microphone or speaker device"_s };

return MockRealtimeAudioSource::create(String { device.persistentId() }, AtomString { device.label() }, WTFMove(hashSalts), constraints, pageIdentifier);
}
Expand Down Expand Up @@ -410,7 +420,10 @@ void MockRealtimeMediaSourceCenter::setDeviceIsEphemeral(const String& persisten
return;

MockMediaDevice device = iterator->value;
device.isEphemeral = isEphemeral;
if (isEphemeral)
device.flags.add(MockMediaDevice::Flag::Ephemeral);
else
device.flags.remove(MockMediaDevice::Flag::Ephemeral);

removeDevice(persistentId);
addDevice(device);
Expand Down
6 changes: 6 additions & 0 deletions Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in
Original file line number Diff line number Diff line change
Expand Up @@ -4348,6 +4348,12 @@ class WebCore::RealtimeMediaSourceCapabilities {
WebCore::RealtimeMediaSourceSupportedConstraints supportedConstraints();
};

header: <WebCore/MockMediaDevice.h>
[Nested, OptionSet] enum class WebCore::MockMediaDevice::Flag : uint8_t {
Ephemeral
Invalid
};

#endif

enum class WebCore::PlatformVideoColorPrimaries : uint8_t {
Expand Down
11 changes: 10 additions & 1 deletion Source/WebKit/UIProcess/API/C/WKMockMediaDevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,16 @@ void WKAddMockMediaDevice(WKContextRef context, WKStringRef persistentId, WKStri
else if (typeString != "microphone"_s)
return;

toImpl(context)->addMockMediaDevice({ WebKit::toImpl(persistentId)->string(), WebKit::toImpl(label)->string(), false, WTFMove(deviceProperties) });
WebCore::MockMediaDevice::Flags flags;
if (properties) {
auto invalidKey = adoptWK(WKStringCreateWithUTF8CString("invalid"));
if (auto invalid = WKDictionaryGetItemForKey(properties, invalidKey.get())) {
if (WKStringIsEqualToUTF8CString(static_cast<WKStringRef>(invalid), "true"))
flags.add(WebCore::MockMediaDevice::Flag::Invalid);
}
}

toImpl(context)->addMockMediaDevice({ WebKit::toImpl(persistentId)->string(), WebKit::toImpl(label)->string(), flags, WTFMove(deviceProperties) });
#endif
}

Expand Down
22 changes: 16 additions & 6 deletions Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -813,13 +813,23 @@ static inline bool haveMicrophoneDevice(const Vector<CaptureDeviceWithCapabiliti
void UserMediaPermissionRequestManagerProxy::platformGetMediaStreamDevices(bool revealIdsAndLabels, CompletionHandler<void(Vector<CaptureDeviceWithCapabilities>&&)>&& completionHandler)
{
RealtimeMediaSourceCenter::singleton().getMediaStreamDevices([revealIdsAndLabels, completionHandler = WTFMove(completionHandler)](auto&& devices) mutable {
auto deviceWithCapabilities = map(devices, [revealIdsAndLabels](auto&& device) -> CaptureDeviceWithCapabilities {
RealtimeMediaSourceCapabilities capabilities;
Vector<CaptureDeviceWithCapabilities> devicesWithCapabilities;

devicesWithCapabilities.reserveInitialCapacity(devices.size());
for (auto& device : devices) {
RealtimeMediaSourceCapabilities deviceCapabilities;

auto capabilities = RealtimeMediaSourceCenter::singleton().getCapabilities(device);
if (!capabilities)
continue;

if (revealIdsAndLabels)
capabilities = RealtimeMediaSourceCenter::singleton().getCapabilities(device);
return { WTFMove(device), WTFMove(capabilities) };
});
completionHandler(WTFMove(deviceWithCapabilities));
deviceCapabilities = WTFMove(*capabilities);

devicesWithCapabilities.uncheckedAppend({ WTFMove(device), WTFMove(deviceCapabilities) });
}

completionHandler(WTFMove(devicesWithCapabilities));
});
}
#endif
Expand Down
22 changes: 16 additions & 6 deletions Source/WebKit/WebProcess/glib/UserMediaCaptureManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,23 @@ void UserMediaCaptureManager::validateUserMediaRequestConstraints(WebCore::Media
void UserMediaCaptureManager::getMediaStreamDevices(bool revealIdsAndLabels, GetMediaStreamDevicesCallback&& completionHandler)
{
RealtimeMediaSourceCenter::singleton().getMediaStreamDevices([completionHandler = WTFMove(completionHandler), revealIdsAndLabels](auto&& devices) mutable {
auto deviceWithCapabilities = map(devices, [revealIdsAndLabels](auto&& device) -> CaptureDeviceWithCapabilities {
RealtimeMediaSourceCapabilities capabilities;
Vector<CaptureDeviceWithCapabilities> devicesWithCapabilities;

devicesWithCapabilities.reserveInitialCapacity(devices.size());
for (auto& device : devices) {
RealtimeMediaSourceCapabilities deviceCapabilities;

auto capabilities = RealtimeMediaSourceCenter::singleton().getCapabilities(device);
if (!capabilities)
continue;

if (revealIdsAndLabels)
capabilities = RealtimeMediaSourceCenter::singleton().getCapabilities(device);
return { WTFMove(device), WTFMove(capabilities) };
});
completionHandler(WTFMove(deviceWithCapabilities));
deviceCapabilities = *capabilities;

devicesWithCapabilities.uncheckedAppend({ WTFMove(device), WTFMove(deviceCapabilities) });
}

completionHandler(WTFMove(devicesWithCapabilities));
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ interface TestRunner {
undefined installFakeHelvetica(DOMString configuration);

undefined addMockCameraDevice(DOMString persistentId, DOMString label, object properties);
undefined addMockMicrophoneDevice(DOMString persistentId, DOMString label);
undefined addMockMicrophoneDevice(DOMString persistentId, DOMString label, object properties);
undefined addMockScreenDevice(DOMString persistentId, DOMString label);
undefined clearMockMediaDevices();
undefined removeMockMediaDevice(DOMString persistentId);
Expand Down
Loading

0 comments on commit f453768

Please sign in to comment.