LibWeb: Add Media Capture and Stream APIs

This commit is contained in:
Jonathan Gamble 2026-02-23 20:21:16 -06:00 committed by Gregory Bertilson
parent d388502a3a
commit baefb51902
35 changed files with 2484 additions and 0 deletions

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibMedia/Audio/AudioDevices.h>
namespace Media {
AudioDevices& AudioDevices::the()
{
static AudioDevices devices;
return devices;
}
void AudioDevices::refresh()
{
}
Vector<AudioDeviceInfo> AudioDevices::input_devices() const
{
return m_cached_input_devices;
}
Vector<AudioDeviceInfo> AudioDevices::output_devices() const
{
return m_cached_output_devices;
}
AudioDevices::ListenerId AudioDevices::add_devices_changed_listener(Function<void()> listener)
{
ListenerId listener_id = m_next_listener_id++;
m_listeners.set(listener_id, move(listener));
return listener_id;
}
void AudioDevices::remove_devices_changed_listener(ListenerId listener_id)
{
m_listeners.remove(listener_id);
}
void AudioDevices::notify_listeners()
{
Vector<ListenerId> listener_ids;
listener_ids.ensure_capacity(m_listeners.size());
for (auto const& listener : m_listeners)
listener_ids.append(listener.key);
for (auto listener_id : listener_ids) {
auto callback = m_listeners.get(listener_id);
if (!callback.has_value())
continue;
callback.value()();
}
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/ByteString.h>
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/Vector.h>
#include <LibMedia/Export.h>
namespace Media {
struct AudioDeviceInfo {
ByteString dom_device_id;
ByteString label;
ByteString group_id;
u32 sample_rate_hz { 0 };
u32 channel_count { 0 };
bool is_default { false };
};
class MEDIA_API AudioDevices {
public:
static AudioDevices& the();
void refresh();
Vector<AudioDeviceInfo> input_devices() const;
Vector<AudioDeviceInfo> output_devices() const;
using ListenerId = u64;
ListenerId add_devices_changed_listener(Function<void()>);
void remove_devices_changed_listener(ListenerId);
private:
void notify_listeners();
Vector<AudioDeviceInfo> m_cached_input_devices;
Vector<AudioDeviceInfo> m_cached_output_devices;
ListenerId m_next_listener_id { 1 };
HashMap<ListenerId, Function<void()>> m_listeners;
};
}

View file

@ -3,6 +3,7 @@ include(audio)
include(ffmpeg)
set(SOURCES
Audio/AudioDevices.cpp
Containers/Matroska/MatroskaDemuxer.cpp
Containers/Matroska/Reader.cpp
IncrementallyPopulatedStream.cpp

View file

@ -796,6 +796,11 @@ set(SOURCES
MathML/MathMLMspaceElement.cpp
MathML/TagNames.cpp
MediaCapabilitiesAPI/MediaCapabilities.cpp
MediaCapture/MediaDeviceInfo.cpp
MediaCapture/MediaDevices.cpp
MediaCapture/MediaStream.cpp
MediaCapture/MediaStreamTrack.cpp
MediaCapture/MediaStreamTrackEvent.cpp
MediaSourceExtensions/BufferedChangeEvent.cpp
MediaSourceExtensions/EventNames.cpp
MediaSourceExtensions/ManagedMediaSource.cpp

View file

@ -95,6 +95,7 @@ enum class HdrMetadataType : u8;
enum class ImageSmoothingQuality : u8;
enum class MediaDecodingType : u8;
enum class MediaEncodingType : u8;
enum class MediaStreamTrackState : u8;
enum class OffscreenRenderingContextId : u8;
enum class ReadableStreamReaderMode : u8;
enum class ReferrerPolicy : u8;
@ -114,6 +115,13 @@ enum class XMLHttpRequestResponseType : u8;
}
namespace Web::MediaCapture {
class MediaStream;
class MediaStreamTrack;
}
namespace Web::Clipboard {
class Clipboard;

View file

@ -47,6 +47,7 @@ namespace Web::HTML::EventNames {
__ENUMERATE_HTML_EVENT(cuechange) \
__ENUMERATE_HTML_EVENT(currententrychange) \
__ENUMERATE_HTML_EVENT(cut) \
__ENUMERATE_HTML_EVENT(devicechange) \
__ENUMERATE_HTML_EVENT(disconnect) \
__ENUMERATE_HTML_EVENT(dispose) \
__ENUMERATE_HTML_EVENT(DOMContentLoaded) \

View file

@ -19,6 +19,7 @@
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Internals/XRTest.h>
#include <LibWeb/Loader/ResourceLoader.h>
#include <LibWeb/MediaCapture/MediaDevices.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/ServiceWorker/ServiceWorkerContainer.h>
#include <LibWeb/WebXR/XRSystem.h>
@ -77,6 +78,7 @@ void Navigator::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_user_activation);
visitor.visit(m_service_worker_container);
visitor.visit(m_media_capabilities);
visitor.visit(m_media_devices);
visitor.visit(m_credentials);
visitor.visit(m_battery_promise);
visitor.visit(m_xr);
@ -163,6 +165,13 @@ GC::Ref<MediaCapabilitiesAPI::MediaCapabilities> Navigator::media_capabilities()
return *m_media_capabilities;
}
GC::Ref<MediaCapture::MediaDevices> Navigator::media_devices()
{
if (!m_media_devices)
m_media_devices = realm().create<MediaCapture::MediaDevices>(realm());
return *m_media_devices;
}
// https://w3c.github.io/battery/#the-getbattery-method
GC::Ref<WebIDL::Promise> Navigator::get_battery()
{

View file

@ -20,6 +20,7 @@
#include <LibWeb/HTML/PluginArray.h>
#include <LibWeb/HTML/UserActivation.h>
#include <LibWeb/MediaCapabilitiesAPI/MediaCapabilities.h>
#include <LibWeb/MediaCapture/MediaDevices.h>
#include <LibWeb/Serial/Serial.h>
#include <LibWeb/StorageAPI/NavigatorStorage.h>
@ -71,6 +72,7 @@ public:
GC::Ref<ServiceWorker::ServiceWorkerContainer> service_worker();
GC::Ref<MediaCapabilitiesAPI::MediaCapabilities> media_capabilities();
GC::Ref<MediaCapture::MediaDevices> media_devices();
static WebIDL::Long max_touch_points();
@ -108,6 +110,9 @@ private:
// https://w3c.github.io/media-capabilities/#dom-navigator-mediacapabilities
GC::Ptr<MediaCapabilitiesAPI::MediaCapabilities> m_media_capabilities;
// https://w3c.github.io/mediacapture-main/#dom-navigator-mediadevices
GC::Ptr<MediaCapture::MediaDevices> m_media_devices;
// https://w3c.github.io/webappsec-credential-management/#framework-credential-management
GC::Ptr<CredentialManagement::CredentialsContainer> m_credentials;

View file

@ -14,6 +14,7 @@
#import <HTML/PluginArray.idl>
#import <HTML/UserActivation.idl>
#import <MediaCapabilitiesAPI/MediaCapabilities.idl>
#import <MediaCapture/MediaDevices.idl>
#import <Serial/Serial.idl>
#import <ServiceWorker/ServiceWorkerContainer.idl>
#import <StorageAPI/NavigatorStorage.idl>
@ -45,6 +46,9 @@ interface Navigator {
// https://w3c.github.io/media-capabilities/#dom-navigator-mediacapabilities
[SameObject] readonly attribute MediaCapabilities mediaCapabilities;
// https://w3c.github.io/mediacapture-main/#dom-navigator-mediadevices
[SecureContext, SameObject] readonly attribute MediaDevices mediaDevices;
// https://w3c.github.io/webappsec-credential-management/#framework-credential-management
[SecureContext, SameObject] readonly attribute CredentialsContainer credentials;

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/MediaDeviceInfoPrototype.h>
#include <LibWeb/MediaCapture/MediaDeviceInfo.h>
namespace Web::MediaCapture {
GC_DEFINE_ALLOCATOR(MediaDeviceInfo);
// https://w3c.github.io/mediacapture-main/#device-info
GC::Ref<MediaDeviceInfo> MediaDeviceInfo::create(JS::Realm& realm, String device_id, Bindings::MediaDeviceKind kind, String label, String group_id)
{
auto device_info = realm.create<MediaDeviceInfo>(realm, move(device_id), kind, move(label), move(group_id));
// AD-HOC: device, mediaDevices, exposure checks handled by the caller.
return device_info;
}
MediaDeviceInfo::MediaDeviceInfo(JS::Realm& realm, String device_id, Bindings::MediaDeviceKind kind, String label, String group_id)
: Bindings::PlatformObject(realm)
, m_device_id(move(device_id))
, m_kind(kind)
, m_label(move(label))
, m_group_id(move(group_id))
{
}
MediaDeviceInfo::~MediaDeviceInfo() = default;
void MediaDeviceInfo::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(MediaDeviceInfo);
Base::initialize(realm);
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/Types.h>
#include <LibWeb/Bindings/PlatformObject.h>
namespace Web::Bindings {
enum class MediaDeviceKind : u8;
}
namespace Web::MediaCapture {
// https://w3c.github.io/mediacapture-main/#device-info
class MediaDeviceInfo final : public Bindings::PlatformObject {
WEB_PLATFORM_OBJECT(MediaDeviceInfo, Bindings::PlatformObject);
GC_DECLARE_ALLOCATOR(MediaDeviceInfo);
public:
[[nodiscard]] static GC::Ref<MediaDeviceInfo> create(JS::Realm&, String device_id, Bindings::MediaDeviceKind kind, String label, String group_id);
virtual ~MediaDeviceInfo() override;
String device_id() const { return m_device_id; }
Bindings::MediaDeviceKind kind() const { return m_kind; }
String label() const { return m_label; }
String group_id() const { return m_group_id; }
private:
MediaDeviceInfo(JS::Realm&, String device_id, Bindings::MediaDeviceKind kind, String label, String group_id);
virtual void initialize(JS::Realm&) override;
String m_device_id;
Bindings::MediaDeviceKind m_kind;
String m_label;
String m_group_id;
};
}

View file

@ -0,0 +1,16 @@
// https://w3c.github.io/mediacapture-main/#dom-mediadevicekind
enum MediaDeviceKind {
"audioinput",
"audiooutput",
"videoinput"
};
// https://w3c.github.io/mediacapture-main/#mediadeviceinfo
[Exposed=Window, SecureContext]
interface MediaDeviceInfo {
readonly attribute DOMString deviceId;
readonly attribute MediaDeviceKind kind;
readonly attribute DOMString label;
readonly attribute DOMString groupId;
[Default] object toJSON();
};

View file

@ -0,0 +1,762 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGC/Root.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/NativeFunction.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/VM.h>
#include <LibMedia/Audio/AudioDevices.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/MediaDeviceInfoPrototype.h>
#include <LibWeb/Bindings/MediaDevicesPrototype.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/IDLEventListener.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/MediaCapture/MediaDeviceInfo.h>
#include <LibWeb/MediaCapture/MediaDevices.h>
#include <LibWeb/MediaCapture/MediaStream.h>
#include <LibWeb/MediaCapture/MediaStreamConstraints.h>
#include <LibWeb/MediaCapture/MediaStreamTrack.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/WebIDL/CallbackType.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::MediaCapture {
static String const AUDIO_INPUT_KIND = "audioinput"_string;
static String const AUDIO_OUTPUT_KIND = "audiooutput"_string;
static String const VIDEO_INPUT_KIND = "videoinput"_string;
static Optional<Vector<String>> extract_device_id_constraint(Optional<ConstrainDOMString> const& device_id_value);
static void resolve_with_device_info_list(JS::Realm& realm, WebIDL::Promise& promise, GC::RootVector<GC::Ref<MediaDeviceInfo>> const& result_list);
GC_DEFINE_ALLOCATOR(MediaDevices);
GC::Ref<MediaDevices> MediaDevices::create(JS::Realm& realm)
{
return realm.create<MediaDevices>(realm);
}
MediaDevices::MediaDevices(JS::Realm& realm)
: DOM::EventTarget(realm)
{
auto pending_request_state_change_callback_function = JS::NativeFunction::create(realm, [media_devices = GC::Ref(*this)](JS::VM&) {
media_devices->process_pending_enumerate_devices_requests();
media_devices->process_pending_get_user_media_requests();
return JS::js_undefined(); }, 0, Utf16FlyString {}, &realm);
auto pending_request_state_change_callback = realm.heap().allocate<WebIDL::CallbackType>(*pending_request_state_change_callback_function, realm);
m_pending_request_state_change_listener = DOM::IDLEventListener::create(realm, pending_request_state_change_callback);
auto& window = as<HTML::Window>(realm.global_object());
window.associated_document().add_event_listener_without_options(HTML::EventNames::visibilitychange, *m_pending_request_state_change_listener);
window.add_event_listener_without_options(HTML::EventNames::focus, *m_pending_request_state_change_listener);
window.add_event_listener_without_options(HTML::EventNames::blur, *m_pending_request_state_change_listener);
m_audio_device_cache_listener_id = Media::AudioDevices::the().add_devices_changed_listener([this] {
did_observe_audio_device_cache_update();
});
did_observe_audio_device_cache_update();
}
void MediaDevices::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(MediaDevices);
Base::initialize(realm);
}
void MediaDevices::finalize()
{
if (m_pending_request_state_change_listener) {
auto& window = as<HTML::Window>(realm().global_object());
window.associated_document().remove_event_listener_without_options(HTML::EventNames::visibilitychange, *m_pending_request_state_change_listener);
window.remove_event_listener_without_options(HTML::EventNames::focus, *m_pending_request_state_change_listener);
window.remove_event_listener_without_options(HTML::EventNames::blur, *m_pending_request_state_change_listener);
}
Base::finalize();
if (m_audio_device_cache_listener_id.has_value())
Media::AudioDevices::the().remove_devices_changed_listener(m_audio_device_cache_listener_id.release_value());
}
void MediaDevices::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_pending_enumerate_devices_promises);
for (auto const& request : m_pending_get_user_media_requests)
visitor.visit(request.promise);
visitor.visit(m_pending_request_state_change_listener);
}
// https://w3c.github.io/mediacapture-main/#device-information-exposure
bool MediaDevices::device_information_can_be_exposed()
{
// 1. If camera information can be exposed on mediaDevices, return true.
if (camera_information_can_be_exposed())
return true;
// 2. If microphone information can be exposed on mediaDevices, return true.
if (microphone_information_can_be_exposed())
return true;
// 3. Return false.
return false;
}
bool MediaDevices::microphone_information_can_be_exposed()
{
// 1. If any of the local devices of kind "audioinput" are attached to a live MediaStreamTrack in
// mediaDevices's relevant global object's associated Document, return true.
if (has_live_device_of_kind(AUDIO_INPUT_KIND))
return true;
// 2. Return mediaDevices.[[canExposeMicrophoneInfo]].
return m_can_expose_microphone_info;
}
bool MediaDevices::camera_information_can_be_exposed() const
{
return has_live_device_of_kind(VIDEO_INPUT_KIND) || m_can_expose_camera_info;
}
bool MediaDevices::can_use_microphone_feature() const
{
auto const& document = as<HTML::Window>(realm().global_object()).associated_document();
return document.is_allowed_to_use_feature(DOM::PolicyControlledFeature::Microphone);
}
bool MediaDevices::can_use_camera_feature() const
{
auto const& document = as<HTML::Window>(realm().global_object()).associated_document();
return document.is_allowed_to_use_feature(DOM::PolicyControlledFeature::Camera);
}
bool MediaDevices::device_enumeration_can_proceed()
{
// 1. The User Agent MAY return true if device information can be exposed on mediaDevices.
if (device_information_can_be_exposed())
return true;
// 2. Return the result of is in view with mediaDevices.
return is_in_view();
}
bool MediaDevices::get_user_media_can_proceed() const
{
return is_in_view() && as<HTML::Window>(realm().global_object()).associated_document().has_focus();
}
bool MediaDevices::is_in_view() const
{
auto const& document = as<HTML::Window>(realm().global_object()).associated_document();
return document.is_fully_active() && document.visibility_state_value() == HTML::VisibilityState::Visible;
}
bool MediaDevices::has_live_device_of_kind(StringView kind) const
{
for (auto const& device : m_stored_device_list) {
if (device.kind != kind)
continue;
auto live = m_devices_live_map.get(device.dom_device_id);
if (live.value_or(false))
return true;
}
return false;
}
// https://w3c.github.io/mediacapture-main/#set-device-information-exposure-0
void MediaDevices::set_device_information_exposure(bool audio_requested, bool video_requested, bool value)
{
// 1. If "video" is in requestedTypes, run the following sub-steps.
if (video_requested) {
// 1.1 Set mediaDevices.[[canExposeCameraInfo]] to value.
m_can_expose_camera_info = value;
// FIXME: 1.2 If value is true and if device exposure can be extended with "microphone", set mediaDevices.[[canExposeMicrophoneInfo]] to true.
}
// 2. If "audio" is in requestedTypes, run the following sub-steps.
if (audio_requested) {
// 2.1 Set mediaDevices.[[canExposeMicrophoneInfo]] to value.
m_can_expose_microphone_info = value;
// FIXME: 2.2 If value is true and if device exposure can be extended with "camera", set mediaDevices.[[canExposeCameraInfo]] to true.
}
process_pending_enumerate_devices_requests();
process_pending_get_user_media_requests();
}
// https://w3c.github.io/mediacapture-main/#dom-mediadevices-getusermedia
void MediaDevices::queue_get_user_media_task(GC::Ref<WebIDL::Promise> promise, Optional<Vector<String>> requested_device_ids)
{
JS::Realm& realm = this->realm();
GC::Ref<MediaDevices> media_devices = *this;
// FIXME: Add camera/video
Vector<Media::AudioDeviceInfo> audio_input_devices = Media::AudioDevices::the().input_devices();
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [promise = GC::Root(promise), media_devices, requested_device_ids = move(requested_device_ids), audio_input_devices = move(audio_input_devices)] mutable {
JS::Realm& realm = HTML::relevant_realm(*promise->promise());
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
auto reject_not_found = [&](Utf16String const& message) {
WebIDL::reject_promise(realm, *promise, WebIDL::NotFoundError::create(realm, message));
};
auto reject_permission_failure = [&] {
WebIDL::reject_promise(realm, *promise, WebIDL::NotAllowedError::create(realm, "Permission denied"_utf16));
};
// 11. Run the following steps in parallel.
// 11.1 While isInView is false, the User Agent MUST wait to proceed to the next step until a task queued to set isInView to the result of the is in view algorithm, would set isInView to true.
// 11.7 While hasSystemFocus is false, the User Agent MUST wait to proceed to the next step until a task queued to set hasSystemFocus to the result of the has system focus algorithm, would set hasSystemFocus to true.
// AD-HOC: Requests stay pending until both conditions are true, then continue here.
// 11.2 Let finalSet be an (initially) empty set.
Vector<Media::AudioDeviceInfo> final_set;
// 11.3 For each media type kind in requestedMediaTypes, run the following steps.
// 11.3.1 For each possible configuration of each possible source device of media of type kind, conceive a candidate as a placeholder for an eventual MediaStreamTrack holding a source device and configured with a settings dictionary comprised of its specific settings.
// Call this set of candidates the candidateSet.
Vector<Media::AudioDeviceInfo> candidate_set = audio_input_devices;
// 11.3.1 If candidateSet is the empty set, jump to the step labeled NotFound Failure below.
if (candidate_set.is_empty()) {
// 11.13.2 Reject p with a new DOMException object whose name attribute has the value "NotFoundError".
reject_not_found("No audio input devices available"_utf16);
return;
}
// 11.3.2 If the value of the kind entry of constraints is true, set CS to the empty constraint set (no constraint). Otherwise, continue with CS set to the value of the kind entry of constraints.
// 11.3.3 Remove any constrainable property inside of CS that are not defined for MediaStreamTrack objects of type kind.
// 11.3.4 If CS contains a member that is a required constraint and whose name is not in the list of allowed required constraints for device selection, then reject p with a TypeError, and abort these steps.
// 11.3.5 Run the SelectSettings algorithm on each candidate in candidateSet with CS as the constraint set.
if (requested_device_ids.has_value() && !requested_device_ids->is_empty()) {
Vector<Media::AudioDeviceInfo> filtered_candidates;
for (auto const& device : candidate_set) {
String device_id = String::from_utf8_with_replacement_character(device.dom_device_id.view());
for (auto const& requested_id : *requested_device_ids) {
if (device_id == requested_id) {
filtered_candidates.append(device);
break;
}
}
}
candidate_set = move(filtered_candidates);
if (candidate_set.is_empty()) {
// FIXME: 11.14.3 Reject p with a new OverconstrainedError created by calling OverconstrainedError(constraint, message).
reject_not_found("Requested audio input device not found"_utf16);
return;
}
}
// FIXME: 11.3.6 Read the current permission state for all candidate devices in candidateSet that are not attached to a live MediaStreamTrack in the current Document. Remove from candidateSet any candidate whose device's permission state is "denied".
// FIXME: 11.3.7 Optionally, e.g., based on a previously-established user preference, for security reasons, or due to platform limitations, jump to the step labeled Permission Failure below.
// 11.3.8 Add all candidates from candidateSet to finalSet.
final_set = move(candidate_set);
// 11.4 Let stream be a new and empty MediaStream object.
GC::Ref<MediaStream> stream = MediaStream::create(realm);
// 11.5 For each media type kind in requestedMediaTypes, run the following sub steps, preferably at the same time.
// FIXME: 11.5.1 Request permission to use a PermissionDescriptor with its name member set to the permission name associated with kind.
// FIXME: 11.5.2 If the result of the request is "denied", jump to the step labeled Permission Failure below.
// 11.8 Set the device information exposure on mediaDevices with requestedMediaTypes and true.
media_devices->set_device_information_exposure(true, false, true);
// 11.9 For each media type kind in requestedMediaTypes, run the following sub steps.
// 11.9.1 Let finalCandidate be the provided media, which MUST be precisely one candidate of type kind from finalSet.
Optional<Media::AudioDeviceInfo> final_candidate;
for (auto const& device : final_set) {
if (!final_candidate.has_value())
final_candidate = device;
if (!device.is_default)
continue;
final_candidate = device;
break;
}
if (!final_candidate.has_value()) {
WebIDL::reject_promise(realm, *promise, WebIDL::NotReadableError::create(realm, "No readable audio input devices available"_utf16));
return;
}
// 11.9.2 The result of the request is "granted".
// 11.9.3 Let grantedDevice be finalCandidate's source device.
Media::AudioDeviceInfo const& granted_device = final_candidate.value();
// 11.9.4 Using grantedDevice's deviceId, deviceId, set mediaDevices.[[devicesLiveMap]][deviceId] to true, if it isn't already true, and set mediaDevices.[[devicesAccessibleMap]][deviceId] to true, if it isn't already true.
String granted_device_id = String::from_utf8_with_replacement_character(granted_device.dom_device_id.view());
media_devices->m_devices_live_map.set(granted_device_id, true);
media_devices->m_devices_accessible_map.set(granted_device_id, true);
// 11.9.5 Let track be the result of creating a MediaStreamTrack with grantedDevice and mediaDevices. The source of the MediaStreamTrack MUST NOT change.
GC::Ref<MediaStreamTrack> track = MediaStreamTrack::create(realm,
Bindings::MediaStreamTrackKind::Audio,
String::from_utf8_with_replacement_character(granted_device.label.view()));
MediaTrackSettings settings = track->get_settings();
settings.device_id = granted_device_id;
settings.sample_rate = granted_device.sample_rate_hz;
settings.channel_count = granted_device.channel_count;
track->set_settings(move(settings));
// 11.9.6 Add track to stream's track set.
stream->add_track(track);
// FIXME: 11.10 Run the ApplyConstraints algorithm on all tracks in stream with the appropriate constraints.
// 11.11 For each track in stream, tie track source to MediaDevices with track.[[Source]] and mediaDevices.
media_devices->m_media_stream_track_sources.set(track->provider_id());
// 11.12 Resolve p with stream and abort these steps.
WebIDL::resolve_promise(realm, *promise, stream);
// 11.15 Permission Failure: Reject p with a new DOMException object whose name attribute has the value "NotAllowedError".
(void)reject_permission_failure;
}));
}
// https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices
GC::Ref<WebIDL::Promise> MediaDevices::enumerate_devices()
{
JS::Realm& realm = this->realm();
// 1. Let p be a new promise.
GC::Ref<WebIDL::Promise> promise = WebIDL::create_promise(realm);
m_stored_device_list = current_audio_device_snapshot();
// 2. Let proceed be the result of device enumeration can proceed with this.
if (!device_enumeration_can_proceed()) {
m_pending_enumerate_devices_promises.append(promise);
return promise;
}
// 3. Let mediaDevices be this.
// 4. Run the following steps in parallel.
queue_enumerate_devices_task(promise);
// 5. Return p.
return promise;
}
// https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices
void MediaDevices::queue_enumerate_devices_task(GC::Ref<WebIDL::Promise> promise)
{
JS::Realm& realm = this->realm();
GC::Ref<MediaDevices> media_devices = *this;
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [promise = GC::Root(promise), media_devices] {
JS::Realm& realm = HTML::relevant_realm(*promise->promise());
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
auto result_list = media_devices->create_list_of_device_info_objects(media_devices->m_stored_device_list);
resolve_with_device_info_list(realm, *promise, result_list);
}));
}
// https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices
GC::RootVector<GC::Ref<MediaDeviceInfo>> MediaDevices::create_list_of_device_info_objects(Vector<StoredDevice> const& device_list)
{
JS::Realm& realm = this->realm();
// To perform creating a list of device info objects, given mediaDevices and deviceList, run the following steps:
// 1. Let resultList be an empty list.
GC::RootVector<GC::Ref<MediaDeviceInfo>> result_list { heap() };
// 2. Let microphoneList, cameraList and otherDeviceList be empty lists.
GC::RootVector<GC::Ref<MediaDeviceInfo>> microphone_list { heap() };
GC::RootVector<GC::Ref<MediaDeviceInfo>> camera_list { heap() };
GC::RootVector<GC::Ref<MediaDeviceInfo>> other_device_list { heap() };
// 3. Let document be mediaDevices's relevant global object's associated Document.
auto const& document = as<HTML::Window>(realm.global_object()).associated_document();
(void)document;
auto create_device_info_object = [&](StoredDevice const& device) -> Optional<GC::Ref<MediaDeviceInfo>> {
Optional<Bindings::MediaDeviceKind> kind;
if (device.kind == "audioinput"sv)
kind = Bindings::MediaDeviceKind::Audioinput;
else if (device.kind == "audiooutput"sv)
kind = Bindings::MediaDeviceKind::Audiooutput;
else
return {};
String device_id = device.dom_device_id;
String label = device.label;
String group_id = device.group_id;
if (device.kind == "audioinput"sv && !microphone_information_can_be_exposed()) {
device_id = String {};
label = String {};
group_id = String {};
}
// FIXME: Implement videoinput creation when camera enumeration is available.
return MediaDeviceInfo::create(realm, move(device_id), *kind, move(label), move(group_id));
};
// 4. Run the following sub steps for each discovered device in deviceList, device.
for (auto const& device : device_list) {
// 4.1 If device is not a microphone, or document is not allowed to use the feature identified by microphone, abort these sub steps and continue with the next device.
if (device.kind != AUDIO_INPUT_KIND)
continue;
if (!can_use_microphone_feature())
continue;
// 4.2 Let deviceInfo be the result of creating a device info object to represent device, with mediaDevices.
Optional<GC::Ref<MediaDeviceInfo>> device_info = create_device_info_object(device);
if (!device_info.has_value())
continue;
// 4.3 If device is the system default microphone, prepend deviceInfo to microphoneList. Otherwise, append deviceInfo to microphoneList.
if (device.is_default)
microphone_list.insert(0, *device_info);
else
microphone_list.append(*device_info);
}
// 5. Run the following sub steps for each discovered device in deviceList, device.
for (auto const& device : device_list) {
// 5.1 If device is not a camera, or document is not allowed to use the feature identified by camera, abort these sub steps and continue with the next device.
if (device.kind != VIDEO_INPUT_KIND)
continue;
if (!can_use_camera_feature())
continue;
// 5.2 Let deviceInfo be the result of creating a device info object to represent device, with mediaDevices.
Optional<GC::Ref<MediaDeviceInfo>> device_info = create_device_info_object(device);
if (!device_info.has_value())
continue;
// 5.3 If device is the system default camera, prepend deviceInfo to cameraList. Otherwise, append deviceInfo to cameraList.
if (device.is_default)
camera_list.insert(0, *device_info);
else
camera_list.append(*device_info);
}
// 6. If microphone information can be exposed on mediaDevices is false, truncate microphoneList to its first item.
if (!microphone_information_can_be_exposed()) {
while (microphone_list.size() > 1)
microphone_list.take_last();
}
// 7. If camera information can be exposed on mediaDevices is false, truncate cameraList to its first item.
if (!camera_information_can_be_exposed()) {
while (camera_list.size() > 1)
camera_list.take_last();
}
// 8. Run the following sub steps for each discovered device in deviceList, device.
for (auto const& device : device_list) {
// 8.1 If device is a microphone or device is a camera, abort these sub steps and continue with the next device.
if (device.kind == AUDIO_INPUT_KIND || device.kind == VIDEO_INPUT_KIND)
continue;
// 8.2 Run the exposure decision algorithm for devices other than camera and microphone.
bool expose_other_device = device_information_can_be_exposed();
// FIXME: Implement the exact exposure decision algorithm from the relevant output-device spec.
if (!expose_other_device)
continue;
// 8.3 Let deviceInfo be the result of creating a device info object to represent device, with mediaDevices.
Optional<GC::Ref<MediaDeviceInfo>> device_info = create_device_info_object(device);
if (!device_info.has_value())
continue;
// 8.4 Append deviceInfo to otherDeviceList.
// 8.5 If device is the system default audio output, prepend it instead.
if (device.kind == "audiooutput"sv && device.is_default) {
// AD-HOC: 8.5.x Label & id in the LibMedia audio device cache are only mutated in response
// to OS notifications. The device manager updates the DOM id & label in the event of changes,
// and these will be broadcast to the LibMedia cache. We don't set that state here.
other_device_list.insert(0, *device_info);
} else {
other_device_list.append(*device_info);
}
}
// 9. Append to resultList all devices of microphoneList in order.
result_list.extend(microphone_list);
// 10. Append to resultList all devices of cameraList in order.
result_list.extend(camera_list);
// 11. Append to resultList all devices of otherDeviceList in order.
result_list.extend(other_device_list);
// 12. Return resultList.
return result_list;
}
void MediaDevices::process_pending_enumerate_devices_requests()
{
if (!device_enumeration_can_proceed())
return;
run_device_change_notification_steps(current_audio_device_snapshot());
auto pending_promises = move(m_pending_enumerate_devices_promises);
for (auto& promise : pending_promises)
queue_enumerate_devices_task(promise);
}
void MediaDevices::process_pending_get_user_media_requests()
{
if (!get_user_media_can_proceed())
return;
auto pending_requests = move(m_pending_get_user_media_requests);
for (auto& request : pending_requests)
queue_get_user_media_task(request.promise, move(request.requested_device_ids));
}
// https://w3c.github.io/mediacapture-main/#dfn-device-change-notification-steps
void MediaDevices::run_device_change_notification_steps(Vector<StoredDevice> const& device_list)
{
// When new media input and/or output devices are made available to the User Agent, or any available
// input and/or output device becomes unavailable, or the system default for input and/or output
// devices of a MediaDeviceKind changed, the User Agent MUST run the following device change
// notification steps for each MediaDevices object, mediaDevices, for which device enumeration can
// proceed is true, but for no other MediaDevices object:
if (!device_enumeration_can_proceed())
return;
// 1. Let lastExposedDevices be the result of creating a list of device info objects with mediaDevices and mediaDevices.[[storedDeviceList]].
GC::RootVector<GC::Ref<MediaDeviceInfo>> last_exposed_devices = create_list_of_device_info_objects(m_stored_device_list);
// 2. Let deviceList be the list of all media input and/or output devices available to the User Agent.
// device_list is the caller-provided snapshot.
// 3. Let newExposedDevices be the result of creating a list of device info objects with mediaDevices and deviceList.
GC::RootVector<GC::Ref<MediaDeviceInfo>> new_exposed_devices = create_list_of_device_info_objects(device_list);
// 4. If the MediaDeviceInfo objects in newExposedDevices match those in lastExposedDevices and have
// the same order, then abort these steps.
if (new_exposed_devices.size() == last_exposed_devices.size()) {
bool exposed_devices_match = true;
for (size_t i = 0; i < new_exposed_devices.size(); ++i) {
if (new_exposed_devices[i]->kind() != last_exposed_devices[i]->kind()
|| new_exposed_devices[i]->device_id() != last_exposed_devices[i]->device_id()
|| new_exposed_devices[i]->label() != last_exposed_devices[i]->label()
|| new_exposed_devices[i]->group_id() != last_exposed_devices[i]->group_id()) {
exposed_devices_match = false;
break;
}
}
if (exposed_devices_match)
return;
}
// 5. Set mediaDevices.[[storedDeviceList]] to deviceList.
m_stored_device_list = device_list;
// 6. Queue a task that fires an event named devicechange, using the DeviceChangeEvent constructor
// with devices initialized to newExposedDevices, at mediaDevices.
// FIXME: Dispatch DeviceChangeEvent with devices once DeviceChangeEvent is implemented in LibWeb.
HTML::queue_global_task(HTML::Task::Source::DOMManipulation, HTML::relevant_global_object(*this), GC::create_function(heap(), [media_devices = GC::Ref(*this)] {
media_devices->dispatch_event(DOM::Event::create(media_devices->realm(), HTML::EventNames::devicechange));
}));
}
// https://w3c.github.io/mediacapture-main/#dom-mediadevices-getsupportedconstraints
MediaTrackSupportedConstraints MediaDevices::get_supported_constraints()
{
// Returns a dictionary whose members are the constrainable properties known to the User Agent.
MediaTrackSupportedConstraints supported_constraints;
supported_constraints.device_id = true;
return supported_constraints;
}
// https://w3c.github.io/mediacapture-main/#dom-mediadevices-getusermedia
GC::Ref<WebIDL::Promise> MediaDevices::get_user_media(Optional<MediaStreamConstraints> const& constraints)
{
JS::Realm& realm = this->realm();
JS::VM& vm = realm.vm();
bool audio_requested = false;
bool video_requested = false;
Optional<Vector<String>> requested_device_ids;
// 1. Let constraints be the method's first argument.
if (!constraints.has_value())
return WebIDL::create_rejected_promise_from_exception(realm, vm.throw_completion<JS::TypeError>("getUserMedia requires constraints"sv));
// 2. Let requestedMediaTypes be the set of media types in constraints with either a dictionary value or a value of true.
auto const& constraints_value = constraints.value();
auto const& audio_value = constraints_value.audio;
if (audio_value.has<bool>()) {
audio_requested = audio_value.get<bool>();
} else {
audio_requested = true;
auto const& audio_constraints = audio_value.get<MediaTrackConstraints>();
requested_device_ids = extract_device_id_constraint(audio_constraints.device_id);
}
auto const& video_value = constraints_value.video;
if (video_value.has<bool>())
video_requested = video_value.get<bool>();
else
video_requested = true;
// 3. If requestedMediaTypes is the empty set, return a promise rejected with a TypeError.
if (!audio_requested && !video_requested)
return WebIDL::create_rejected_promise_from_exception(realm, vm.throw_completion<JS::TypeError>("No media types requested"sv));
// 4. Let document be the relevant global object's associated Document.
auto const& document = as<HTML::Window>(realm.global_object()).associated_document();
// 5. If document is NOT fully active, return a promise rejected with an InvalidStateError.
if (!document.is_fully_active())
return WebIDL::create_rejected_promise(realm, WebIDL::InvalidStateError::create(realm, "Document is not fully active"_utf16));
// 6. If requestedMediaTypes contains "audio" and document is not allowed to use the feature identified by the "microphone" permission name, jump to Permission Failure.
// FIXME: Do microphone permission policy checks once PolicyControlledFeature includes microphone.
// 7. If requestedMediaTypes contains "video" and document is not allowed to use the feature identified by the "camera" permission name, jump to Permission Failure.
// FIXME: Do camera permission policy checks once PolicyControlledFeature includes camera.
// 8. Let mediaDevices be this.
// 9. Let isInView be the result of the is in view algorithm.
// 10. Let p be a new promise.
if (video_requested)
return WebIDL::create_rejected_promise(realm, WebIDL::NotSupportedError::create(realm, "Video capture is not supported"_utf16));
GC::Ref<WebIDL::Promise> promise = WebIDL::create_promise(realm);
// 11. Run the following steps in parallel.
// AD-HOC: Keep the request queued until isInView and hasSystemFocus allow the deferred task
// to continue without spinning in the event loop.
if (!get_user_media_can_proceed()) {
m_pending_get_user_media_requests.append({ .promise = promise, .requested_device_ids = move(requested_device_ids) });
return promise;
}
queue_get_user_media_task(promise, move(requested_device_ids));
// Return p.
return promise;
}
// https://w3c.github.io/mediacapture-main/#dom-mediadevices-ondevicechange
WebIDL::CallbackType* MediaDevices::ondevicechange()
{
return event_handler_attribute(HTML::EventNames::devicechange);
}
void MediaDevices::set_ondevicechange(WebIDL::CallbackType* event_handler)
{
set_event_handler_attribute(HTML::EventNames::devicechange, event_handler);
}
Vector<MediaDevices::StoredDevice> MediaDevices::current_audio_device_snapshot()
{
Vector<StoredDevice> stored_devices;
auto input_devices = Media::AudioDevices::the().input_devices();
auto output_devices = Media::AudioDevices::the().output_devices();
stored_devices.ensure_capacity(input_devices.size() + output_devices.size());
for (auto const& device : input_devices) {
stored_devices.append(StoredDevice {
.dom_device_id = String::from_utf8_with_replacement_character(device.dom_device_id.view()),
.kind = AUDIO_INPUT_KIND,
.label = String::from_utf8_with_replacement_character(device.label.view()),
.group_id = String::from_utf8_with_replacement_character(device.group_id.view()),
.is_default = device.is_default,
});
}
for (auto const& device : output_devices) {
stored_devices.append(StoredDevice {
.dom_device_id = String::from_utf8_with_replacement_character(device.dom_device_id.view()),
.kind = AUDIO_OUTPUT_KIND,
.label = String::from_utf8_with_replacement_character(device.label.view()),
.group_id = String::from_utf8_with_replacement_character(device.group_id.view()),
.is_default = device.is_default,
});
}
return stored_devices;
}
void MediaDevices::did_observe_audio_device_cache_update()
{
process_pending_enumerate_devices_requests();
process_pending_get_user_media_requests();
}
static Optional<Vector<String>> dom_string_values_from_variant(Variant<String, Vector<String>> const& value)
{
Vector<String> values;
if (value.has<String>()) {
auto const& string_value = value.get<String>();
if (!string_value.is_empty())
values.append(string_value);
} else {
for (auto const& entry : value.get<Vector<String>>()) {
if (!entry.is_empty())
values.append(entry);
}
}
return values.is_empty() ? Optional<Vector<String>> {} : Optional<Vector<String>> { move(values) };
}
static Optional<Vector<String>> extract_dom_string_constraint_values(ConstrainDOMString const& constraint)
{
// https://w3c.github.io/mediacapture-main/#dom-constraindomstring
// ConstrainDOMString is (DOMString or sequence<DOMString> or ConstrainDOMStringParameters).
// https://w3c.github.io/mediacapture-main/#dom-constraindomstringparameters
// ConstrainDOMStringParameters members:
// - exact: (DOMString or sequence<DOMString>) The exact required value for this property.
// - ideal: (DOMString or sequence<DOMString>) The ideal (target) value for this property.
// https://w3c.github.io/mediacapture-main/#constraint-types
// List values MUST be interpreted as disjunctions.
if (constraint.has<ConstrainDOMStringParameters>()) {
auto const& parameters = constraint.get<ConstrainDOMStringParameters>();
if (parameters.exact.has_value())
return dom_string_values_from_variant(*parameters.exact);
if (parameters.ideal.has_value())
return dom_string_values_from_variant(*parameters.ideal);
return {};
}
if (constraint.has<String>())
return dom_string_values_from_variant(Variant<String, Vector<String>> { constraint.get<String>() });
return dom_string_values_from_variant(Variant<String, Vector<String>> { constraint.get<Vector<String>>() });
}
static Optional<Vector<String>> extract_device_id_constraint(Optional<ConstrainDOMString> const& device_id_value)
{
if (!device_id_value.has_value())
return {};
return extract_dom_string_constraint_values(*device_id_value);
}
static void resolve_with_device_info_list(JS::Realm& realm, WebIDL::Promise& promise, GC::RootVector<GC::Ref<MediaDeviceInfo>> const& result_list)
{
GC::Ref<JS::Array> array = MUST(JS::Array::create(realm, result_list.size()));
for (size_t index = 0; index < result_list.size(); ++index) {
JS::PropertyKey property_index { index };
if (array->create_data_property(property_index, result_list[index]).is_error()) {
WebIDL::reject_promise(realm, promise, WebIDL::OperationError::create(realm, "Failed to build enumerateDevices result"_utf16));
return;
}
}
WebIDL::resolve_promise(realm, promise, array);
}
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashMap.h>
#include <AK/HashTable.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibGC/Forward.h>
#include <LibGC/Root.h>
#include <LibJS/Forward.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/DOM/IDLEventListener.h>
#include <LibWeb/MediaCapture/MediaStreamConstraints.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::MediaCapture {
class MediaDeviceInfo;
// https://w3c.github.io/mediacapture-main/#mediadevices
class MediaDevices final : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(MediaDevices, DOM::EventTarget);
GC_DECLARE_ALLOCATOR(MediaDevices);
public:
static constexpr bool OVERRIDES_FINALIZE = true;
[[nodiscard]] static GC::Ref<MediaDevices> create(JS::Realm&);
GC::Ref<WebIDL::Promise> enumerate_devices();
MediaTrackSupportedConstraints get_supported_constraints();
GC::Ref<WebIDL::Promise> get_user_media(Optional<MediaStreamConstraints> const& constraints = {});
void set_ondevicechange(WebIDL::CallbackType* event_handler);
WebIDL::CallbackType* ondevicechange();
private:
struct StoredDevice final {
String dom_device_id;
String kind;
String label;
String group_id;
bool is_default { false };
};
struct PendingGetUserMediaRequest final {
GC::Ref<WebIDL::Promise> promise;
Optional<Vector<String>> requested_device_ids;
};
explicit MediaDevices(JS::Realm&);
virtual void initialize(JS::Realm&) override;
virtual void finalize() override;
bool microphone_information_can_be_exposed();
bool can_use_microphone_feature() const;
bool can_use_camera_feature() const;
bool device_information_can_be_exposed();
bool device_enumeration_can_proceed();
bool get_user_media_can_proceed() const;
bool is_in_view() const;
bool has_live_device_of_kind(StringView kind) const;
void set_device_information_exposure(bool audio_requested, bool video_requested, bool value);
void queue_enumerate_devices_task(GC::Ref<WebIDL::Promise>);
void queue_get_user_media_task(GC::Ref<WebIDL::Promise>, Optional<Vector<String>> requested_device_ids);
void process_pending_enumerate_devices_requests();
void process_pending_get_user_media_requests();
GC::RootVector<GC::Ref<MediaDeviceInfo>> create_list_of_device_info_objects(Vector<StoredDevice> const& device_list);
void run_device_change_notification_steps(Vector<StoredDevice> const& device_list);
static Vector<StoredDevice> current_audio_device_snapshot();
void did_observe_audio_device_cache_update();
virtual void visit_edges(Cell::Visitor&) override;
// https://w3c.github.io/mediacapture-main/#mediadevices
// [[devicesLiveMap]]
HashMap<String, bool> m_devices_live_map;
// [[devicesAccessibleMap]]
HashMap<String, bool> m_devices_accessible_map;
// [[storedDeviceList]]
Vector<StoredDevice> m_stored_device_list;
// [[canExposeCameraInfo]]
bool m_can_expose_camera_info { false };
bool camera_information_can_be_exposed() const;
// [[canExposeMicrophoneInfo]]
bool m_can_expose_microphone_info { false };
// [[mediaStreamTrackSources]]
// FIXME: Replace provider IDs with concrete source objects when MediaCapture source modeling lands.
HashTable<u64> m_media_stream_track_sources;
Vector<GC::Ref<WebIDL::Promise>> m_pending_enumerate_devices_promises;
Vector<PendingGetUserMediaRequest> m_pending_get_user_media_requests;
GC::Ptr<DOM::IDLEventListener> m_pending_request_state_change_listener;
Optional<u64> m_audio_device_cache_listener_id;
};
}

