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 9c32b5d565..7ba3d8955e 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 @@ -1,6 +1,9 @@ use super::super::*; use crate::web_api_interfaces; -use crate::{util::get_private_value, webidl}; +use crate::{ + util::get_private_value, + webidl::{self, SnapshotPrototype, WebIdlDictionarySerialize}, +}; use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; const MEDIA_CAPABILITIES_SECURE_CONTEXT_SLOT: &str = "__moliMediaCapabilitiesSecureContext"; @@ -69,16 +72,6 @@ enum MediaDecodingType { Webrtc, } -impl MediaDecodingType { - fn as_str(self) -> &'static str { - match self { - Self::File => "file", - Self::MediaSource => "media-source", - Self::Webrtc => "webrtc", - } - } -} - #[derive(Clone, Copy, Debug, Eq, PartialEq, webidl::WebIdlEnum)] #[webidl(name = "MediaEncodingType")] enum MediaEncodingType { @@ -86,15 +79,6 @@ enum MediaEncodingType { Webrtc, } -impl MediaEncodingType { - fn as_str(self) -> &'static str { - match self { - Self::Record => "record", - Self::Webrtc => "webrtc", - } - } -} - #[derive(Clone, Copy, Debug, webidl::WebIdlEnum)] #[webidl(name = "HdrMetadataType")] enum HdrMetadataType { @@ -106,16 +90,6 @@ enum HdrMetadataType { SmpteSt2094_40, } -impl HdrMetadataType { - fn as_str(self) -> &'static str { - match self { - Self::SmpteSt2086 => "smpteSt2086", - Self::SmpteSt2094_10 => "smpteSt2094-10", - Self::SmpteSt2094_40 => "smpteSt2094-40", - } - } -} - #[derive(Clone, Copy, Debug, webidl::WebIdlEnum)] #[webidl(name = "ColorGamut")] enum ColorGamut { @@ -124,16 +98,6 @@ enum ColorGamut { Rec2020, } -impl ColorGamut { - fn as_str(self) -> &'static str { - match self { - Self::Srgb => "srgb", - Self::P3 => "p3", - Self::Rec2020 => "rec2020", - } - } -} - #[derive(Clone, Copy, Debug, webidl::WebIdlEnum)] #[webidl(name = "TransferFunction")] enum TransferFunction { @@ -142,16 +106,6 @@ enum TransferFunction { Hlg, } -impl TransferFunction { - fn as_str(self) -> &'static str { - match self { - Self::Srgb => "srgb", - Self::Pq => "pq", - Self::Hlg => "hlg", - } - } -} - #[derive(Clone, Copy, Debug, webidl::WebIdlEnum)] #[webidl(name = "MediaKeysRequirement", rename_all = "kebab-case")] enum MediaKeysRequirement { @@ -160,100 +114,99 @@ enum MediaKeysRequirement { NotAllowed, } -impl MediaKeysRequirement { - fn as_str(self) -> &'static str { - match self { - Self::Required => "required", - Self::Optional => "optional", - Self::NotAllowed => "not-allowed", - } - } -} - #[derive(webidl::WebIdlDictionary)] -#[webidl(prefix = "AudioConfiguration")] +#[webidl(prefix = "AudioConfiguration", snapshot)] struct AudioConfiguration { - #[webidl(required)] + // Snapshot order preserves result keys independently of input conversion. + #[webidl(required, snapshot_order = 0)] 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")] +#[webidl(prefix = "VideoConfiguration", snapshot)] struct VideoConfiguration { - #[webidl(required)] + #[webidl(required, snapshot_order = 0)] content_type: String, - #[webidl(required)] + #[webidl(required, snapshot_order = 1)] width: u32, - #[webidl(required)] + #[webidl(required, snapshot_order = 2)] height: u32, - #[webidl(required)] + #[webidl(required, snapshot_order = 3)] bitrate: u64, - #[webidl(required, converter = "double")] + #[webidl(required, converter = "double", snapshot_order = 4)] framerate: f64, + #[webidl(snapshot_order = 5)] has_alpha_channel: Option, - #[webidl(converter = "enum")] + #[webidl(converter = "enum", snapshot_order = 6)] hdr_metadata_type: Option, - #[webidl(converter = "enum")] + #[webidl(converter = "enum", snapshot_order = 7)] color_gamut: Option, - #[webidl(converter = "enum")] + #[webidl(converter = "enum", snapshot_order = 8)] transfer_function: Option, + #[webidl(snapshot_order = 9)] scalability_mode: Option, + #[webidl(snapshot_order = 10)] spatial_scalability: Option, } #[derive(webidl::WebIdlDictionary)] -#[webidl(prefix = "KeySystemTrackConfiguration")] +#[webidl(prefix = "KeySystemTrackConfiguration", snapshot)] struct KeySystemTrackConfiguration { - #[webidl(default = "")] + #[webidl(default = "", snapshot_order = 0)] robustness: String, - #[webidl(with = optional_nullable_dom_string_member)] + #[webidl(with = optional_nullable_dom_string_member, snapshot_order = 1)] + #[webidl(snapshot_kind = webidl::SnapshotKind::Nullable(&webidl::SnapshotKind::Scalar))] encryption_scheme: Option>, } #[derive(webidl::WebIdlDictionary)] -#[webidl(prefix = "MediaCapabilitiesKeySystemConfiguration")] +#[webidl(prefix = "MediaCapabilitiesKeySystemConfiguration", snapshot)] struct MediaCapabilitiesKeySystemConfiguration { - #[webidl(required)] + #[webidl(required, snapshot_order = 0)] key_system: String, - #[webidl(default = "")] + #[webidl(default = "", snapshot_order = 1)] init_data_type: String, - #[webidl(converter = "enum", default = MediaKeysRequirement::Optional)] + #[webidl(converter = "enum", default = MediaKeysRequirement::Optional, snapshot_order = 2)] distinctive_identifier: MediaKeysRequirement, - #[webidl(converter = "enum", default = MediaKeysRequirement::Optional)] + #[webidl(converter = "enum", default = MediaKeysRequirement::Optional, snapshot_order = 3)] persistent_state: MediaKeysRequirement, - #[webidl(sequence)] + #[webidl(sequence, snapshot_order = 4)] session_types: Option>, - #[webidl(dictionary)] + #[webidl(dictionary, snapshot_order = 5)] audio: Option, - #[webidl(dictionary)] + #[webidl(dictionary, snapshot_order = 6)] video: Option, } #[derive(webidl::WebIdlDictionary)] -#[webidl(prefix = "MediaDecodingConfiguration")] +#[webidl(prefix = "MediaDecodingConfiguration", snapshot)] struct MediaDecodingConfiguration { - #[webidl(dictionary)] + #[webidl(dictionary, snapshot_order = 0)] audio: Option, - #[webidl(name = "keySystemConfiguration", dictionary)] + #[webidl(name = "keySystemConfiguration", dictionary, snapshot_order = 3)] key_system_configuration: Option, - #[webidl(name = "type", required, converter = "enum")] + #[webidl(name = "type", required, converter = "enum", snapshot_order = 2)] decoding_type: MediaDecodingType, - #[webidl(dictionary)] + #[webidl(dictionary, snapshot_order = 1)] video: Option, } #[derive(webidl::WebIdlDictionary)] -#[webidl(prefix = "MediaEncodingConfiguration")] +#[webidl(prefix = "MediaEncodingConfiguration", snapshot)] struct MediaEncodingConfiguration { - #[webidl(dictionary)] + #[webidl(dictionary, snapshot_order = 0)] audio: Option, - #[webidl(name = "type", required, converter = "enum")] + #[webidl(name = "type", required, converter = "enum", snapshot_order = 2)] encoding_type: MediaEncodingType, - #[webidl(dictionary)] + #[webidl(dictionary, snapshot_order = 1)] video: Option, } @@ -326,7 +279,8 @@ fn media_capabilities_decoding_info_callback<'s>( "Encrypted media capabilities require a secure context", ) } else { - let Some(configuration_object) = decoding_configuration_object(scope, &configuration) + let Some(configuration_object) = + configuration.to_js_snapshot(scope, SnapshotPrototype::Ordinary) else { rv.set_undefined(); return; @@ -350,7 +304,9 @@ fn media_capabilities_decoding_info_callback<'s>( return; } - let Some(configuration_object) = decoding_configuration_object(scope, &configuration) else { + let Some(configuration_object) = + configuration.to_js_snapshot(scope, SnapshotPrototype::Ordinary) + else { rv.set_undefined(); return; }; @@ -388,7 +344,9 @@ fn media_capabilities_encoding_info_callback<'s>( set_rejected_promise(scope, &mut rv, reason); return; } - let Some(configuration_object) = encoding_configuration_object(scope, &configuration) else { + let Some(configuration_object) = + configuration.to_js_snapshot(scope, SnapshotPrototype::Ordinary) + else { rv.set_undefined(); return; }; @@ -639,210 +597,3 @@ fn configuration_supported( != moli_web_mime::MediaMimeSupport::Unsupported }) } - -fn decoding_configuration_object<'s>( - scope: &mut v8::PinScope<'s, '_>, - configuration: &MediaDecodingConfiguration, -) -> Option> { - let object = media_configuration_object( - scope, - configuration.audio.as_ref(), - configuration.video.as_ref(), - )?; - set_string_property(scope, object, "type", configuration.decoding_type.as_str())?; - if let Some(key_system) = configuration.key_system_configuration.as_ref() { - let value = key_system_configuration_object(scope, key_system)?; - set_object_property(scope, object, "keySystemConfiguration", value)?; - } - Some(object) -} - -fn encoding_configuration_object<'s>( - scope: &mut v8::PinScope<'s, '_>, - configuration: &MediaEncodingConfiguration, -) -> Option> { - let object = media_configuration_object( - scope, - configuration.audio.as_ref(), - configuration.video.as_ref(), - )?; - set_string_property(scope, object, "type", configuration.encoding_type.as_str())?; - Some(object) -} - -fn media_configuration_object<'s>( - scope: &mut v8::PinScope<'s, '_>, - audio: Option<&AudioConfiguration>, - video: Option<&VideoConfiguration>, -) -> Option> { - let object = v8::Object::new(scope); - if let Some(audio) = audio { - let audio = audio_configuration_object(scope, audio)?; - set_object_property(scope, object, "audio", audio)?; - } - if let Some(video) = video { - let video = video_configuration_object(scope, video)?; - set_object_property(scope, object, "video", video)?; - } - Some(object) -} - -fn audio_configuration_object<'s>( - scope: &mut v8::PinScope<'s, '_>, - configuration: &AudioConfiguration, -) -> Option> { - let object = v8::Object::new(scope); - set_string_property(scope, object, "contentType", &configuration.content_type)?; - if let Some(value) = configuration.channels.as_deref() { - set_string_property(scope, object, "channels", value)?; - } - if let Some(value) = configuration.bitrate { - set_number_property(scope, object, "bitrate", value as f64)?; - } - if let Some(value) = configuration.samplerate { - set_number_property(scope, object, "samplerate", f64::from(value))?; - } - if let Some(value) = configuration.spatial_rendering { - set_bool_property(scope, object, "spatialRendering", value)?; - } - Some(object) -} - -fn video_configuration_object<'s>( - scope: &mut v8::PinScope<'s, '_>, - configuration: &VideoConfiguration, -) -> Option> { - let object = v8::Object::new(scope); - set_string_property(scope, object, "contentType", &configuration.content_type)?; - set_number_property(scope, object, "width", f64::from(configuration.width))?; - set_number_property(scope, object, "height", f64::from(configuration.height))?; - set_number_property(scope, object, "bitrate", configuration.bitrate as f64)?; - set_number_property(scope, object, "framerate", configuration.framerate)?; - if let Some(value) = configuration.has_alpha_channel { - set_bool_property(scope, object, "hasAlphaChannel", value)?; - } - if let Some(value) = configuration.hdr_metadata_type { - set_string_property(scope, object, "hdrMetadataType", value.as_str())?; - } - if let Some(value) = configuration.color_gamut { - set_string_property(scope, object, "colorGamut", value.as_str())?; - } - if let Some(value) = configuration.transfer_function { - set_string_property(scope, object, "transferFunction", value.as_str())?; - } - if let Some(value) = configuration.scalability_mode.as_deref() { - set_string_property(scope, object, "scalabilityMode", value)?; - } - if let Some(value) = configuration.spatial_scalability { - set_bool_property(scope, object, "spatialScalability", value)?; - } - Some(object) -} - -fn key_system_configuration_object<'s>( - scope: &mut v8::PinScope<'s, '_>, - configuration: &MediaCapabilitiesKeySystemConfiguration, -) -> Option> { - let object = v8::Object::new(scope); - set_string_property(scope, object, "keySystem", &configuration.key_system)?; - set_string_property(scope, object, "initDataType", &configuration.init_data_type)?; - set_string_property( - scope, - object, - "distinctiveIdentifier", - configuration.distinctive_identifier.as_str(), - )?; - set_string_property( - scope, - object, - "persistentState", - configuration.persistent_state.as_str(), - )?; - if let Some(session_types) = configuration.session_types.as_ref() { - let values = session_types - .iter() - .map(|value| v8_string(scope, value).map(v8::Local::into)) - .collect::>>>()?; - let array = v8::Array::new(scope, values.len() as i32); - for (index, value) in values.into_iter().enumerate() { - array.set_index(scope, index as u32, value)?; - } - set_value_property(scope, object, "sessionTypes", array.into())?; - } - if let Some(audio) = configuration.audio.as_ref() { - let value = key_system_track_configuration_object(scope, audio)?; - set_object_property(scope, object, "audio", value)?; - } - if let Some(video) = configuration.video.as_ref() { - let value = key_system_track_configuration_object(scope, video)?; - set_object_property(scope, object, "video", value)?; - } - Some(object) -} - -fn key_system_track_configuration_object<'s>( - scope: &mut v8::PinScope<'s, '_>, - configuration: &KeySystemTrackConfiguration, -) -> Option> { - let object = v8::Object::new(scope); - set_string_property(scope, object, "robustness", &configuration.robustness)?; - if let Some(value) = configuration.encryption_scheme.as_ref() { - let value = value - .as_deref() - .and_then(|value| v8_string(scope, value).map(v8::Local::into)) - .unwrap_or_else(|| v8::null(scope).into()); - set_value_property(scope, object, "encryptionScheme", value)?; - } - Some(object) -} - -fn set_string_property( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, - name: &str, - value: &str, -) -> Option<()> { - let value = v8_string(scope, value)?; - set_value_property(scope, object, name, value.into()) -} - -fn set_number_property( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, - name: &str, - value: f64, -) -> Option<()> { - let value = v8::Number::new(scope, value); - set_value_property(scope, object, name, value.into()) -} - -fn set_bool_property( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, - name: &str, - value: bool, -) -> Option<()> { - let value = v8::Boolean::new(scope, value); - set_value_property(scope, object, name, value.into()) -} - -fn set_object_property( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, - name: &str, - value: v8::Local<'_, v8::Object>, -) -> Option<()> { - set_value_property(scope, object, name, value.into()) -} - -fn set_value_property( - scope: &mut v8::PinScope<'_, '_>, - object: v8::Local<'_, v8::Object>, - name: &str, - value: v8::Local<'_, v8::Value>, -) -> Option<()> { - object - .set(scope, v8_string(scope, name)?.into(), value) - .filter(|set| *set) - .map(|_| ()) -} 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 new file mode 100644 index 0000000000..6a28614ef6 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/media_capabilities_snapshot.js @@ -0,0 +1,99 @@ +globalThis.__mediaCapabilitiesSnapshotResult = 'pending'; +(async () => { + const check = (ok, message) => { if (!ok) throw new Error(message); }; + const capabilities = navigator.mediaCapabilities; + const audio = () => ({contentType: 'audio/webm; codecs="opus"', channels: '2', bitrate: 128000, samplerate: 48000, spatialRendering: false}); + const video = () => ({contentType: 'video/webm; codecs="vp09.00.10.08"', width: 320, height: 240, bitrate: 200000, framerate: 24, hasAlphaChannel: false, hdrMetadataType: 'smpteSt2086', colorGamut: 'srgb', transferFunction: 'srgb'}); + function dataProperties(value, realm) { + if (value === null || typeof value !== 'object') return; + check(Object.getPrototypeOf(value) === (Array.isArray(value) ? realm.Array.prototype : realm.Object.prototype), 'result realm'); + for (const key of Object.keys(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + check(Object.hasOwn(descriptor, 'value') && descriptor.writable && descriptor.enumerable && descriptor.configurable, 'own data property: ' + key); + dataProperties(descriptor.value, realm); + } + } + for (const method of ['decodingInfo', 'encodingInfo']) { + let reads = ''; + const child = {}; + for (const [key, value] of Object.entries(audio())) Object.defineProperty(child, key, {configurable: true, get() { reads += key + ','; return value; }}); + const input = { + get audio() { reads += 'audio,'; return child; }, + get type() { reads += 'type,'; return method === 'decodingInfo' ? 'file' : 'record'; }, + get video() { reads += 'video,'; return undefined; } + }; + 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'); + 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(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'); + dataProperties(result, window); + const stable = Object.freeze({type: method === 'decodingInfo' ? 'file' : 'record', audio: Object.freeze(audio())}); + const first = await capabilities[method](stable); + const second = await capabilities[method](stable); + check(first !== second && first.configuration !== second.configuration && first.configuration.audio !== second.configuration.audio, 'fresh nested output'); + first.configuration.audio.channels = 'changed'; + check(second.configuration.audio.channels === '2' && stable.audio.channels === '2', 'no input or result alias'); + Object.freeze(second.configuration.audio); + check((await capabilities[method](stable)).configuration.audio.channels === '2', 'frozen prior result'); + } + for (const scheme of [undefined, null, 'cenc']) { + const track = {}; + if (scheme !== undefined) track.encryptionScheme = scheme; + const input = {type: 'file', audio: audio(), keySystemConfiguration: {keySystem: 'org.w3.clearkey', sessionTypes: ['temporary', 'temporary'], audio: track}}; + const promise = capabilities.decodingInfo(input); + 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(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(key.sessionTypes.join() === 'temporary,temporary' && key.sessionTypes !== input.keySystemConfiguration.sessionTypes, 'copied sequence preserves duplicates'); + dataProperties(result, window); + } + const names = ['audio', 'video', 'type', 'configuration', 'contentType', 'width', 'sessionTypes', 'keySystemConfiguration', 'encryptionScheme']; + const marker = {}; + for (const mode of ['count', 'throw', 'readonly', 'index']) { + const input = {type: 'file', audio: audio(), video: video(), keySystemConfiguration: {keySystem: 'org.w3.clearkey', sessionTypes: ['temporary', 'temporary'], audio: {encryptionScheme: null}}}; + const expected = JSON.stringify(input); + let writes = 0; + let result; + try { + if (mode !== 'index') for (const name of names) Object.defineProperty(Object.prototype, name, { + configurable: true, + get() { return undefined; }, + ...(mode === 'readonly' ? {} : {set() { writes++; if (mode === 'throw') throw marker; }}) + }); + if (mode === 'index') Object.defineProperty(Array.prototype, '0', {configurable: true, get() { return undefined; }, set() { writes++; throw marker; }}); + result = await capabilities.decodingInfo(input); + } finally { + if (mode !== 'index') for (const name of names) delete Object.prototype[name]; + if (mode === 'index') delete Array.prototype[0]; + } + check(writes === 0, 'prototype setter invoked: ' + mode); + check(result.configuration.audio.contentType === input.audio.contentType && result.configuration.video.width === 320, 'pollution cannot remove output'); + check(result.configuration.keySystemConfiguration.sessionTypes.join() === 'temporary,temporary', 'indexed own data'); + check(JSON.stringify(input) === expected, 'input remains unchanged'); + dataProperties(result, window); + } + const frame = document.createElement('iframe'); + document.body.append(frame); + const foreign = frame.contentWindow; + for (const realm of [window, foreign]) { + const receiver = realm === window ? foreign.navigator.mediaCapabilities : capabilities; + const promise = realm.MediaCapabilities.prototype.decodingInfo.call(receiver, {type: 'file', audio: audio()}); + check(Object.getPrototypeOf(promise) === realm.Promise.prototype, 'callee Promise realm'); + const result = await promise; + dataProperties(result, realm); + check(result.configuration.audio.channels === '2', 'foreign receiver output'); + } + frame.remove(); + return 'ok'; +})().then(value => globalThis.__mediaCapabilitiesSnapshotResult = value, + error => globalThis.__mediaCapabilitiesSnapshotResult = String(error)); 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 9445f99324..eb16a11016 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,15 @@ use super::*; +#[test] +fn media_capabilities_dictionary_snapshots_preserve_conversion_and_define_own_data() { + let mut vm = + new_storage_page_task_executor_test_vm("https://media-capabilities-snapshot.test/"); + vm.eval(include_str!("media_capabilities_snapshot.js")) + .unwrap(); + vm.eval("0").unwrap(); + assert_eq!(vm.eval("__mediaCapabilitiesSnapshotResult").unwrap(), "ok"); +} + #[test] fn media_capabilities_nested_dictionaries_validate_null_before_later_getters() { let mut vm = new_storage_test_vm("https://media-capabilities-dictionary.test/");