Support RTL layout and video posters
This commit is contained in:
1 parent
68c81f319c
commit
012a7dce2f
9 files changed
+473
-77
No files matched your search
Vendored
+49
-16
@@ -5116,25 +5116,42 @@ globalThis.HTMLImageElement = HTMLImageElement;
|
||||
_markNative(HTMLImageElement);
|
||||
_markNative(HTMLImageElement.prototype.decode);
|
||||
|
||||
// Media elements need canPlayType for codec detection fingerprinting.
|
||||
// Values match Chrome 145 on Linux x86_64 without proprietary codecs.
|
||||
// Report only capabilities backed by a real decoder. Poster rendering is an
|
||||
// image operation and does not make any audio/video container playable.
|
||||
class HTMLMediaElement extends Element {
|
||||
canPlayType(type) {
|
||||
if (!type || typeof type !== 'string') return '';
|
||||
const mime = type.split(';')[0].trim().toLowerCase();
|
||||
if (mime === 'video/mp4' || mime === 'video/webm' || mime === 'video/ogg') return 'probably';
|
||||
if (mime === 'video/x-matroska') return 'maybe';
|
||||
if (mime === 'audio/ogg' || mime === 'audio/webm' || mime === 'audio/wav' ||
|
||||
mime === 'audio/mpeg') return 'probably';
|
||||
if (mime === 'audio/mp4' || mime === 'audio/x-m4a' || mime === 'audio/aac') return 'maybe';
|
||||
return '';
|
||||
}
|
||||
static NETWORK_EMPTY = 0;
|
||||
static NETWORK_IDLE = 1;
|
||||
static NETWORK_LOADING = 2;
|
||||
static NETWORK_NO_SOURCE = 3;
|
||||
static HAVE_NOTHING = 0;
|
||||
static HAVE_METADATA = 1;
|
||||
static HAVE_CURRENT_DATA = 2;
|
||||
static HAVE_FUTURE_DATA = 3;
|
||||
static HAVE_ENOUGH_DATA = 4;
|
||||
canPlayType(_type) { return ''; }
|
||||
load() {}
|
||||
play() { return Promise.resolve(); }
|
||||
play() {
|
||||
return Promise.reject(new DOMException(
|
||||
"The element has no supported sources.",
|
||||
"NotSupportedError",
|
||||
));
|
||||
}
|
||||
pause() {}
|
||||
get NETWORK_EMPTY() { return HTMLMediaElement.NETWORK_EMPTY; }
|
||||
get NETWORK_IDLE() { return HTMLMediaElement.NETWORK_IDLE; }
|
||||
get NETWORK_LOADING() { return HTMLMediaElement.NETWORK_LOADING; }
|
||||
get NETWORK_NO_SOURCE() { return HTMLMediaElement.NETWORK_NO_SOURCE; }
|
||||
get HAVE_NOTHING() { return HTMLMediaElement.HAVE_NOTHING; }
|
||||
get HAVE_METADATA() { return HTMLMediaElement.HAVE_METADATA; }
|
||||
get HAVE_CURRENT_DATA() { return HTMLMediaElement.HAVE_CURRENT_DATA; }
|
||||
get HAVE_FUTURE_DATA() { return HTMLMediaElement.HAVE_FUTURE_DATA; }
|
||||
get HAVE_ENOUGH_DATA() { return HTMLMediaElement.HAVE_ENOUGH_DATA; }
|
||||
get paused() { return true; }
|
||||
get ended() { return false; }
|
||||
get readyState() { return 0; }
|
||||
get networkState() { return HTMLMediaElement.NETWORK_EMPTY; }
|
||||
get readyState() { return HTMLMediaElement.HAVE_NOTHING; }
|
||||
get error() { return null; }
|
||||
get seeking() { return false; }
|
||||
get currentTime() { return 0; }
|
||||
set currentTime(v) {}
|
||||
get duration() { return NaN; }
|
||||
@@ -5142,8 +5159,14 @@ class HTMLMediaElement extends Element {
|
||||
set volume(v) {}
|
||||
get muted() { return false; }
|
||||
set muted(v) {}
|
||||
get src() { return this.getAttribute('src') || ''; }
|
||||
get src() {
|
||||
const raw = this.getAttribute("src");
|
||||
if (!raw) return "";
|
||||
try { return new URL(raw, this.baseURI || globalThis.location?.href || "about:blank").href; }
|
||||
catch (_error) { return raw; }
|
||||
}
|
||||
set src(v) { this.setAttribute('src', v); }
|
||||
get currentSrc() { return ""; }
|
||||
get textTracks() {
|
||||
return TextTrackList.from(
|
||||
Array.from(this.querySelectorAll("track")).map((element) => element.track)
|
||||
@@ -5157,7 +5180,17 @@ _markNative(HTMLMediaElement.prototype.canPlayType);
|
||||
_markNative(HTMLMediaElement.prototype.play);
|
||||
_markNative(HTMLMediaElement.prototype.load);
|
||||
_markNative(HTMLMediaElement.prototype.pause);
|
||||
class HTMLVideoElement extends HTMLMediaElement {}
|
||||
class HTMLVideoElement extends HTMLMediaElement {
|
||||
get poster() {
|
||||
const raw = this.getAttribute("poster");
|
||||
if (!raw) return "";
|
||||
try { return new URL(raw, this.baseURI || globalThis.location?.href || "about:blank").href; }
|
||||
catch (_error) { return raw; }
|
||||
}
|
||||
set poster(value) { this.setAttribute("poster", value); }
|
||||
get videoWidth() { return 0; }
|
||||
get videoHeight() { return 0; }
|
||||
}
|
||||
class HTMLAudioElement extends HTMLMediaElement {}
|
||||
class HTMLTrackElement extends Element {
|
||||
static NONE = 0;
|
||||
|
||||
@@ -667,9 +667,9 @@ impl ObscuraJsRuntime {
|
||||
}
|
||||
|
||||
/// Return the exact responsive candidates selected for live `<img>`
|
||||
/// elements without loading them. The browser layer can then fetch them
|
||||
/// concurrently through the page-owned transport before synchronous layout
|
||||
/// or paint observes the cache.
|
||||
/// elements and `<video poster>` resources without loading them. The
|
||||
/// browser layer can then fetch them concurrently through the page-owned
|
||||
/// transport before synchronous layout or paint observes the cache.
|
||||
#[cfg(feature = "render")]
|
||||
pub fn pending_render_image_urls(&self) -> Vec<(String, crate::ops::ImageRequestProfile)> {
|
||||
let state = self.state.borrow();
|
||||
@@ -682,30 +682,37 @@ impl ObscuraJsRuntime {
|
||||
let Some(node) = dom.get_node(id) else {
|
||||
continue;
|
||||
};
|
||||
if node
|
||||
.as_element()
|
||||
.is_none_or(|element| element.local.as_ref() != "img")
|
||||
{
|
||||
let Some(element) = node.as_element() else {
|
||||
continue;
|
||||
}
|
||||
let Some((url, _, known, _)) = state.render_resources.cached_image_element_metadata(
|
||||
dom,
|
||||
id,
|
||||
state.viewport,
|
||||
base_url.as_deref(),
|
||||
) else {
|
||||
};
|
||||
let candidate = match element.local.as_ref() {
|
||||
"img" => state
|
||||
.render_resources
|
||||
.cached_image_element_metadata(dom, id, state.viewport, base_url.as_deref())
|
||||
.map(|(url, _, known, _)| {
|
||||
let profile = match node
|
||||
.get_attribute("crossorigin")
|
||||
.map(|value| value.trim().to_ascii_lowercase())
|
||||
.as_deref()
|
||||
{
|
||||
Some("use-credentials") => {
|
||||
crate::ops::ImageRequestProfile::CorsInclude
|
||||
}
|
||||
Some(_) => crate::ops::ImageRequestProfile::CorsSameOrigin,
|
||||
None => crate::ops::ImageRequestProfile::NoCorsInclude,
|
||||
};
|
||||
(url, profile, known)
|
||||
}),
|
||||
"video" => state
|
||||
.render_resources
|
||||
.cached_video_poster_metadata(dom, id, base_url.as_deref())
|
||||
.map(|(url, profile, known, _)| (url, profile, known)),
|
||||
_ => None,
|
||||
};
|
||||
let Some((url, profile, known)) = candidate else {
|
||||
continue;
|
||||
};
|
||||
if !known && !url.starts_with("data:") {
|
||||
let profile = match node
|
||||
.get_attribute("crossorigin")
|
||||
.map(|value| value.trim().to_ascii_lowercase())
|
||||
.as_deref()
|
||||
{
|
||||
Some("use-credentials") => crate::ops::ImageRequestProfile::CorsInclude,
|
||||
Some(_) => crate::ops::ImageRequestProfile::CorsSameOrigin,
|
||||
None => crate::ops::ImageRequestProfile::NoCorsInclude,
|
||||
};
|
||||
urls.push((url, profile));
|
||||
}
|
||||
}
|
||||
@@ -12608,6 +12615,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_media_capabilities_and_readiness_are_honest() {
|
||||
let mut rt = setup_runtime(
|
||||
r#"<video id="media" src="https://example.test/movie.mp4"
|
||||
poster="https://example.test/poster.png"></video>"#,
|
||||
);
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"
|
||||
const media = document.getElementById("media");
|
||||
return [
|
||||
media.canPlayType("video/mp4"),
|
||||
media.canPlayType('video/webm; codecs="vp9"'),
|
||||
media.readyState,
|
||||
media.currentTime,
|
||||
media.videoWidth,
|
||||
media.videoHeight,
|
||||
media.paused,
|
||||
media.currentSrc,
|
||||
media.poster,
|
||||
];
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
serde_json::json!([
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
"",
|
||||
"https://example.test/poster.png"
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_string_scripts_remain_inert_when_connected() {
|
||||
let mut rt = setup_runtime("<html><head></head><body><div id=target></div></body></html>");
|
||||
|
||||
Reference in new issue
Block a user