View file

@ -0,0 +1,16 @@
#import <DOM/EventHandler.idl>
#import <DOM/EventTarget.idl>
#import <MediaCapture/MediaDeviceInfo.idl>
#import <MediaCapture/MediaStream.idl>
#import <MediaCapture/MediaStreamConstraints.idl>
// https://w3c.github.io/mediacapture-main/#mediadevices
[Exposed=Window, SecureContext]
interface MediaDevices : EventTarget {
attribute EventHandler ondevicechange;
Promise<sequence<MediaDeviceInfo>> enumerateDevices();
// https://w3c.github.io/mediacapture-main/#dom-mediadevices-getsupportedconstraints
// Returns a dictionary whose members are the constrainable properties known to the User Agent.
MediaTrackSupportedConstraints getSupportedConstraints();
Promise<MediaStream> getUserMedia(optional MediaStreamConstraints constraints = {});
};

View file

@ -0,0 +1,196 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/MediaStreamPrototype.h>
#include <LibWeb/Bindings/MediaStreamTrackPrototype.h>
#include <LibWeb/Crypto/Crypto.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/MediaCapture/MediaStream.h>
#include <LibWeb/MediaCapture/MediaStreamTrackEvent.h>
namespace Web::MediaCapture {
GC_DEFINE_ALLOCATOR(MediaStream);
MediaStream::MediaStream(JS::Realm& realm)
: DOM::EventTarget(realm)
{
}
GC::Ref<MediaStream> MediaStream::create(JS::Realm& realm)
{
auto stream = realm.create<MediaStream>(realm);
// https://w3c.github.io/mediacapture-main/#dom-mediastream-id
stream->m_id = Crypto::generate_random_uuid();
return stream;
}
// https://w3c.github.io/mediacapture-main/#mediastream
GC::Ref<MediaStream> MediaStream::construct_impl(JS::Realm& realm, GC::RootVector<GC::Root<MediaStreamTrack>> const& tracks)
{
// 1. Let stream be a newly constructed MediaStream object.
// 2. Initialize stream.id attribute to a newly generated value.
auto stream = create(realm);
// 3. If the constructor's argument is present, run the following steps:
// 3.1. Construct a set of tracks tracks based on the type of argument.
// 3.2. For each MediaStreamTrack, track, in tracks:
for (auto const& track : tracks) {
// 3.2.1. If track is already in stream's track set, skip track.
// 3.2.2. Otherwise, add track to stream's track set.
stream->add_track(*track);
}
// 4. Return stream.
return stream;
}
void MediaStream::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(MediaStream);
Base::initialize(realm);
}
void MediaStream::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
for (auto& track : m_tracks)
visitor.visit(track);
}
// https://w3c.github.io/mediacapture-main/#dom-mediastream-getaudiotracks
Vector<GC::Ref<MediaStreamTrack>> MediaStream::get_audio_tracks() const
{
// The getAudioTracks method MUST return a sequence that represents a snapshot of all the MediaStreamTrack objects in this stream's track set whose [[Kind]] is equal to "audio".
Vector<GC::Ref<MediaStreamTrack>> result;
for (auto const& track : m_tracks) {
if (track->is_audio())
result.append(track);
}
return result;
}
// https://w3c.github.io/mediacapture-main/#dom-mediastream-getvideotracks
Vector<GC::Ref<MediaStreamTrack>> MediaStream::get_video_tracks() const
{
// The getVideoTracks method MUST return a sequence that represents a snapshot of all the MediaStreamTrack objects in this stream's track set whose [[Kind]] is equal to "video".
Vector<GC::Ref<MediaStreamTrack>> result;
for (auto const& track : m_tracks) {
if (track->is_video())
result.append(track);
}
return result;
}
// https://w3c.github.io/mediacapture-main/#dom-mediastream-gettracks
Vector<GC::Ref<MediaStreamTrack>> MediaStream::get_tracks() const
{
// The getTracks method MUST return a sequence that represents a snapshot of all the MediaStreamTrack objects in this stream's track set, regardless of [[Kind]].
return m_tracks;
}
// https://w3c.github.io/mediacapture-main/#dom-mediastream-gettrackbyid
GC::Ptr<MediaStreamTrack> MediaStream::get_track_by_id(String const& track_id) const
{
// The getTrackById method MUST return either a MediaStreamTrack object from this stream's track set whose [[Id]] is equal to trackId, or null.
for (auto const& track : m_tracks) {
if (track->id() == track_id)
return track;
}
return nullptr;
}
// https://w3c.github.io/mediacapture-main/#dom-mediastream-addtrack
void MediaStream::add_track(GC::Ref<MediaStreamTrack> track)
{
// 1. Let track be the methods argument and stream the MediaStream object on which the method was called.
for (auto const& existing_track : m_tracks) {
// 2. If track is already in stream's track set, then abort these steps.
if (existing_track.ptr() == track.ptr())
return;
}
// 3. Add track to stream's track set.
m_tracks.append(track);
// 4. Fire a track event named addtrack with track at stream.
MediaStreamTrackEventInit event_init {};
event_init.track = track;
auto event = MediaStreamTrackEvent::create(realm(), HTML::EventNames::addtrack, event_init);
dispatch_event(event);
}
// https://w3c.github.io/mediacapture-main/#dom-mediastream-removetrack
void MediaStream::remove_track(GC::Ref<MediaStreamTrack> track)
{
// 1. Let track be the methods argument and stream the MediaStream object on which the method was called.
// 2. If track is not in stream's track set, then abort these steps.
// 3. Remove track from stream's track set.
auto removed = m_tracks.remove_first_matching([&](auto const& existing_track) {
return existing_track.ptr() == track.ptr();
});
if (!removed)
return;
// 4. Fire a track event named removetrack with track at stream.
MediaStreamTrackEventInit event_init;
event_init.track = track;
auto event = MediaStreamTrackEvent::create(realm(), HTML::EventNames::removetrack, event_init);
dispatch_event(event);
}
// https://w3c.github.io/mediacapture-main/#dom-mediastream-clone
GC::Ref<MediaStream> MediaStream::clone() const
{
// 1. Let streamClone be a newly constructed MediaStream object.
auto stream_clone = create(realm());
// 3. Clone each track in this MediaStream object and add the result to streamClone's track set.
for (auto const& track : m_tracks)
stream_clone->add_track(track->clone());
// 4. Return streamClone.
return stream_clone;
}
// https://w3c.github.io/mediacapture-main/#dom-mediastream-active
bool MediaStream::active() const
{
// The active attribute MUST return true if this MediaStream is active and false otherwise.
for (auto const& track : m_tracks) {
if (track->ready_state() != Bindings::MediaStreamTrackState::Ended)
return true;
}
return false;
}
// https://w3c.github.io/mediacapture-main/#dom-mediastream-onaddtrack
WebIDL::CallbackType* MediaStream::onaddtrack()
{
return event_handler_attribute(HTML::EventNames::addtrack);
}
void MediaStream::set_onaddtrack(WebIDL::CallbackType* event_handler)
{
// The event type of this event handler is addtrack.
set_event_handler_attribute(HTML::EventNames::addtrack, event_handler);
}
// https://w3c.github.io/mediacapture-main/#dom-mediastream-onremovetrack
WebIDL::CallbackType* MediaStream::onremovetrack()
{
return event_handler_attribute(HTML::EventNames::removetrack);
}
void MediaStream::set_onremovetrack(WebIDL::CallbackType* event_handler)
{
// The event type of this event handler is removetrack.
set_event_handler_attribute(HTML::EventNames::removetrack, event_handler);
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Vector.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/Forward.h>
#include <LibWeb/MediaCapture/MediaStreamTrack.h>
namespace Web::MediaCapture {
// Spec: https://w3c.github.io/mediacapture-main/#mediastream
class MediaStream final : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(MediaStream, DOM::EventTarget);
GC_DECLARE_ALLOCATOR(MediaStream);
public:
static GC::Ref<MediaStream> create(JS::Realm&);
static GC::Ref<MediaStream> construct_impl(JS::Realm&, GC::RootVector<GC::Root<MediaStreamTrack>> const& tracks);
virtual ~MediaStream() override = default;
String id() const { return m_id; }
Vector<GC::Ref<MediaStreamTrack>> get_audio_tracks() const;
Vector<GC::Ref<MediaStreamTrack>> get_video_tracks() const;
Vector<GC::Ref<MediaStreamTrack>> get_tracks() const;
GC::Ptr<MediaStreamTrack> get_track_by_id(String const& track_id) const;
void add_track(GC::Ref<MediaStreamTrack> track);
void remove_track(GC::Ref<MediaStreamTrack> track);
GC::Ref<MediaStream> clone() const;
bool active() const;
void set_onaddtrack(WebIDL::CallbackType* event_handler);
WebIDL::CallbackType* onaddtrack();
void set_onremovetrack(WebIDL::CallbackType* event_handler);
WebIDL::CallbackType* onremovetrack();
private:
explicit MediaStream(JS::Realm&);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
String m_id;
Vector<GC::Ref<MediaStreamTrack>> m_tracks;
};
}

