diff --git a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/media_capabilities.rs b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/media_capabilities.rs index 7ba3d8955e..c6d92228a7 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigator_runtime/media_capabilities.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigator_runtime/media_capabilities.rs @@ -29,39 +29,35 @@ struct MediaCapabilitiesPrototypeDeclaration { encoding_info: (), } -#[derive(WebApiObject)] -#[webapi(plain)] -struct MediaCapabilitiesDecodingInfoDeclaration<'scope> { - #[webapi(data_property, enumerable)] - supported: bool, - - #[webapi(data_property, enumerable)] - smooth: bool, - - #[webapi(data_property, enumerable)] +#[derive(webidl::WebIdlDictionary)] +#[webidl(prefix = "MediaCapabilitiesInfo", snapshot)] +struct MediaCapabilitiesInfo { + #[webidl(required)] power_efficient: bool, - - #[webapi(data_property, enumerable)] - key_system_access: v8::Local<'scope, v8::Value>, - - #[webapi(data_property, enumerable)] - configuration: v8::Local<'scope, v8::Object>, + #[webidl(required)] + smooth: bool, + #[webidl(required)] + supported: bool, } -#[derive(WebApiObject)] -#[webapi(plain)] -struct MediaCapabilitiesEncodingInfoDeclaration<'scope> { - #[webapi(data_property, enumerable)] - supported: bool, +#[derive(webidl::WebIdlDictionary)] +#[webidl(prefix = "MediaCapabilitiesDecodingInfo", snapshot)] +struct MediaCapabilitiesDecodingInfo<'s> { + #[webidl(inherit)] + info: MediaCapabilitiesInfo, + #[webidl(required, dictionary)] + configuration: MediaDecodingConfiguration, + #[webidl(required, nullable, interface = web_api_interfaces::MediaKeySystemAccess)] + key_system_access: Option>, +} - #[webapi(data_property, enumerable)] - smooth: bool, - - #[webapi(data_property, enumerable)] - power_efficient: bool, - - #[webapi(data_property, enumerable)] - configuration: v8::Local<'scope, v8::Object>, +#[derive(webidl::WebIdlDictionary)] +#[webidl(prefix = "MediaCapabilitiesEncodingInfo", snapshot)] +struct MediaCapabilitiesEncodingInfo { + #[webidl(inherit)] + info: MediaCapabilitiesInfo, + #[webidl(required, dictionary)] + configuration: MediaEncodingConfiguration, } #[derive(Clone, Copy, Debug, Eq, PartialEq, webidl::WebIdlEnum)] @@ -117,97 +113,93 @@ enum MediaKeysRequirement { #[derive(webidl::WebIdlDictionary)] #[webidl(prefix = "AudioConfiguration", snapshot)] struct AudioConfiguration { - // Snapshot order preserves result keys independently of input conversion. - #[webidl(required, snapshot_order = 0)] + #[webidl(required)] content_type: String, - #[webidl(snapshot_order = 1)] channels: Option, - #[webidl(snapshot_order = 2)] bitrate: Option, - #[webidl(snapshot_order = 3)] samplerate: Option, - #[webidl(snapshot_order = 4)] spatial_rendering: Option, } #[derive(webidl::WebIdlDictionary)] #[webidl(prefix = "VideoConfiguration", snapshot)] struct VideoConfiguration { - #[webidl(required, snapshot_order = 0)] + #[webidl(required)] content_type: String, - #[webidl(required, snapshot_order = 1)] + #[webidl(required)] width: u32, - #[webidl(required, snapshot_order = 2)] + #[webidl(required)] height: u32, - #[webidl(required, snapshot_order = 3)] + #[webidl(required)] bitrate: u64, - #[webidl(required, converter = "double", snapshot_order = 4)] + #[webidl(required, converter = "double")] framerate: f64, - #[webidl(snapshot_order = 5)] has_alpha_channel: Option, - #[webidl(converter = "enum", snapshot_order = 6)] + #[webidl(converter = "enum")] hdr_metadata_type: Option, - #[webidl(converter = "enum", snapshot_order = 7)] + #[webidl(converter = "enum")] color_gamut: Option, - #[webidl(converter = "enum", snapshot_order = 8)] + #[webidl(converter = "enum")] transfer_function: Option, - #[webidl(snapshot_order = 9)] scalability_mode: Option, - #[webidl(snapshot_order = 10)] spatial_scalability: Option, } #[derive(webidl::WebIdlDictionary)] #[webidl(prefix = "KeySystemTrackConfiguration", snapshot)] struct KeySystemTrackConfiguration { - #[webidl(default = "", snapshot_order = 0)] - robustness: String, - #[webidl(with = optional_nullable_dom_string_member, snapshot_order = 1)] - #[webidl(snapshot_kind = webidl::SnapshotKind::Nullable(&webidl::SnapshotKind::Scalar))] - encryption_scheme: Option>, + #[webidl(converter = "raw", default = webidl::DomString16(Vec::new()))] + robustness: webidl::DomString16, + #[webidl(nullable, converter = "raw")] + encryption_scheme: Option, } #[derive(webidl::WebIdlDictionary)] #[webidl(prefix = "MediaCapabilitiesKeySystemConfiguration", snapshot)] struct MediaCapabilitiesKeySystemConfiguration { - #[webidl(required, snapshot_order = 0)] + #[webidl(required)] key_system: String, - #[webidl(default = "", snapshot_order = 1)] + #[webidl(default = "")] init_data_type: String, - #[webidl(converter = "enum", default = MediaKeysRequirement::Optional, snapshot_order = 2)] + #[webidl(converter = "enum", default = MediaKeysRequirement::Optional)] distinctive_identifier: MediaKeysRequirement, - #[webidl(converter = "enum", default = MediaKeysRequirement::Optional, snapshot_order = 3)] + #[webidl(converter = "enum", default = MediaKeysRequirement::Optional)] persistent_state: MediaKeysRequirement, - #[webidl(sequence, snapshot_order = 4)] + #[webidl(sequence)] session_types: Option>, - #[webidl(dictionary, snapshot_order = 5)] + #[webidl(dictionary)] audio: Option, - #[webidl(dictionary, snapshot_order = 6)] + #[webidl(dictionary)] video: Option, } +#[derive(webidl::WebIdlDictionary)] +#[webidl(prefix = "MediaConfiguration", snapshot)] +struct MediaConfiguration { + #[webidl(dictionary)] + audio: Option, + #[webidl(dictionary)] + video: Option, +} + #[derive(webidl::WebIdlDictionary)] #[webidl(prefix = "MediaDecodingConfiguration", snapshot)] struct MediaDecodingConfiguration { - #[webidl(dictionary, snapshot_order = 0)] - audio: Option, - #[webidl(name = "keySystemConfiguration", dictionary, snapshot_order = 3)] + #[webidl(inherit)] + media: MediaConfiguration, + #[webidl(name = "keySystemConfiguration", dictionary)] key_system_configuration: Option, - #[webidl(name = "type", required, converter = "enum", snapshot_order = 2)] + #[webidl(name = "type", required, converter = "enum")] decoding_type: MediaDecodingType, - #[webidl(dictionary, snapshot_order = 1)] - video: Option, } #[derive(webidl::WebIdlDictionary)] #[webidl(prefix = "MediaEncodingConfiguration", snapshot)] struct MediaEncodingConfiguration { - #[webidl(dictionary, snapshot_order = 0)] - audio: Option, - #[webidl(name = "type", required, converter = "enum", snapshot_order = 2)] + #[webidl(inherit)] + media: MediaConfiguration, + #[webidl(name = "type", required, converter = "enum")] encoding_type: MediaEncodingType, - #[webidl(dictionary, snapshot_order = 1)] - video: Option, } #[derive(webidl::WebIdlArgs)] @@ -267,59 +259,42 @@ fn media_capabilities_decoding_info_callback<'s>( return; } - if configuration.key_system_configuration.is_some() { - let (name, message) = if media_capabilities_is_worker(scope, receiver) { - ( - "InvalidStateError", + let supported = if configuration.key_system_configuration.is_some() { + if media_capabilities_is_worker(scope, receiver) { + let reason = crate::context_bootstrap::new_dom_exception_value( + scope, "Encrypted media capabilities are unavailable in workers", - ) - } else if !media_capabilities_is_secure_context(scope, receiver) { - ( - "SecurityError", - "Encrypted media capabilities require a secure context", - ) - } else { - let Some(configuration_object) = - configuration.to_js_snapshot(scope, SnapshotPrototype::Ordinary) - else { - rv.set_undefined(); - return; - }; - let Ok(info) = MediaCapabilitiesDecodingInfoDeclaration::new( - false, - false, - false, - v8::null(scope).into(), - configuration_object, - ) - .bind(scope) else { - rv.set_undefined(); - return; - }; - set_resolved_promise(scope, &mut rv, info.into()); + "InvalidStateError", + ); + set_rejected_promise(scope, &mut rv, reason); return; - }; - let reason = crate::context_bootstrap::new_dom_exception_value(scope, message, name); - set_rejected_promise(scope, &mut rv, reason); - return; - } - - let Some(configuration_object) = - configuration.to_js_snapshot(scope, SnapshotPrototype::Ordinary) - else { - rv.set_undefined(); - return; + } + if !media_capabilities_is_secure_context(scope, receiver) { + let reason = crate::context_bootstrap::new_dom_exception_value( + scope, + "Encrypted media capabilities require a secure context", + "SecurityError", + ); + set_rejected_promise(scope, &mut rv, reason); + return; + } + false + } else { + configuration_supported( + configuration.media.audio.as_ref(), + configuration.media.video.as_ref(), + ) }; - let supported = - configuration_supported(configuration.audio.as_ref(), configuration.video.as_ref()); - let Ok(info) = MediaCapabilitiesDecodingInfoDeclaration::new( - supported, - false, - false, - v8::null(scope).into(), - configuration_object, - ) - .bind(scope) else { + let info = MediaCapabilitiesDecodingInfo { + info: MediaCapabilitiesInfo { + power_efficient: false, + smooth: false, + supported, + }, + configuration, + key_system_access: None, + }; + let Some(info) = info.to_js_snapshot(scope, SnapshotPrototype::Ordinary) else { rv.set_undefined(); return; }; @@ -344,21 +319,19 @@ fn media_capabilities_encoding_info_callback<'s>( set_rejected_promise(scope, &mut rv, reason); return; } - let Some(configuration_object) = - configuration.to_js_snapshot(scope, SnapshotPrototype::Ordinary) - else { - rv.set_undefined(); - return; + let supported = configuration_supported( + configuration.media.audio.as_ref(), + configuration.media.video.as_ref(), + ); + let info = MediaCapabilitiesEncodingInfo { + info: MediaCapabilitiesInfo { + power_efficient: false, + smooth: false, + supported, + }, + configuration, }; - let supported = - configuration_supported(configuration.audio.as_ref(), configuration.video.as_ref()); - let Ok(info) = MediaCapabilitiesEncodingInfoDeclaration::new( - supported, - false, - false, - configuration_object, - ) - .bind(scope) else { + let Some(info) = info.to_js_snapshot(scope, SnapshotPrototype::Ordinary) else { rv.set_undefined(); return; }; @@ -435,30 +408,12 @@ fn media_capabilities_is_worker<'s>( .is_some_and(|value| value.boolean_value(scope)) } -fn optional_nullable_dom_string_member<'s>( - scope: &mut v8::PinScope<'s, '_>, - object: v8::Local<'s, v8::Object>, - name: &'static str, -) -> std::result::Result>, webidl::WebIdlError> { - let context = webidl::Context::member("KeySystemTrackConfiguration", name); - let Some(value) = webidl::property_result(scope, object, name, context)? else { - return Ok(None); - }; - if value.is_undefined() { - return Ok(None); - } - if value.is_null() { - return Ok(Some(None)); - } - webidl::convert::(scope, value, context).map(|value| Some(Some(value.0))) -} - fn validate_decoding_configuration( configuration: &MediaDecodingConfiguration, ) -> std::result::Result<(), &'static str> { validate_media_configuration( - configuration.audio.as_ref(), - configuration.video.as_ref(), + configuration.media.audio.as_ref(), + configuration.media.video.as_ref(), configuration.decoding_type == MediaDecodingType::Webrtc, true, )?; @@ -466,10 +421,10 @@ fn validate_decoding_configuration( if configuration.decoding_type == MediaDecodingType::Webrtc { return Err("keySystemConfiguration is not valid for WebRTC decoding"); } - if key_system.audio.is_some() && configuration.audio.is_none() { + if key_system.audio.is_some() && configuration.media.audio.is_none() { return Err("keySystemConfiguration.audio requires an audio configuration"); } - if key_system.video.is_some() && configuration.video.is_none() { + if key_system.video.is_some() && configuration.media.video.is_none() { return Err("keySystemConfiguration.video requires a video configuration"); } if key_system.session_types.as_ref().is_some_and(|types| { @@ -487,8 +442,8 @@ fn validate_encoding_configuration( configuration: &MediaEncodingConfiguration, ) -> std::result::Result<(), &'static str> { validate_media_configuration( - configuration.audio.as_ref(), - configuration.video.as_ref(), + configuration.media.audio.as_ref(), + configuration.media.video.as_ref(), configuration.encoding_type == MediaEncodingType::Webrtc, false, ) diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/media_capabilities_conversion.js b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/media_capabilities_conversion.js new file mode 100644 index 0000000000..67ff6dd3f1 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/media_capabilities_conversion.js @@ -0,0 +1,44 @@ +globalThis.__mediaCapabilitiesConversionResult = 'pending'; +(async () => { + const rows = []; + for (const method of ['decodingInfo', 'encodingInfo']) { + for (const scenario of ['normal', 'video-before-derived', 'invalid-video-before-type', 'invalid-audio']) { + let reads = ''; + const videoMarker = {}, keyMarker = {}, typeMarker = {}; + const input = new Proxy({ + audio: scenario === 'invalid-audio' ? null : {contentType: 'audio/webm; codecs="opus"'}, + video: scenario === 'invalid-video-before-type' ? null : {contentType: 'video/webm; codecs="vp09.00.10.08"', width: 320, height: 240, bitrate: 200000, framerate: 24}, + type: scenario === 'invalid-video-before-type' ? undefined : method === 'decodingInfo' ? 'file' : 'webrtc' + }, { + get(target, name, receiver) { + reads += name + ','; + if (scenario === 'video-before-derived') { + if (name === 'video') throw videoMarker; + if (name === 'keySystemConfiguration') throw keyMarker; + if (name === 'type') throw typeMarker; + } + return Reflect.get(target, name, receiver); + } + }); + let outcome = 'resolved', isPromise = false, synchronous = false; + try { + let promise; + try { promise = navigator.mediaCapabilities[method](input); } + catch (error) { synchronous = true; throw error; } + isPromise = promise instanceof Promise; + await promise; + } catch (error) { + outcome = error === videoMarker ? 'video' : error === keyMarker ? 'key' : error === typeMarker ? 'type' : error instanceof TypeError ? 'TypeError' : 'other'; + } + rows.push({method, scenario, reads, outcome, isPromise, synchronous}); + } + } + globalThis.__mediaCapabilitiesConversionRows = JSON.stringify(rows); + const matches = rows.every(row => { + const reads = row.scenario === 'invalid-audio' ? 'audio,' : row.scenario !== 'normal' ? 'audio,video,' : row.method === 'decodingInfo' ? 'audio,video,keySystemConfiguration,type,' : 'audio,video,type,'; + const outcome = row.scenario === 'normal' ? 'resolved' : row.scenario === 'video-before-derived' ? 'video' : 'TypeError'; + return row.reads === reads && row.outcome === outcome && row.isPromise && !row.synchronous; + }); + return matches ? 'ok' : JSON.stringify(rows); +})().then(value => globalThis.__mediaCapabilitiesConversionResult = value, + error => globalThis.__mediaCapabilitiesConversionResult = String(error)); diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/media_capabilities_snapshot.js b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/media_capabilities_snapshot.js index 6a28614ef6..938aae28f8 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/media_capabilities_snapshot.js +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/media_capabilities_snapshot.js @@ -24,13 +24,14 @@ globalThis.__mediaCapabilitiesSnapshotResult = 'pending'; }; const promise = capabilities[method](input); check(promise instanceof Promise, 'query returns a Promise'); - check(reads === 'audio,bitrate,channels,contentType,samplerate,spatialRendering,type,video,', 'lexical conversion order'); + check(reads === 'audio,bitrate,channels,contentType,samplerate,spatialRendering,video,type,', 'base dictionary converts before derived members'); for (const key of Object.keys(input)) Object.defineProperty(input, key, {get() { throw new Error('input read after conversion'); }}); for (const key of Object.getOwnPropertyNames(child)) Object.defineProperty(child, key, {get() { throw new Error('nested input read after conversion'); }}); const result = await promise; + check(Object.keys(result).join() === (method === 'decodingInfo' ? 'powerEfficient,smooth,supported,configuration,keySystemAccess' : 'powerEfficient,smooth,supported,configuration'), 'result dictionary inheritance order'); check(result.configuration.audio.channels === '2', 'converted nested result'); check(Object.keys(result.configuration).join() === 'audio,type', 'configuration output order'); - check(Object.keys(result.configuration.audio).join() === 'contentType,channels,bitrate,samplerate,spatialRendering', 'audio output order'); + check(Object.keys(result.configuration.audio).join() === 'bitrate,channels,contentType,samplerate,spatialRendering', 'audio output order'); dataProperties(result, window); const stable = Object.freeze({type: method === 'decodingInfo' ? 'file' : 'record', audio: Object.freeze(audio())}); const first = await capabilities[method](stable); @@ -41,7 +42,7 @@ globalThis.__mediaCapabilitiesSnapshotResult = 'pending'; Object.freeze(second.configuration.audio); check((await capabilities[method](stable)).configuration.audio.channels === '2', 'frozen prior result'); } - for (const scheme of [undefined, null, 'cenc']) { + for (const scheme of [undefined, null, 'cenc', 'cbc\uD800']) { const track = {}; if (scheme !== undefined) track.encryptionScheme = scheme; const input = {type: 'file', audio: audio(), keySystemConfiguration: {keySystem: 'org.w3.clearkey', sessionTypes: ['temporary', 'temporary'], audio: track}}; @@ -49,14 +50,18 @@ globalThis.__mediaCapabilitiesSnapshotResult = 'pending'; input.keySystemConfiguration.sessionTypes[0] = 'changed'; const result = await promise; const key = result.configuration.keySystemConfiguration; - check(Object.keys(result.configuration).join() === 'audio,type,keySystemConfiguration', 'decoding output order'); + check(Object.keys(result.configuration).join() === 'audio,keySystemConfiguration,type', 'decoding output order'); check(key.distinctiveIdentifier === 'optional' && key.persistentState === 'optional' && key.initDataType === '', 'dictionary defaults'); check(key.audio.robustness === '', 'nested dictionary default'); - check(Object.hasOwn(key.audio, 'encryptionScheme') === (scheme !== undefined), 'absent versus nullable member'); - if (scheme !== undefined) check(key.audio.encryptionScheme === scheme, 'nullable member value'); + check(Object.hasOwn(key.audio, 'encryptionScheme'), 'nullable default creates a member'); + check(key.audio.encryptionScheme === (scheme === undefined ? null : scheme), 'nullable member default and value'); + check(Object.keys(key.audio).join() === 'encryptionScheme,robustness', 'nested dictionary member order'); check(key.sessionTypes.join() === 'temporary,temporary' && key.sessionTypes !== input.keySystemConfiguration.sessionTypes, 'copied sequence preserves duplicates'); dataProperties(result, window); } + const codeUnits = '\uD800\u0000\uDCFF'; + const unicode = await capabilities.decodingInfo({type: 'file', audio: audio(), keySystemConfiguration: {keySystem: 'org.w3.clearkey', audio: {robustness: codeUnits, encryptionScheme: codeUnits}}}); + check(unicode.configuration.keySystemConfiguration.audio.robustness === codeUnits && unicode.configuration.keySystemConfiguration.audio.encryptionScheme === codeUnits, 'DOMString code units'); const names = ['audio', 'video', 'type', 'configuration', 'contentType', 'width', 'sessionTypes', 'keySystemConfiguration', 'encryptionScheme']; const marker = {}; for (const mode of ['count', 'throw', 'readonly', 'index']) { diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/navigator_devices.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/navigator_devices.rs index eb16a11016..d3e7f4553c 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/navigator_devices.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/navigator_devices.rs @@ -1,5 +1,18 @@ use super::*; +#[test] +fn media_capabilities_inherited_members_convert_before_derived_getters() { + let mut vm = + new_storage_page_task_executor_test_vm("https://media-capabilities-conversion.test/"); + vm.eval(include_str!("media_capabilities_conversion.js")) + .unwrap(); + vm.eval("0").unwrap(); + assert_eq!( + vm.eval("__mediaCapabilitiesConversionResult").unwrap(), + "ok" + ); +} + #[test] fn media_capabilities_dictionary_snapshots_preserve_conversion_and_define_own_data() { let mut vm =