Support RTL layout and video posters

This commit is contained in:
SGavrl committed 2026-08-04 09:04:45 +00:00
1 parent 68c81f319c
commit 012a7dce2f
9 files changed
+473 -77

No files matched your search

+49 -16
View File
@@ -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;
+70 -23
View File
@@ -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>");