View file

@ -0,0 +1,21 @@
#import <DOM/EventHandler.idl>
#import <DOM/EventTarget.idl>
#import <MediaCapture/MediaStreamTrack.idl>
#import <MediaCapture/MediaStreamTrackEvent.idl>
// https://w3c.github.io/mediacapture-main/#mediastream
[Exposed=Window]
interface MediaStream : EventTarget {
constructor(optional sequence<MediaStreamTrack> tracks = []);
readonly attribute DOMString id;
sequence<MediaStreamTrack> getAudioTracks();
sequence<MediaStreamTrack> getVideoTracks();
sequence<MediaStreamTrack> getTracks();
MediaStreamTrack? getTrackById(DOMString trackId);
undefined addTrack(MediaStreamTrack track);
undefined removeTrack(MediaStreamTrack track);
MediaStream clone();
readonly attribute boolean active;
attribute EventHandler onaddtrack;
attribute EventHandler onremovetrack;
};

View file

@ -0,0 +1,198 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibWeb/WebIDL/Types.h>
namespace Web::MediaCapture {
// https://w3c.github.io/mediacapture-main/#dom-constraindomstringparameters
struct ConstrainDOMStringParameters {
// The exact required value for this property.
Optional<Variant<String, Vector<String>>> exact;
// The ideal (target) value for this property.
Optional<Variant<String, Vector<String>>> ideal;
};
// https://w3c.github.io/mediacapture-main/#dom-constraindoublerange
struct DoubleRange {
// The maximum valid value of this property.
Optional<double> max;
// The minimum value of this property.
Optional<double> min;
};
// https://w3c.github.io/mediacapture-main/#dom-constraindoublerange
struct ConstrainDoubleRange : DoubleRange {
// The exact required value for this property.
Optional<double> exact;
// The ideal (target) value for this property.
Optional<double> ideal;
};
// https://w3c.github.io/mediacapture-main/#dom-constrainulongrange
struct ULongRange {
// The maximum valid value of this property.
Optional<WebIDL::UnsignedLong> max;
// The minimum value of this property.
Optional<WebIDL::UnsignedLong> min;
};
// https://w3c.github.io/mediacapture-main/#dom-constrainulongrange
struct ConstrainULongRange : ULongRange {
// The exact required value for this property.
Optional<WebIDL::UnsignedLong> exact;
// The ideal (target) value for this property.
Optional<WebIDL::UnsignedLong> ideal;
};
// https://w3c.github.io/mediacapture-main/#dom-constrainbooleanparameters
struct ConstrainBooleanParameters {
// The exact required value for this property.
Optional<bool> exact;
// The ideal (target) value for this property.
Optional<bool> ideal;
};
// https://w3c.github.io/mediacapture-main/#dom-constrainbooleanordomstringparameters
struct ConstrainBooleanOrDOMStringParameters {
// The exact required value for this property.
Optional<Variant<bool, String>> exact;
// The ideal (target) value for this property.
Optional<Variant<bool, String>> ideal;
};
// https://w3c.github.io/mediacapture-main/#dom-constrainulong
// Throughout this specification, the identifier ConstrainULong is used to refer to the
// (unsigned long or ConstrainULongRange) type.
using ConstrainULong = Variant<WebIDL::UnsignedLong, ConstrainULongRange>;
// https://w3c.github.io/mediacapture-main/#dom-constraindouble
// Throughout this specification, the identifier ConstrainDouble is used to refer to the
// (double or ConstrainDoubleRange) type.
using ConstrainDouble = Variant<double, ConstrainDoubleRange>;
// https://w3c.github.io/mediacapture-main/#dom-constrainboolean
// Throughout this specification, the identifier ConstrainBoolean is used to refer to the
// (boolean or ConstrainBooleanParameters) type.
using ConstrainBoolean = Variant<bool, ConstrainBooleanParameters>;
// https://w3c.github.io/mediacapture-main/#dom-constraindomstring
// Throughout this specification, the identifier ConstrainDOMString is used to refer to the
// (DOMString or sequence<DOMString> or ConstrainDOMStringParameters) type.
using ConstrainDOMString = Variant<String, Vector<String>, ConstrainDOMStringParameters>;
// https://w3c.github.io/mediacapture-main/#dom-constrainbooleanordomstring
// Throughout this specification, the identifier ConstrainBooleanOrDOMString is used to refer to the
// (boolean or DOMString or ConstrainBooleanOrDOMStringParameters) type.
using ConstrainBooleanOrDOMString = Variant<bool, String, ConstrainBooleanOrDOMStringParameters>;
// https://w3c.github.io/mediacapture-main/#dictdef-mediatrackconstraintset
struct MediaTrackConstraintSet {
Optional<ConstrainULong> width;
Optional<ConstrainULong> height;
Optional<ConstrainDouble> aspect_ratio;
Optional<ConstrainDouble> frame_rate;
Optional<ConstrainDOMString> facing_mode;
Optional<ConstrainDOMString> resize_mode;
Optional<ConstrainULong> sample_rate;
Optional<ConstrainULong> sample_size;
Optional<ConstrainBooleanOrDOMString> echo_cancellation;
Optional<ConstrainBoolean> auto_gain_control;
Optional<ConstrainBoolean> noise_suppression;
Optional<ConstrainDouble> latency;
Optional<ConstrainULong> channel_count;
Optional<ConstrainDOMString> device_id;
Optional<ConstrainDOMString> group_id;
Optional<ConstrainBoolean> background_blur;
};
// https://w3c.github.io/mediacapture-main/#mediatrackconstraints
struct MediaTrackConstraints : MediaTrackConstraintSet {
// This is the list of ConstraintSets that the User Agent MUST attempt to satisfy, in order,
// skipping only those that cannot be satisfied.
Optional<Vector<MediaTrackConstraintSet>> advanced;
};
// https://w3c.github.io/mediacapture-main/#mediastreamconstraints
struct MediaStreamConstraints {
// If true, it requests that the returned MediaStream contain a video track. If a Constraints
// structure is provided, it further specifies the nature and settings of the video Track.
// If false, the MediaStream MUST NOT contain a video Track.
Variant<bool, MediaTrackConstraints> video { false };
// If true, it requests that the returned MediaStream contain an audio track. If a Constraints
// structure is provided, it further specifies the nature and settings of the audio Track.
// If false, the MediaStream MUST NOT contain an audio Track.
Variant<bool, MediaTrackConstraints> audio { false };
};
// https://w3c.github.io/mediacapture-main/#dom-mediatracksupportedconstraints
struct MediaTrackSupportedConstraints {
bool width { false };
bool height { false };
bool aspect_ratio { false };
bool frame_rate { false };
bool facing_mode { false };
bool resize_mode { false };
bool sample_rate { false };
bool sample_size { false };
bool echo_cancellation { false };
bool auto_gain_control { false };
bool noise_suppression { false };
bool latency { false };
bool channel_count { false };
bool device_id { false };
bool group_id { false };
bool background_blur { false };
};
// https://w3c.github.io/mediacapture-main/#dom-mediatrackcapabilities
struct MediaTrackCapabilities {
Optional<ULongRange> width;
Optional<ULongRange> height;
Optional<DoubleRange> aspect_ratio;
Optional<DoubleRange> frame_rate;
Optional<Vector<String>> facing_mode;
Optional<Vector<String>> resize_mode;
Optional<ULongRange> sample_rate;
Optional<ULongRange> sample_size;
Optional<Vector<Variant<bool, String>>> echo_cancellation;
Optional<Vector<bool>> auto_gain_control;
Optional<Vector<bool>> noise_suppression;
Optional<DoubleRange> latency;
Optional<ULongRange> channel_count;
Optional<String> device_id;
Optional<String> group_id;
Optional<Vector<bool>> background_blur;
};
// https://w3c.github.io/mediacapture-main/#dom-mediatracksettings
struct MediaTrackSettings {
Optional<WebIDL::UnsignedLong> width;
Optional<WebIDL::UnsignedLong> height;
Optional<double> aspect_ratio;
Optional<double> frame_rate;
Optional<String> facing_mode;
Optional<String> resize_mode;
Optional<WebIDL::UnsignedLong> sample_rate;
Optional<WebIDL::UnsignedLong> sample_size;
Optional<Variant<bool, String>> echo_cancellation;
Optional<bool> auto_gain_control;
Optional<bool> noise_suppression;
Optional<double> latency;
Optional<WebIDL::UnsignedLong> channel_count;
Optional<String> device_id;
Optional<String> group_id;
Optional<bool> background_blur;
};
}

