fix(renderer): unblock Feishin media player initialization

This commit is contained in:
ldm0
2026-11-07 03:32:34 +08:00
committed by Donough Liu
parent e7780afdc4
commit cdc4423cf3
13 changed files with 512 additions and 2 deletions
@@ -5,6 +5,7 @@ mod gamepad;
mod geolocation;
mod media_capabilities;
mod media_devices;
mod media_session;
mod navigator;
mod navigator_subobjects;
mod screen;
@@ -0,0 +1,141 @@
//! Headless media action registration and playback state. Registrations belong
//! to the Navigator's realm and retain callbacks through traced native slots;
//! they do not synthesize media-key events or control an operating-system player.
use super::super::*;
use crate::util::{get_private_value, set_private_value};
use crate::v8_traced_webidl_callback::V8TracedWebIdlCallbackFunction;
use crate::{web_api_interfaces, webidl};
use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
const HANDLERS: &str = "__moliMediaSessionHandlers";
const PLAYBACK_STATE: &str = "__moliMediaSessionPlaybackState";
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::MediaSession)]
struct MediaSessionObjectDeclaration {
#[webapi(slot = HANDLERS, init = "null_object")]
handlers: (),
#[webapi(slot = PLAYBACK_STATE)]
playback_state: &'static str,
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::MediaSession, enumerable, receiver)]
struct MediaSessionPrototypeDeclaration {
#[webapi(method, length = 2, callback = set_action_handler)]
set_action_handler: (),
#[webapi(accessor_property, getter = playback_state, setter = set_playback_state)]
playback_state: (),
}
#[derive(Clone, Copy, webidl::WebIdlEnum)]
#[webidl(name = "MediaSessionAction")]
enum MediaSessionAction {
Play,
Pause,
Seekbackward,
Seekforward,
Previoustrack,
Nexttrack,
Skipad,
Stop,
Seekto,
Togglemicrophone,
Togglecamera,
Hangup,
Previousslide,
Nextslide,
Enterpictureinpicture,
}
#[derive(Clone, Copy, webidl::WebIdlEnum)]
#[webidl(name = "MediaSessionPlaybackState")]
enum PlaybackState {
None,
Paused,
Playing,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "MediaSession.setActionHandler")]
struct SetActionHandlerArgs {
#[webidl(required, converter = "enum")]
action: MediaSessionAction,
#[webidl(required, nullable, converter = "callback_function")]
handler: Option<webidl::WebIdlCallbackFunction>,
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "MediaSession.playbackState")]
struct PlaybackStateArgs {
#[webidl(required)]
value: String,
}
pub(super) fn build<'s>(scope: &mut v8::PinScope<'s, '_>) -> Result<v8::Local<'s, v8::Object>> {
MediaSessionObjectDeclaration::new("none")
.bind(scope)
.map_err(|error| anyhow!("failed to bind MediaSession object: {error}"))
}
pub(super) fn install<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
interface_name: &str,
) {
if interface_name == "MediaSession" {
MediaSessionPrototypeDeclaration::initialize_prototype_template(
scope,
template.prototype_template(scope),
);
}
}
fn set_action_handler<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<SetActionHandlerArgs>(scope, &args) else {
return;
};
let handlers = get_private_value(scope, args.this(), HANDLERS)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
.expect("MediaSession has native action handlers");
let action = v8str(
scope,
webidl::WebIdlEnumSnapshot::snapshot_token(&parsed.action),
);
if let Some(handler) = parsed.handler {
let callback = V8TracedWebIdlCallbackFunction::from_callback(scope, &handler).into_object();
let _ = handlers.create_data_property(scope, action.into(), callback.into());
} else {
let _ = handlers.delete(scope, action.into());
}
}
fn playback_state<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
if let Some(value) = get_private_value(scope, args.this(), PLAYBACK_STATE) {
rv.set(value);
}
}
fn set_playback_state<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<PlaybackStateArgs>(scope, &args) else {
return;
};
// Like other Web IDL enum attributes, invalid tokens leave the state intact.
if let Some(state) = <PlaybackState as webidl::WebIdlEnum>::parse_token(&parsed.value) {
let value = v8str(scope, webidl::WebIdlEnumSnapshot::snapshot_token(&state));
set_private_value(scope, args.this(), PLAYBACK_STATE, value.into());
}
}
@@ -75,6 +75,7 @@ const NAVIGATOR_RUNTIME_DATA_KEYS: &[&str] = &[
"geolocation",
"mediaCapabilities",
"credentials",
"mediaSession",
];
const WORKER_NAVIGATOR_INSTALLED_SLOT: &str = "__moliWorkerNavigatorInstalled";
const WORKER_NAVIGATOR_MATERIALIZING_SLOT: &str = "__moliWorkerNavigatorMaterializing";
@@ -193,6 +194,8 @@ struct NavigatorRuntimeDataPrototypeDeclaration {
geolocation: (),
#[webapi(accessor_property, getter = navigator_runtime_data_getter_callback, data = callback_data_index_value(scope, 32))]
media_capabilities: (),
#[webapi(accessor_property, getter = navigator_runtime_data_getter_callback, data = callback_data_index_value(scope, 34), receiver = web_api_interfaces::Navigator::is_instance)]
media_session: (),
#[webapi(accessor_property, getter = navigator_cookie_enabled_getter_callback, receiver = web_api_interfaces::Navigator::is_instance)]
cookie_enabled: (),
}
@@ -844,6 +847,7 @@ pub(in crate::context_bootstrap) fn install_navigator_template_bindings<'s>(
super::gamepad::install(scope, template, interface_name);
install_media_capabilities_template_bindings(scope, template, interface_name);
super::credentials::install(scope, template, interface_name);
super::media_session::install(scope, template, interface_name);
let prototype = template.prototype_template(scope);
match interface_name {
"MediaDevices" | "MediaDeviceInfo" | "InputDeviceInfo" => {
@@ -1055,6 +1059,7 @@ pub(super) fn build_lazy_navigator_subobject_in_current_realm<'s>(
}
NavigatorSubobject::Clipboard => build_clipboard_object(scope)?.into(),
NavigatorSubobject::Credentials => super::credentials::build_container(scope)?.into(),
NavigatorSubobject::MediaSession => super::media_session::build(scope)?.into(),
NavigatorSubobject::UserActivation => build_user_activation(scope)?.into(),
NavigatorSubobject::StorageBuckets => {
build_storage_bucket_manager(scope, owner_child, owner_popup)?.into()
@@ -22,12 +22,13 @@ pub(super) enum NavigatorSubobject {
StorageBuckets,
Geolocation,
MediaCapabilities,
MediaSession,
Credentials,
}
impl NavigatorSubobject {
#[cfg(test)]
pub(super) const ALL: [Self; 17] = [
pub(super) const ALL: [Self; 18] = [
Self::Languages,
Self::MimeTypes,
Self::Plugins,
@@ -44,6 +45,7 @@ impl NavigatorSubobject {
Self::StorageBuckets,
Self::Geolocation,
Self::MediaCapabilities,
Self::MediaSession,
Self::Credentials,
];
@@ -65,6 +67,7 @@ impl NavigatorSubobject {
"storageBuckets" => Some(Self::StorageBuckets),
"geolocation" => Some(Self::Geolocation),
"mediaCapabilities" => Some(Self::MediaCapabilities),
"mediaSession" => Some(Self::MediaSession),
"credentials" => Some(Self::Credentials),
_ => None,
}
@@ -88,6 +91,7 @@ impl NavigatorSubobject {
Self::StorageBuckets => "storageBuckets",
Self::Geolocation => "geolocation",
Self::MediaCapabilities => "mediaCapabilities",
Self::MediaSession => "mediaSession",
Self::Credentials => "credentials",
}
}
@@ -1575,6 +1575,14 @@ const CONSTRUCTOR_SPECS_AFTER_STREAMS: &[ConstructorSpec] = &[
interface: web_api_interfaces::OscillatorNode::DESCRIPTOR,
kind: ConstructorKind::OscillatorNode,
},
ConstructorSpec {
interface: web_api_interfaces::GainNode::DESCRIPTOR,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::MediaElementAudioSourceNode::DESCRIPTOR,
kind: ConstructorKind::Illegal,
},
ConstructorSpec {
interface: web_api_interfaces::DynamicsCompressorNode::DESCRIPTOR,
kind: ConstructorKind::DynamicsCompressorNode,
@@ -10,8 +10,10 @@ use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject};
mod audio_param;
mod biquad;
mod gain;
mod graph;
mod listener;
mod media_element_source;
mod node;
use audio_param::{audio_param, detune_param};
@@ -353,6 +355,8 @@ struct AnalyserFftSizeArgs {
struct AudioContextTemplateDeclaration {
#[webapi(method = "close", length = 0, callback = audio_context_close_callback)]
close: (),
#[webapi(method = "createMediaElementSource", length = 1, callback = media_element_source::create, receiver = web_api_interfaces::AudioContext::is_instance)]
create_media_element_source: (),
}
#[derive(WebApiFunctionTemplate)]
@@ -395,6 +399,9 @@ struct BaseAudioContextPrototypeDeclaration {
#[webapi(method = "createBiquadFilter", length = 0, callback = biquad::create_biquad_filter)]
create_biquad_filter: (),
#[webapi(method = "createGain", length = 0, callback = gain::create)]
create_gain: (),
#[webapi(
method = "createOscillator",
length = 0,
@@ -465,6 +472,8 @@ pub(in crate::context_bootstrap) fn install_web_audio_template_bindings<'s>(
"AudioParam" => audio_param::install(scope, template),
"AudioListener" => listener::install(scope, template),
"BiquadFilterNode" => biquad::install(scope, template),
"GainNode" => gain::install(scope, template),
"MediaElementAudioSourceNode" => media_element_source::install(scope, template),
"BaseAudioContext" => {
BaseAudioContextPrototypeDeclaration::initialize_prototype_template(
scope,
@@ -0,0 +1,53 @@
use super::*;
const GAIN: &str = "__moliGainNodeGain";
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::GainNode)]
struct GainNodeObjectDeclaration<'scope> {
#[webapi(slot = GAIN)]
gain: v8::Local<'scope, v8::Object>,
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::GainNode, enumerable, receiver)]
struct GainNodePrototypeDeclaration {
#[webapi(accessor_property, getter = gain)]
gain: (),
}
pub(super) fn install<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
) {
GainNodePrototypeDeclaration::initialize_prototype_template(
scope,
template.prototype_template(scope),
);
}
pub(super) fn create<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
if !require_base_audio_context(scope, args.this()) {
return;
}
let gain = audio_param(scope, 1.0, f32::MIN as f64, f32::MAX as f64);
let node = GainNodeObjectDeclaration::new(gain)
.bind(scope)
.expect("GainNode declaration should bind");
graph::initialize_node(scope, node, args.this());
rv.set(node.into());
}
fn gain<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
if let Some(value) = get_private_value(scope, args.this(), GAIN) {
rv.set(value);
}
}
@@ -0,0 +1,77 @@
//! Media-element source ownership and graph wiring for the existing partial
//! audio backend. Decoded audio transport remains with the media backend.
use super::*;
const ELEMENT: &str = "__moliAudioSourceMediaElement";
const SOURCE: &str = "__moliMediaElementAudioSource";
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::MediaElementAudioSourceNode)]
struct MediaElementSourceObjectDeclaration<'scope> {
#[webapi(slot = ELEMENT)]
element: v8::Local<'scope, v8::Object>,
}
#[derive(WebApiFunctionTemplate)]
#[webapi(interface = web_api_interfaces::MediaElementAudioSourceNode, enumerable, receiver)]
struct MediaElementSourcePrototypeDeclaration {
#[webapi(accessor_property, getter = media_element)]
media_element: (),
}
#[derive(webidl::WebIdlArgs)]
#[webidl(prefix = "AudioContext.createMediaElementSource")]
struct CreateSourceArgs<'s> {
#[webidl(required, interface = web_api_interfaces::HTMLMediaElement)]
media_element: v8::Local<'s, v8::Object>,
}
pub(super) fn install<'s>(
scope: &mut v8::PinScope<'s, '_, ()>,
template: v8::Local<'s, v8::FunctionTemplate>,
) {
MediaElementSourcePrototypeDeclaration::initialize_prototype_template(
scope,
template.prototype_template(scope),
);
}
pub(super) fn create<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let Some(parsed) = webidl::parse_args::<CreateSourceArgs>(scope, &args) else {
return;
};
let element = parsed.media_element;
if get_private_value(scope, element, SOURCE).is_some() {
throw_dom_exception(
scope,
"InvalidStateError",
11,
"The media element is already connected to an audio source node.",
);
return;
}
let node = MediaElementSourceObjectDeclaration::new(element)
.bind(scope)
.expect("MediaElementAudioSourceNode declaration should bind");
graph::initialize_node(scope, node, args.this());
// A source has no input port. It is driven by its media element, not by
// AudioScheduledSourceNode.start() or the synthetic offline oscillator.
graph::initialize_source(scope, node);
set_private_value(scope, element, SOURCE, node.into());
rv.set(node.into());
}
fn media_element<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
if let Some(value) = get_private_value(scope, args.this(), ELEMENT) {
rv.set(value);
}
}
@@ -54,7 +54,8 @@ pub(super) fn install<'s>(
}
pub(super) fn initialize<'s>(scope: &mut v8::PinScope<'s, '_>, node: v8::Local<'s, v8::Object>) {
let source = web_api_interfaces::OscillatorNode::is_instance(scope, node);
let source = web_api_interfaces::OscillatorNode::is_instance(scope, node)
|| web_api_interfaces::MediaElementAudioSourceNode::is_instance(scope, node);
let destination = web_api_interfaces::AudioDestinationNode::is_instance(scope, node);
let compressor = web_api_interfaces::DynamicsCompressorNode::is_instance(scope, node);
set_web_audio_number_slot(scope, node, INPUT_COUNT, if source { 0.0 } else { 1.0 });
@@ -0,0 +1,46 @@
// Feishin's startup registers media controls before connecting two playback
// gains through a preamp, equalizer and compressor. Both missing APIs used to
// throw during React effects and leave the application root empty.
// Also run against Chrome 157.0.8103.38 as the compatibility oracle.
(() => {
const assert = (ok, message) => { if (!ok) throw new Error(message); };
const session = navigator.mediaSession;
const actions = ['nexttrack', 'pause', 'play', 'previoustrack', 'seekto', 'stop', 'seekbackward', 'seekforward'];
for (const action of actions) session.setActionHandler(action, () => {});
const context = new AudioContext({latencyHint: 'playback'});
const gains = [context.createGain(), context.createGain()];
const preamp = context.createGain();
preamp.gain.value = 1;
const filters = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000].map(frequency => {
const filter = context.createBiquadFilter();
filter.type = 'peaking';
filter.frequency.value = frequency;
filter.Q.value = 1.41;
filter.gain.value = 0;
return filter;
});
const compressor = context.createDynamicsCompressor();
compressor.threshold.value = 0;
compressor.ratio.value = 1;
compressor.attack.value = 0;
compressor.release.value = 0.25;
compressor.knee.value = 0;
for (const gain of gains) assert(gain.connect(preamp) === preamp, 'playback gains');
preamp.connect(filters[0]);
for (let i = 0; i < filters.length - 1; i++) filters[i].connect(filters[i + 1]);
filters.at(-1).connect(compressor);
compressor.connect(context.destination);
const sources = gains.map(gain => {
const element = document.createElement('audio');
const source = context.createMediaElementSource(element);
assert(source.mediaElement === element, 'media element ownership');
source.connect(gain);
return source;
});
document.body.innerHTML = '<main id="root">Player initialized</main>';
assert(document.getElementById('root').textContent === 'Player initialized', 'startup completed');
for (const action of actions) session.setActionHandler(action, null);
for (const node of [...sources, ...gains, preamp, ...filters, compressor]) node.disconnect();
context.close();
return 'ok';
})()
@@ -0,0 +1,162 @@
use super::*;
#[test]
fn media_player_initialization_can_register_controls_and_connect_gain_nodes() {
let mut vm = new_storage_html_test_vm("http://media-player.test/");
assert_eq!(
vm.eval(include_str!("media_player_init.js"))
.expect("media-player initialization should complete"),
"ok"
);
}
#[test]
fn media_session_is_same_object_with_native_receivers_and_per_window_state() {
let mut vm = new_storage_html_test_vm("http://media-session.test/");
assert_eq!(
vm.eval(r#"
(() => {
const assert = (ok, message) => { if (!ok) throw new Error(message); };
const typeError = (fn) => {
try { fn(); } catch (error) { assert(error.name === 'TypeError', error.name); return; }
throw new Error('expected TypeError');
};
const session = navigator.mediaSession;
assert(session === navigator.mediaSession && session instanceof MediaSession, 'SameObject');
assert(Object.getPrototypeOf(session) === MediaSession.prototype, 'native prototype');
assert(!Object.hasOwn(navigator, 'mediaSession'), 'prototype getter');
assert(Reflect.set(navigator, 'mediaSession', {}) === false, 'readonly');
assert(session.playbackState === 'none', 'initial playback state');
typeError(() => new MediaSession());
assert(session.setActionHandler.length === 2, 'method arity');
for (const action of ['play','pause','seekbackward','seekforward','previoustrack',
'nexttrack','skipad','stop','seekto','togglemicrophone','togglecamera','hangup',
'previousslide','nextslide','enterpictureinpicture']) {
assert(session.setActionHandler(action, () => { throw new Error('must not invoke'); }) === undefined, 'register');
session.setActionHandler(action, null);
session.setActionHandler(action, undefined);
}
typeError(() => session.setActionHandler());
typeError(() => session.setActionHandler('play'));
typeError(() => session.setActionHandler('unknown', () => {}));
for (const bad of [0, 'callback', {}, {handleEvent(){}}]) {
typeError(() => session.setActionHandler('play', bad));
}
const marker = new Error('conversion');
try { session.setActionHandler({toString(){throw marker;}}, null); }
catch (e) { assert(e === marker, 'action conversion exception'); }
for (const state of ['playing','paused','none']) {
session.playbackState = state;
assert(session.playbackState === state, 'playback state '+state);
session.playbackState = 'invalid';
assert(session.playbackState === state, 'invalid enum ignored');
}
typeError(() => {session.playbackState = Symbol();});
const descriptor = Object.getOwnPropertyDescriptor(MediaSession.prototype, 'playbackState');
for (const bad of [{}, MediaSession.prototype, Object.create(session), new Proxy(session,{})]) {
typeError(() => session.setActionHandler.call(bad, 'play', null));
typeError(() => descriptor.get.call(bad));
typeError(() => descriptor.set.call(bad, 'playing'));
}
document.body.innerHTML = '<iframe></iframe>';
const other = document.querySelector('iframe').contentWindow;
const getter = Object.getOwnPropertyDescriptor(Navigator.prototype, 'mediaSession').get;
const otherSession = getter.call(other.navigator);
assert(otherSession === other.navigator.mediaSession && otherSession !== session, 'per-window identity');
assert(Object.getPrototypeOf(otherSession) === other.MediaSession.prototype, 'receiver realm');
otherSession.playbackState = 'playing';
assert(session.playbackState === 'none', 'isolated state');
session.setActionHandler.call(otherSession, 'play', null);
return 'ok';
})()
"#).expect("MediaSession identity and Web IDL contracts should evaluate"),
"ok"
);
}
#[test]
fn gain_factory_creates_independent_parameters_and_validates_graph_connections() {
let mut vm = new_storage_html_test_vm("https://gain-node.test/");
assert_eq!(vm.eval(r#"
(() => {
const assert = (ok, message) => { if (!ok) throw new Error(message); };
const throws = (name, fn) => {
try { fn(); } catch (e) { assert(e.name === name, e.name+' != '+name); return; }
throw new Error('expected '+name);
};
assert(Object.hasOwn(BaseAudioContext.prototype, 'createGain'), 'shared factory');
assert(BaseAudioContext.prototype.createGain.length === 0, 'factory arity');
for (const context of [new AudioContext(), new OfflineAudioContext(1,64,44100)]) {
const gain = context.createGain(), other = context.createGain();
assert(gain instanceof GainNode && gain instanceof AudioNode, 'node inheritance');
assert(gain.context === context, 'context ownership');
assert(gain.numberOfInputs === 1 && gain.numberOfOutputs === 1, 'ports');
assert(gain.channelCount === 2 && gain.channelCountMode === 'max' && gain.channelInterpretation === 'speakers', 'channel defaults');
assert(gain.gain instanceof AudioParam && gain.gain === gain.gain, 'SameObject parameter');
assert(gain.gain.value === 1 && gain.gain.defaultValue === 1, 'default gain');
assert(gain.gain.minValue === -3.4028234663852886e38 && gain.gain.maxValue === 3.4028234663852886e38, 'float bounds');
assert(!Object.hasOwn(gain,'gain') && Reflect.set(gain,'gain',{}) === false, 'readonly parameter');
gain.gain.value = 0.5;
assert(other.gain.value === 1 && gain.gain.defaultValue === 1, 'independent parameters');
assert(gain.gain.setValueAtTime(0.25,0) === gain.gain, 'existing AudioParam methods');
assert(gain.connect(other) === other && other.connect(context.destination) === context.destination, 'graph connectivity');
throws('InvalidAccessError', () => gain.connect(new AudioContext().destination));
throws('IndexSizeError', () => gain.connect(other,1));
assert(gain.disconnect(other) === undefined, 'remove connection');
throws('InvalidAccessError', () => gain.disconnect(other));
const getter = Object.getOwnPropertyDescriptor(GainNode.prototype,'gain').get;
for (const bad of [{}, GainNode.prototype, Object.create(gain), new Proxy(gain,{})]) {
throws('TypeError', () => getter.call(bad));
}
for (const bad of [{}, BaseAudioContext.prototype, Object.create(context), new Proxy(context,{})]) {
throws('TypeError', () => BaseAudioContext.prototype.createGain.call(bad));
}
}
return 'ok';
})()
"#).expect("GainNode factory and graph contracts should evaluate"), "ok");
}
#[test]
fn media_element_audio_source_validates_ownership_receivers_and_ports() {
let mut vm = new_storage_html_test_vm("http://media-source-node.test/");
assert_eq!(vm.eval(r#"
(() => {
const assert = (ok, message) => { if (!ok) throw new Error(message); };
const throws = (name, fn) => {
try { fn(); } catch (e) { assert(e.name === name, e.name+' != '+name); return; }
throw new Error('expected '+name);
};
const context = new AudioContext();
assert(context.createMediaElementSource.length === 1, 'method arity');
assert(typeof OfflineAudioContext.prototype.createMediaElementSource === 'undefined', 'realtime only');
for (const tag of ['audio','video']) {
const element = document.createElement(tag);
const source = context.createMediaElementSource(element);
assert(source instanceof MediaElementAudioSourceNode && source instanceof AudioNode, 'inheritance');
assert(source.context === context && source.mediaElement === element, 'ownership');
assert(source.numberOfInputs === 0 && source.numberOfOutputs === 1, 'source ports');
assert(Reflect.set(source,'mediaElement',{}) === false, 'readonly media element');
const gain = context.createGain();
assert(source.connect(gain) === gain, 'source connection');
throws('IndexSizeError', () => gain.connect(source));
throws('InvalidStateError', () => context.createMediaElementSource(element));
throws('InvalidStateError', () => new AudioContext().createMediaElementSource(element));
source.disconnect();
throws('InvalidStateError', () => context.createMediaElementSource(element));
assert(context.createMediaElementSource(element.cloneNode()).mediaElement !== element, 'clone has independent ownership');
const getter = Object.getOwnPropertyDescriptor(MediaElementAudioSourceNode.prototype,'mediaElement').get;
for (const bad of [{}, Object.create(source), new Proxy(source,{})]) {
throws('TypeError', () => getter.call(bad));
}
}
for (const bad of [undefined, null, {}, document.createElement('div'), Object.create(new Audio()), new Proxy(new Audio(),{})]) {
throws('TypeError', () => context.createMediaElementSource(bad));
}
for (const bad of [{}, new OfflineAudioContext(1,64,44100), Object.create(context), new Proxy(context,{})]) {
throws('TypeError', () => context.createMediaElementSource.call(bad,new Audio()));
}
return 'ok';
})()
"#).expect("MediaElementAudioSourceNode contracts should evaluate"), "ok");
}
@@ -42,6 +42,7 @@ mod input_event_init;
mod legacy_event_init;
mod media;
mod media_devices;
mod media_player_init;
mod media_query_list_events;
mod message_channel;
mod message_port_events;
@@ -190,6 +190,7 @@ interfaces! {
FormData;
FormDataIterator = "FormData Iterator";
FormDataEvent: Event;
GainNode: AudioNode;
Geolocation;
GeolocationCoordinates;
GeolocationPosition;
@@ -321,6 +322,7 @@ interfaces! {
MediaStreamTrack: EventTarget;
CanvasCaptureMediaStreamTrack: MediaStreamTrack;
MediaDevices: EventTarget;
MediaElementAudioSourceNode: AudioNode;
MediaError;
MediaKeySession: EventTarget;
MediaKeyStatusMap;