View file

@ -0,0 +1,137 @@
// https://w3c.github.io/mediacapture-main/#dom-constraindomstringparameters
dictionary ConstrainDOMStringParameters {
(DOMString or sequence<DOMString>) exact;
(DOMString or sequence<DOMString>) ideal;
};
// https://w3c.github.io/mediacapture-main/#dom-constraindoublerange
dictionary DoubleRange {
double max;
double min;
};
// https://w3c.github.io/mediacapture-main/#dom-constraindoublerange
dictionary ConstrainDoubleRange : DoubleRange {
double exact;
double ideal;
};
// https://w3c.github.io/mediacapture-main/#dom-constrainulongrange
dictionary ULongRange {
[Clamp] unsigned long max;
[Clamp] unsigned long min;
};
// https://w3c.github.io/mediacapture-main/#dom-constrainulongrange
dictionary ConstrainULongRange : ULongRange {
[Clamp] unsigned long exact;
[Clamp] unsigned long ideal;
};
// https://w3c.github.io/mediacapture-main/#dom-constrainbooleanparameters
dictionary ConstrainBooleanParameters {
boolean exact;
boolean ideal;
};
// https://w3c.github.io/mediacapture-main/#dom-constrainbooleanordomstringparameters
dictionary ConstrainBooleanOrDOMStringParameters {
(boolean or DOMString) exact;
(boolean or DOMString) ideal;
};
typedef (unsigned long or ConstrainULongRange) ConstrainULong;
typedef (double or ConstrainDoubleRange) ConstrainDouble;
typedef (boolean or ConstrainBooleanParameters) ConstrainBoolean;
typedef (DOMString or sequence<DOMString> or ConstrainDOMStringParameters) ConstrainDOMString;
typedef (boolean or DOMString or ConstrainBooleanOrDOMStringParameters) ConstrainBooleanOrDOMString;
// https://w3c.github.io/mediacapture-main/#dictdef-mediatrackconstraintset
dictionary MediaTrackConstraintSet {
ConstrainULong width;
ConstrainULong height;
ConstrainDouble aspectRatio;
ConstrainDouble frameRate;
ConstrainDOMString facingMode;
ConstrainDOMString resizeMode;
ConstrainULong sampleRate;
ConstrainULong sampleSize;
ConstrainBooleanOrDOMString echoCancellation;
ConstrainBoolean autoGainControl;
ConstrainBoolean noiseSuppression;
ConstrainDouble latency;
ConstrainULong channelCount;
ConstrainDOMString deviceId;
ConstrainDOMString groupId;
ConstrainBoolean backgroundBlur;
};
dictionary MediaTrackConstraints : MediaTrackConstraintSet {
sequence<MediaTrackConstraintSet> advanced;
};
// https://w3c.github.io/mediacapture-main/#mediastreamconstraints
dictionary MediaStreamConstraints {
(boolean or MediaTrackConstraints) video = false;
(boolean or MediaTrackConstraints) audio = false;
};
// https://w3c.github.io/mediacapture-main/#dom-mediatracksupportedconstraints
dictionary MediaTrackSupportedConstraints {
boolean width = true;
boolean height = true;
boolean aspectRatio = true;
boolean frameRate = true;
boolean facingMode = true;
boolean resizeMode = true;
boolean sampleRate = true;
boolean sampleSize = true;
boolean echoCancellation = true;
boolean autoGainControl = true;
boolean noiseSuppression = true;
boolean latency = true;
boolean channelCount = true;
boolean deviceId = true;
boolean groupId = true;
boolean backgroundBlur = true;
};
// https://w3c.github.io/mediacapture-main/#dom-mediatrackcapabilities
dictionary MediaTrackCapabilities {
ULongRange width;
ULongRange height;
DoubleRange aspectRatio;
DoubleRange frameRate;
sequence<DOMString> facingMode;
sequence<DOMString> resizeMode;
ULongRange sampleRate;
ULongRange sampleSize;
sequence<(boolean or DOMString)> echoCancellation;
sequence<boolean> autoGainControl;
sequence<boolean> noiseSuppression;
DoubleRange latency;
ULongRange channelCount;
DOMString deviceId;
DOMString groupId;
sequence<boolean> backgroundBlur;
};
// https://w3c.github.io/mediacapture-main/#dom-mediatracksettings
dictionary MediaTrackSettings {
unsigned long width;
unsigned long height;
double aspectRatio;
double frameRate;
DOMString facingMode;
DOMString resizeMode;
unsigned long sampleRate;
unsigned long sampleSize;
(boolean or DOMString) echoCancellation;
boolean autoGainControl;
boolean noiseSuppression;
double latency;
unsigned long channelCount;
DOMString deviceId;
DOMString groupId;
boolean backgroundBlur;
};

View file

@ -0,0 +1,186 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Value.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/MediaStreamTrackPrototype.h>
#include <LibWeb/Crypto/Crypto.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/MediaCapture/MediaStreamTrack.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::MediaCapture {
GC_DEFINE_ALLOCATOR(MediaStreamTrack);
Atomic<u64> MediaStreamTrack::s_next_provider_id { 1 };
static constexpr auto audio_track_kind = static_cast<Bindings::MediaStreamTrackKind>(0);
static constexpr auto video_track_kind = static_cast<Bindings::MediaStreamTrackKind>(1);
MediaStreamTrack::MediaStreamTrack(JS::Realm& realm)
: DOM::EventTarget(realm)
{
}
// https://w3c.github.io/mediacapture-main/#mediastreamtrack
GC::Ref<MediaStreamTrack> MediaStreamTrack::create(JS::Realm& realm, Bindings::MediaStreamTrackKind kind, Optional<String> label, bool muted)
{
// https://w3c.github.io/mediacapture-main/#dfn-create-a-mediastreamtrack
// 1. Let track be a new object of type source's MediaStreamTrack source type.
auto track = realm.create<MediaStreamTrack>(realm);
// Initialize track with the following internal slots.
// FIXME: [[Source]], initialized to source.
// [[Id]]: See MediaStream.id attribute for guidelines on how to generate such an identifier.
track->m_id = Crypto::generate_random_uuid();
// [[Kind]]: "audio" if source is an audio source, or "video" if source is a video source.
track->m_kind = kind;
// [[Label]]: source label or empty string.
track->m_label = label.value_or(""_string);
// [[ReadyState]]: "live".
track->m_state = Bindings::MediaStreamTrackState::Live;
// [[Enabled]]: true.
track->m_enabled = true;
// [[Muted]]: true if source is muted, false otherwise.
track->m_muted = muted;
// [[Capabilities]], [[Constraints]], and [[Settings]], all initialized as specified in the ConstrainablePattern.
// [[Restrictable]], initialized to false.
track->m_provider_id = s_next_provider_id.fetch_add(1, AK::MemoryOrder::memory_order_relaxed);
// FIXME: 2. If mediaDevicesToTieSourceTo is not null, tie track source to MediaDevices with source and mediaDevicesToTieSourceTo.
// FIXME: 3. Run source's MediaStreamTrack source-specific construction steps with track as parameter.
// 4. Return track.
return track;
}
void MediaStreamTrack::set_settings(MediaTrackSettings settings)
{
m_settings = move(settings);
}
void MediaStreamTrack::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(MediaStreamTrack);
Base::initialize(realm);
}
// https://w3c.github.io/mediacapture-main/#dom-mediastreamtrack-stop
void MediaStreamTrack::stop()
{
// 1. Let track be the current MediaStreamTrack object.
// 2. If track's [[ReadyState]] is "ended", then abort these steps.
if (m_state == Bindings::MediaStreamTrackState::Ended)
return;
// FIXME: 3. Notify track's source that track is ended.
// 4. Set track's [[ReadyState]] to "ended".
m_state = Bindings::MediaStreamTrackState::Ended;
// AD-HOC: Until track sources are modeled, stop() is the only implemented end-of-life path.
// Preserve the observable ended event instead of dropping it on the floor.
dispatch_event(DOM::Event::create(realm(), HTML::EventNames::ended));
}
// https://w3c.github.io/mediacapture-main/#dom-mediastreamtrack-clone
GC::Ref<MediaStreamTrack> MediaStreamTrack::clone() const
{
// When the clone() method is invoked, the User Agent MUST return the result of clone a track with this.
// https://w3c.github.io/mediacapture-main/#clone-a-track
// 1. Let track be the MediaStreamTrack object to be cloned.
//
// FIXME: 2. Let source be track's [[Source]].
// 3. Let trackClone be the result of creating a MediaStreamTrack with source and null.
auto track_clone = create(realm(), m_kind, m_label, m_muted);
// 4. Set trackClone's [[ReadyState]] to track's [[ReadyState]] value.
track_clone->m_state = m_state;
// 5. Set trackClone's [[Capabilities]] to a clone of track's [[Capabilities]].
track_clone->m_capabilities = m_capabilities;
// 6. Set trackClone's [[Constraints]] to a clone of track's [[Constraints]].
track_clone->m_constraints = m_constraints;
// 7. Set trackClone's [[Settings]] to a clone of track's [[Settings]].
track_clone->m_settings = m_settings;
// Initialize the remaining internal slots to match the source track.
track_clone->m_enabled = m_enabled;
track_clone->m_provider_id = s_next_provider_id.fetch_add(1, AK::MemoryOrder::memory_order_relaxed);
// FIXME: 8. Run source MediaStreamTrack source-specific clone steps with track and trackClone as parameters.
// 9. Return trackClone.
return track_clone;
}
bool MediaStreamTrack::is_audio() const
{
return m_kind == audio_track_kind;
}
bool MediaStreamTrack::is_video() const
{
return m_kind == video_track_kind;
}
Optional<String> MediaStreamTrack::device_id() const
{
return m_settings.device_id;
}
u32 MediaStreamTrack::sample_rate_hz() const
{
if (m_settings.sample_rate.has_value())
return *m_settings.sample_rate;
return 0;
}
u32 MediaStreamTrack::channel_count() const
{
if (m_settings.channel_count.has_value())
return *m_settings.channel_count;
return 0;
}
// https://w3c.github.io/mediacapture-main/#dom-mediastreamtrack-getcapabilities
MediaTrackCapabilities MediaStreamTrack::get_capabilities() const
{
return m_capabilities;
}
// https://w3c.github.io/mediacapture-main/#dom-mediastreamtrack-getconstraints
MediaTrackConstraints MediaStreamTrack::get_constraints() const
{
return m_constraints;
}
// https://w3c.github.io/mediacapture-main/#dom-mediastreamtrack-getsettings
MediaTrackSettings MediaStreamTrack::get_settings() const
{
return m_settings;
}
// https://w3c.github.io/mediacapture-main/#dom-mediastreamtrack-applyconstraints
GC::Ref<WebIDL::Promise> MediaStreamTrack::apply_constraints(Optional<MediaTrackConstraints> const& constraints)
{
if (constraints.has_value())
m_constraints = constraints.value();
// FIXME: Apply constraints to the underlying source and update settings.
return WebIDL::create_resolved_promise(realm(), JS::js_undefined());
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Atomic.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <LibWeb/Bindings/MediaStreamTrackPrototype.h>
#include <LibWeb/Bindings/PlatformObject.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/Forward.h>
#include <LibWeb/MediaCapture/MediaStreamConstraints.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::MediaCapture {
// Spec: https://w3c.github.io/mediacapture-main/#mediastreamtrack
class MediaStreamTrack final : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(MediaStreamTrack, DOM::EventTarget);
GC_DECLARE_ALLOCATOR(MediaStreamTrack);
public:
static GC::Ref<MediaStreamTrack> create(JS::Realm&, Bindings::MediaStreamTrackKind, Optional<String> label = {}, bool muted = false);
virtual ~MediaStreamTrack() override = default;
Bindings::MediaStreamTrackKind kind() const { return m_kind; }
String id() const { return m_id; }
String label() const { return m_label; }
bool enabled() const { return m_enabled; }
void set_enabled(bool enabled) { m_enabled = enabled; }
bool muted() const { return m_muted; }
Bindings::MediaStreamTrackState ready_state() const { return m_state; }
void stop();
GC::Ref<MediaStreamTrack> clone() const;
bool is_audio() const;
bool is_video() const;
MediaTrackCapabilities get_capabilities() const;
MediaTrackConstraints get_constraints() const;
MediaTrackSettings get_settings() const;
GC::Ref<WebIDL::Promise> apply_constraints(Optional<MediaTrackConstraints> const& constraints);
void set_settings(MediaTrackSettings settings);
Optional<String> device_id() const;
u32 sample_rate_hz() const;
u32 channel_count() const;
u64 provider_id() const { return m_provider_id; }
private:
explicit MediaStreamTrack(JS::Realm&);
virtual void initialize(JS::Realm&) override;
static Atomic<u64> s_next_provider_id;
Bindings::MediaStreamTrackKind m_kind { static_cast<Bindings::MediaStreamTrackKind>(0) };
String m_id;
String m_label;
bool m_enabled { true };
bool m_muted { false };
Bindings::MediaStreamTrackState m_state { static_cast<Bindings::MediaStreamTrackState>(0) };
MediaTrackCapabilities m_capabilities;
MediaTrackConstraints m_constraints;
MediaTrackSettings m_settings;
u64 m_provider_id { 0 };
};
}

View file

@ -0,0 +1,22 @@
#import <DOM/EventTarget.idl>
#import <MediaCapture/MediaStreamConstraints.idl>
// https://w3c.github.io/mediacapture-main/#mediastreamtrack
enum MediaStreamTrackState { "live", "ended" };
enum MediaStreamTrackKind { "audio", "video" };
[Exposed=Window]
interface MediaStreamTrack : EventTarget {
readonly attribute MediaStreamTrackKind kind;
readonly attribute DOMString id;
readonly attribute DOMString label;
attribute boolean enabled;
readonly attribute boolean muted;
readonly attribute MediaStreamTrackState readyState;
undefined stop();
MediaTrackCapabilities getCapabilities();
MediaTrackConstraints getConstraints();
MediaTrackSettings getSettings();
Promise<undefined> applyConstraints(optional MediaTrackConstraints constraints = {});
};

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/MediaStreamTrackEventPrototype.h>
#include <LibWeb/MediaCapture/MediaStreamTrackEvent.h>
namespace Web::MediaCapture {
static GC::Ref<MediaStreamTrack> require_track(MediaStreamTrackEventInit const& event_init)
{
VERIFY(event_init.track);
return *event_init.track;
}
GC_DEFINE_ALLOCATOR(MediaStreamTrackEvent);
GC::Ref<MediaStreamTrackEvent> MediaStreamTrackEvent::create(JS::Realm& realm, FlyString const& event_name, MediaStreamTrackEventInit const& event_init)
{
return realm.create<MediaStreamTrackEvent>(realm, event_name, event_init);
}
GC::Ref<MediaStreamTrackEvent> MediaStreamTrackEvent::construct_impl(JS::Realm& realm, FlyString const& event_name, MediaStreamTrackEventInit const& event_init)
{
return create(realm, event_name, event_init);
}
// https://w3c.github.io/mediacapture-main/#mediastreamtrackevent
MediaStreamTrackEvent::MediaStreamTrackEvent(JS::Realm& realm, FlyString const& event_name, MediaStreamTrackEventInit const& event_init)
: DOM::Event(realm, event_name, [&] {
DOM::EventInit base_init {};
base_init.bubbles = event_init.bubbles;
base_init.cancelable = event_init.cancelable;
base_init.composed = event_init.composed;
return base_init;
}())
, m_track(require_track(event_init))
{
}
MediaStreamTrackEvent::~MediaStreamTrackEvent() = default;
void MediaStreamTrackEvent::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(MediaStreamTrackEvent);
Base::initialize(realm);
}
void MediaStreamTrackEvent::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_track);
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2026, The Ladybird developers
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/DOM/Event.h>
#include <LibWeb/MediaCapture/MediaStreamTrack.h>
namespace Web::MediaCapture {
// https://w3c.github.io/mediacapture-main/#dictdef-mediastreamtrackeventinit
struct MediaStreamTrackEventInit final : public DOM::EventInit {
GC::Ptr<MediaStreamTrack> track;
};
// https://w3c.github.io/mediacapture-main/#mediastreamtrackevent
class MediaStreamTrackEvent final : public DOM::Event {
WEB_PLATFORM_OBJECT(MediaStreamTrackEvent, DOM::Event);
GC_DECLARE_ALLOCATOR(MediaStreamTrackEvent);
public:
[[nodiscard]] static GC::Ref<MediaStreamTrackEvent> create(JS::Realm&, FlyString const& event_name, MediaStreamTrackEventInit const& event_init);
[[nodiscard]] static GC::Ref<MediaStreamTrackEvent> construct_impl(JS::Realm&, FlyString const& event_name, MediaStreamTrackEventInit const& event_init);
virtual ~MediaStreamTrackEvent() override;
GC::Ref<MediaStreamTrack> track() const { return m_track; }
private:
MediaStreamTrackEvent(JS::Realm&, FlyString const& event_name, MediaStreamTrackEventInit const& event_init);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
GC::Ref<MediaStreamTrack> m_track;
};
}

View file

@ -0,0 +1,14 @@
#import <DOM/Event.idl>
#import <MediaCapture/MediaStreamTrack.idl>
// https://w3c.github.io/mediacapture-main/#dictdef-mediastreamtrackeventinit
dictionary MediaStreamTrackEventInit : EventInit {
required MediaStreamTrack track;
};
// https://w3c.github.io/mediacapture-main/#mediastreamtrackevent
[Exposed=Window]
interface MediaStreamTrackEvent : Event {
constructor(DOMString type, MediaStreamTrackEventInit eventInitDict);
[SameObject] readonly attribute MediaStreamTrack track;
};

View file

@ -161,6 +161,11 @@ libweb_js_bindings(Geometry/DOMQuad)
libweb_js_bindings(Geometry/DOMRect)
libweb_js_bindings(Geometry/DOMRectList)
libweb_js_bindings(Geometry/DOMRectReadOnly)
libweb_js_bindings(MediaCapture/MediaDeviceInfo)
libweb_js_bindings(MediaCapture/MediaDevices)
libweb_js_bindings(MediaCapture/MediaStream)
libweb_js_bindings(MediaCapture/MediaStreamTrack)
libweb_js_bindings(MediaCapture/MediaStreamTrackEvent)
libweb_js_bindings(HTML/AudioTrack)
libweb_js_bindings(HTML/AudioTrackList)
libweb_js_bindings(HTML/BarProp)

View file

@ -100,8 +100,13 @@ static bool is_platform_object(Type const& type)
"KeyframeEffect"sv,
"MediaKeySystemAccess"sv,
"MediaList"sv,
"MediaDeviceInfo"sv,
"MediaDevices"sv,
"MediaSource"sv,
"Memory"sv,
"MediaStream"sv,
"MediaStreamTrack"sv,
"MediaStreamTrackEvent"sv,
"MessagePort"sv,
"Module"sv,
"MutationRecord"sv,
@ -5438,6 +5443,7 @@ using namespace Web::IndexedDB;
using namespace Web::Internals;
using namespace Web::IntersectionObserver;
using namespace Web::MediaCapabilitiesAPI;
using namespace Web::MediaCapture;
using namespace Web::MediaSourceExtensions;
using namespace Web::NavigationTiming;
using namespace Web::NotificationsAPI;

View file

@ -13,6 +13,7 @@ Crash/HTML/video-track-switch-during-seek.html
Text/input/HTML/media-source-buffered.html
Text/input/HTML/media-source-setup.html
Text/input/css/FontFace-arraybuffer-matching.html
Text/input/wpt-import/mediacapture-streams/idlharness.https.window.html
; pushState with path URL requires HTTP(s) scheme.
Text/input/navigation/history-replace-push-then-back.html

View file

@ -296,6 +296,8 @@ ManagedSourceBuffer
Map
MathMLElement
MediaCapabilities
MediaDeviceInfo
MediaDevices
MediaElementAudioSourceNode
MediaError
MediaKeySystemAccess
@ -304,6 +306,9 @@ MediaQueryList
MediaQueryListEvent
MediaSource
MediaSourceHandle
MediaStream
MediaStreamTrack
MediaStreamTrackEvent
MessageChannel
MessageEvent
MessagePort

View file

@ -0,0 +1,174 @@
Harness status: OK
Found 168 tests
102 Pass
66 Fail
Pass idl_test setup
Pass idl_test validation
Pass Partial interface Navigator: original interface defined
Pass Partial interface Navigator: member names are unique
Pass Partial interface MediaDevices: original interface defined
Pass Partial interface MediaDevices: member names are unique
Pass Partial interface Navigator[2]: member names are unique
Pass Partial interface mixin NavigatorID: member names are unique
Pass Navigator includes NavigatorID: member names are unique
Pass Navigator includes NavigatorLanguage: member names are unique
Pass Navigator includes NavigatorOnLine: member names are unique
Pass Navigator includes NavigatorContentUtils: member names are unique
Pass Navigator includes NavigatorCookies: member names are unique
Pass Navigator includes NavigatorPlugins: member names are unique
Pass Navigator includes NavigatorConcurrentHardware: member names are unique
Pass MediaStream interface: existence and properties of interface object
Pass MediaStream interface object length
Pass MediaStream interface object name
Pass MediaStream interface: existence and properties of interface prototype object
Pass MediaStream interface: existence and properties of interface prototype object's "constructor" property
Pass MediaStream interface: existence and properties of interface prototype object's @@unscopables property
Pass MediaStream interface: attribute id
Pass MediaStream interface: operation getAudioTracks()
Pass MediaStream interface: operation getVideoTracks()
Pass MediaStream interface: operation getTracks()
Pass MediaStream interface: operation getTrackById(DOMString)
Pass MediaStream interface: operation addTrack(MediaStreamTrack)
Pass MediaStream interface: operation removeTrack(MediaStreamTrack)
Pass MediaStream interface: operation clone()
Pass MediaStream interface: attribute active
Pass MediaStream interface: attribute onaddtrack
Pass MediaStream interface: attribute onremovetrack
Fail MediaStream must be primary interface of stream
Fail Stringification of stream
Fail MediaStream interface: stream must inherit property "id" with the proper type
Fail MediaStream interface: stream must inherit property "getAudioTracks()" with the proper type
Fail MediaStream interface: stream must inherit property "getVideoTracks()" with the proper type
Fail MediaStream interface: stream must inherit property "getTracks()" with the proper type
Fail MediaStream interface: stream must inherit property "getTrackById(DOMString)" with the proper type
Fail MediaStream interface: calling getTrackById(DOMString) on stream with too few arguments must throw TypeError
Fail MediaStream interface: stream must inherit property "addTrack(MediaStreamTrack)" with the proper type
Fail MediaStream interface: calling addTrack(MediaStreamTrack) on stream with too few arguments must throw TypeError
Fail MediaStream interface: stream must inherit property "removeTrack(MediaStreamTrack)" with the proper type
Fail MediaStream interface: calling removeTrack(MediaStreamTrack) on stream with too few arguments must throw TypeError
Fail MediaStream interface: stream must inherit property "clone()" with the proper type
Fail MediaStream interface: stream must inherit property "active" with the proper type
Fail MediaStream interface: stream must inherit property "onaddtrack" with the proper type
Fail MediaStream interface: stream must inherit property "onremovetrack" with the proper type
Pass MediaStream must be primary interface of new MediaStream()
Pass Stringification of new MediaStream()
Pass MediaStream interface: new MediaStream() must inherit property "id" with the proper type
Pass MediaStream interface: new MediaStream() must inherit property "getAudioTracks()" with the proper type
Pass MediaStream interface: new MediaStream() must inherit property "getVideoTracks()" with the proper type
Pass MediaStream interface: new MediaStream() must inherit property "getTracks()" with the proper type
Pass MediaStream interface: new MediaStream() must inherit property "getTrackById(DOMString)" with the proper type
Pass MediaStream interface: calling getTrackById(DOMString) on new MediaStream() with too few arguments must throw TypeError
Pass MediaStream interface: new MediaStream() must inherit property "addTrack(MediaStreamTrack)" with the proper type
Pass MediaStream interface: calling addTrack(MediaStreamTrack) on new MediaStream() with too few arguments must throw TypeError
Pass MediaStream interface: new MediaStream() must inherit property "removeTrack(MediaStreamTrack)" with the proper type
Pass MediaStream interface: calling removeTrack(MediaStreamTrack) on new MediaStream() with too few arguments must throw TypeError
Pass MediaStream interface: new MediaStream() must inherit property "clone()" with the proper type
Pass MediaStream interface: new MediaStream() must inherit property "active" with the proper type
Pass MediaStream interface: new MediaStream() must inherit property "onaddtrack" with the proper type
Pass MediaStream interface: new MediaStream() must inherit property "onremovetrack" with the proper type
Pass MediaStreamTrack interface: existence and properties of interface object
Pass MediaStreamTrack interface object length
Pass MediaStreamTrack interface object name
Pass MediaStreamTrack interface: existence and properties of interface prototype object
Pass MediaStreamTrack interface: existence and properties of interface prototype object's "constructor" property
Pass MediaStreamTrack interface: existence and properties of interface prototype object's @@unscopables property
Pass MediaStreamTrack interface: attribute kind
Pass MediaStreamTrack interface: attribute id
Pass MediaStreamTrack interface: attribute label
Pass MediaStreamTrack interface: attribute enabled
Pass MediaStreamTrack interface: attribute muted
Fail MediaStreamTrack interface: attribute onmute
Fail MediaStreamTrack interface: attribute onunmute
Pass MediaStreamTrack interface: attribute readyState
Fail MediaStreamTrack interface: attribute onended
Fail MediaStreamTrack interface: operation clone()
Pass MediaStreamTrack interface: operation stop()
Pass MediaStreamTrack interface: operation getCapabilities()
Pass MediaStreamTrack interface: operation getConstraints()
Pass MediaStreamTrack interface: operation getSettings()
Pass MediaStreamTrack interface: operation applyConstraints(optional MediaTrackConstraints)
Fail MediaStreamTrack must be primary interface of track
Fail Stringification of track
Fail MediaStreamTrack interface: track must inherit property "kind" with the proper type
Fail MediaStreamTrack interface: track must inherit property "id" with the proper type
Fail MediaStreamTrack interface: track must inherit property "label" with the proper type
Fail MediaStreamTrack interface: track must inherit property "enabled" with the proper type
Fail MediaStreamTrack interface: track must inherit property "muted" with the proper type
Fail MediaStreamTrack interface: track must inherit property "onmute" with the proper type
Fail MediaStreamTrack interface: track must inherit property "onunmute" with the proper type
Fail MediaStreamTrack interface: track must inherit property "readyState" with the proper type
Fail MediaStreamTrack interface: track must inherit property "onended" with the proper type
Fail MediaStreamTrack interface: track must inherit property "clone()" with the proper type
Fail MediaStreamTrack interface: track must inherit property "stop()" with the proper type
Fail MediaStreamTrack interface: track must inherit property "getCapabilities()" with the proper type
Fail MediaStreamTrack interface: track must inherit property "getConstraints()" with the proper type
Fail MediaStreamTrack interface: track must inherit property "getSettings()" with the proper type
Fail MediaStreamTrack interface: track must inherit property "applyConstraints(optional MediaTrackConstraints)" with the proper type
Fail MediaStreamTrack interface: calling applyConstraints(optional MediaTrackConstraints) on track with too few arguments must throw TypeError
Pass MediaStreamTrackEvent interface: existence and properties of interface object
Pass MediaStreamTrackEvent interface object length
Pass MediaStreamTrackEvent interface object name
Pass MediaStreamTrackEvent interface: existence and properties of interface prototype object
Pass MediaStreamTrackEvent interface: existence and properties of interface prototype object's "constructor" property
Pass MediaStreamTrackEvent interface: existence and properties of interface prototype object's @@unscopables property
Pass MediaStreamTrackEvent interface: attribute track
Fail MediaStreamTrackEvent must be primary interface of trackEvent
Fail Stringification of trackEvent
Fail MediaStreamTrackEvent interface: trackEvent must inherit property "track" with the proper type
Fail OverconstrainedError interface: existence and properties of interface object
Fail OverconstrainedError interface object length
Fail OverconstrainedError interface object name
Fail OverconstrainedError interface: existence and properties of interface prototype object
Fail OverconstrainedError interface: existence and properties of interface prototype object's "constructor" property
Fail OverconstrainedError interface: existence and properties of interface prototype object's @@unscopables property
Fail OverconstrainedError interface: attribute constraint
Fail OverconstrainedError must be primary interface of new OverconstrainedError("constraint")
Fail Stringification of new OverconstrainedError("constraint")
Fail OverconstrainedError interface: new OverconstrainedError("constraint") must inherit property "constraint" with the proper type
Pass MediaDevices interface: existence and properties of interface object
Pass MediaDevices interface object length
Pass MediaDevices interface object name
Pass MediaDevices interface: existence and properties of interface prototype object
Pass MediaDevices interface: existence and properties of interface prototype object's "constructor" property
Pass MediaDevices interface: existence and properties of interface prototype object's @@unscopables property
Pass MediaDevices interface: attribute ondevicechange
Pass MediaDevices interface: operation enumerateDevices()
Pass MediaDevices interface: operation getSupportedConstraints()
Pass MediaDevices interface: operation getUserMedia(optional MediaStreamConstraints)
Pass MediaDevices must be primary interface of navigator.mediaDevices
Pass Stringification of navigator.mediaDevices
Pass MediaDevices interface: navigator.mediaDevices must inherit property "ondevicechange" with the proper type
Pass MediaDevices interface: navigator.mediaDevices must inherit property "enumerateDevices()" with the proper type
Pass MediaDevices interface: navigator.mediaDevices must inherit property "getSupportedConstraints()" with the proper type
Pass MediaDevices interface: navigator.mediaDevices must inherit property "getUserMedia(optional MediaStreamConstraints)" with the proper type
Pass MediaDevices interface: calling getUserMedia(optional MediaStreamConstraints) on navigator.mediaDevices with too few arguments must throw TypeError
Pass MediaDeviceInfo interface: existence and properties of interface object
Pass MediaDeviceInfo interface object length
Pass MediaDeviceInfo interface object name
Pass MediaDeviceInfo interface: existence and properties of interface prototype object
Pass MediaDeviceInfo interface: existence and properties of interface prototype object's "constructor" property
Pass MediaDeviceInfo interface: existence and properties of interface prototype object's @@unscopables property
Pass MediaDeviceInfo interface: attribute deviceId
Pass MediaDeviceInfo interface: attribute kind
Pass MediaDeviceInfo interface: attribute label
Pass MediaDeviceInfo interface: attribute groupId
Pass MediaDeviceInfo interface: operation toJSON()
Fail InputDeviceInfo interface: existence and properties of interface object
Fail InputDeviceInfo interface object length
Fail InputDeviceInfo interface object name
Fail InputDeviceInfo interface: existence and properties of interface prototype object
Fail InputDeviceInfo interface: existence and properties of interface prototype object's "constructor" property
Fail InputDeviceInfo interface: existence and properties of interface prototype object's @@unscopables property
Fail InputDeviceInfo interface: operation getCapabilities()
Fail DeviceChangeEvent interface: existence and properties of interface object
Fail DeviceChangeEvent interface object length
Fail DeviceChangeEvent interface object name
Fail DeviceChangeEvent interface: existence and properties of interface prototype object
Fail DeviceChangeEvent interface: existence and properties of interface prototype object's "constructor" property
Fail DeviceChangeEvent interface: existence and properties of interface prototype object's @@unscopables property
Fail DeviceChangeEvent interface: attribute devices
Fail DeviceChangeEvent interface: attribute userInsertedDevices
Pass Navigator interface: attribute mediaDevices
Pass Navigator interface: navigator must inherit property "mediaDevices" with the proper type

View file

@ -0,0 +1,41 @@
// GENERATED CONTENT - DO NOT EDIT
// Content was automatically extracted by Reffy into webref
// (https://github.com/w3c/webref)
// Source: Permissions (https://w3c.github.io/permissions/)
[Exposed=(Window)]
partial interface Navigator {
[SameObject] readonly attribute Permissions permissions;
};
[Exposed=(Worker)]
partial interface WorkerNavigator {
[SameObject] readonly attribute Permissions permissions;
};
[Exposed=(Window,Worker)]
interface Permissions {
Promise<PermissionStatus> query(object permissionDesc);
};
dictionary PermissionDescriptor {
required DOMString name;
};
[Exposed=(Window,Worker)]
interface PermissionStatus : EventTarget {
readonly attribute PermissionState state;
readonly attribute DOMString name;
attribute EventHandler onchange;
};
enum PermissionState {
"granted",
"denied",
"prompt",
};
dictionary PermissionSetParameters {
required object descriptor;
required PermissionState state;
};

View file

@ -0,0 +1,62 @@
// GENERATED CONTENT - DO NOT EDIT
// Content was automatically extracted by Reffy into webref
// (https://github.com/w3c/webref)
// Source: Web IDL Standard (https://webidl.spec.whatwg.org/)
[Exposed=*, Serializable]
interface QuotaExceededError : DOMException {
constructor(optional DOMString message = "", optional QuotaExceededErrorOptions options = {});
readonly attribute double? quota;
readonly attribute double? requested;
};
dictionary QuotaExceededErrorOptions {
double quota;
double requested;
};
typedef (Int8Array or Int16Array or Int32Array or
Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
BigInt64Array or BigUint64Array or
Float16Array or Float32Array or Float64Array or DataView) ArrayBufferView;
typedef (ArrayBufferView or ArrayBuffer) BufferSource;
typedef (ArrayBuffer or SharedArrayBuffer or [AllowShared] ArrayBufferView) AllowSharedBufferSource;
[Exposed=*,
Serializable]
interface DOMException { // but see below note about JavaScript binding
constructor(optional DOMString message = "", optional DOMString name = "Error");
readonly attribute DOMString name;
readonly attribute DOMString message;
readonly attribute unsigned short code;
const unsigned short INDEX_SIZE_ERR = 1;
const unsigned short DOMSTRING_SIZE_ERR = 2;
const unsigned short HIERARCHY_REQUEST_ERR = 3;
const unsigned short WRONG_DOCUMENT_ERR = 4;
const unsigned short INVALID_CHARACTER_ERR = 5;
const unsigned short NO_DATA_ALLOWED_ERR = 6;
const unsigned short NO_MODIFICATION_ALLOWED_ERR = 7;
const unsigned short NOT_FOUND_ERR = 8;
const unsigned short NOT_SUPPORTED_ERR = 9;
const unsigned short INUSE_ATTRIBUTE_ERR = 10;
const unsigned short INVALID_STATE_ERR = 11;
const unsigned short SYNTAX_ERR = 12;
const unsigned short INVALID_MODIFICATION_ERR = 13;
const unsigned short NAMESPACE_ERR = 14;
const unsigned short INVALID_ACCESS_ERR = 15;
const unsigned short VALIDATION_ERR = 16;
const unsigned short TYPE_MISMATCH_ERR = 17;
const unsigned short SECURITY_ERR = 18;
const unsigned short NETWORK_ERR = 19;
const unsigned short ABORT_ERR = 20;
const unsigned short URL_MISMATCH_ERR = 21;
const unsigned short QUOTA_EXCEEDED_ERR = 22;
const unsigned short TIMEOUT_ERR = 23;
const unsigned short INVALID_NODE_TYPE_ERR = 24;
const unsigned short DATA_CLONE_ERR = 25;
};
callback Function = any (any... arguments);
callback VoidFunction = undefined ();

View file

@ -0,0 +1,9 @@
<!doctype html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="../resources/WebIDLParser.js"></script>
<script src="../resources/idlharness.js"></script>
<div id=log></div>
<script src="../mediacapture-streams/idlharness.https.window.js"></script>

View file

@ -0,0 +1,50 @@
// META: script=/resources/WebIDLParser.js
// META: script=/resources/idlharness.js
// META: timeout=long
'use strict';
// https://w3c.github.io/mediacapture-main/
idl_test(
['mediacapture-streams'],
['webidl', 'dom', 'html', 'permissions'],
async idl_array => {
const inputDevices = [];
const outputDevices = [];
try {
const list = await navigator.mediaDevices.enumerateDevices();
for (const device of list) {
if (device.kind in self) {
continue;
}
assert_in_array(device.kind, ['audioinput', 'videoinput', 'audiooutput']);
self[device.kind] = device;
if (device.kind.endsWith('input')) {
inputDevices.push(device.kind);
} else {
outputDevices.push(device.kind);
}
}
} catch (e) {}
try {
self.stream = await navigator.mediaDevices.getUserMedia({audio: true});
self.track = stream.getTracks()[0];
self.trackEvent = new MediaStreamTrackEvent("type", {
track: track,
});
} catch (e) {}
idl_array.add_objects({
InputDeviceInfo: inputDevices,
MediaStream: ['stream', 'new MediaStream()'],
Navigator: ['navigator'],
MediaDevices: ['navigator.mediaDevices'],
MediaDeviceInfo: outputDevices,
MediaStreamTrack: ['track'],
MediaStreamTrackEvent: ['trackEvent'],
OverconstrainedError: ['new OverconstrainedError("constraint")'],
});
}
);