mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(media): dispatch canplaythrough after canplay
This commit is contained in:
@@ -554,6 +554,7 @@ pub(crate) enum MediaLoadEventPhase {
|
||||
LoadedMetadata,
|
||||
LoadedData,
|
||||
CanPlay,
|
||||
CanPlayThrough,
|
||||
Error,
|
||||
}
|
||||
|
||||
@@ -564,6 +565,7 @@ impl MediaLoadEventPhase {
|
||||
Self::LoadedMetadata => "loadedmetadata",
|
||||
Self::LoadedData => "loadeddata",
|
||||
Self::CanPlay => "canplay",
|
||||
Self::CanPlayThrough => "canplaythrough",
|
||||
Self::Error => "error",
|
||||
}
|
||||
}
|
||||
@@ -573,7 +575,8 @@ impl MediaLoadEventPhase {
|
||||
Self::LoadStart => Some(Self::LoadedMetadata),
|
||||
Self::LoadedMetadata => Some(Self::LoadedData),
|
||||
Self::LoadedData => Some(Self::CanPlay),
|
||||
Self::CanPlay | Self::Error => None,
|
||||
Self::CanPlay => Some(Self::CanPlayThrough),
|
||||
Self::CanPlayThrough | Self::Error => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -604,7 +607,7 @@ pub(crate) fn dispatch_media_load_event_phase(
|
||||
return false;
|
||||
}
|
||||
match phase {
|
||||
MediaLoadEventPhase::LoadStart => {}
|
||||
MediaLoadEventPhase::LoadStart | MediaLoadEventPhase::CanPlayThrough => {}
|
||||
MediaLoadEventPhase::LoadedMetadata => {
|
||||
let _ = unsafe { &mut *runtime_ptr }.set_media_ready_state(handle, 1);
|
||||
}
|
||||
|
||||
@@ -383,3 +383,212 @@ emptyMedia.volume === 0.25 && !emptyMedia.muted && emptyMedia.defaultMuted"#,
|
||||
.await
|
||||
.expect("empty local media uses its owned error task without readiness events");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn media_canplaythrough_runs_in_a_separate_owned_task_after_canplay_microtasks() {
|
||||
run_page_vm_async_test(async move {
|
||||
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())
|
||||
.expect("loader");
|
||||
let (mut page, _source, _wake) = page_vm_with_bound_task_sources_and_owner_wake(
|
||||
&loader, Url::parse("https://media-canplaythrough.test/")?,
|
||||
);
|
||||
page.vm_mut().set_layout_policy(moli_page_types::LayoutPolicy::Mock);
|
||||
page.vm_mut().eval(r#"
|
||||
globalThis.mediaEvents = [];
|
||||
globalThis.media = document.createElement('video');
|
||||
for (const type of ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough']) {
|
||||
media.addEventListener(type, event => {
|
||||
mediaEvents.push(type + ':' + media.readyState);
|
||||
if (type === 'canplay' || type === 'canplaythrough') {
|
||||
Promise.resolve().then(() => mediaEvents.push(type + '-microtask'));
|
||||
}
|
||||
});
|
||||
}
|
||||
media.oncanplay = () => setTimeout(() => mediaEvents.push('timer'), 0);
|
||||
document.body.append(media);
|
||||
media.src = 'data:video/mp4;base64,AAAAGGZ0eXBtcDQyAAAAAG1wNDJtcDQx';
|
||||
"#)?;
|
||||
for _ in 0..4 {
|
||||
let turn = page.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
).expect("each load phase must have its own media-element task");
|
||||
page.run_claimed_selected_page_task_for_test(turn, &loader).await?;
|
||||
}
|
||||
assert_eq!(page.vm_mut().eval("mediaEvents.join('|')")?,
|
||||
"loadstart:0|loadedmetadata:1|loadeddata:2|canplay:4|canplay-microtask");
|
||||
assert!(page.vm().has_ready_timeout(), "the unrelated timer must remain queued");
|
||||
let through = page.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
).expect("HAVE_ENOUGH_DATA must queue canplaythrough after canplay");
|
||||
page.run_claimed_selected_page_task_for_test(through, &loader).await?;
|
||||
assert_eq!(page.vm_mut().eval("mediaEvents.join('|')")?,
|
||||
"loadstart:0|loadedmetadata:1|loadeddata:2|canplay:4|canplay-microtask|canplaythrough:4|canplaythrough-microtask");
|
||||
assert_eq!(page.vm_mut().eval("media.paused && media.networkState === 1")?, "true");
|
||||
assert!(page.vm().has_ready_timeout(), "canplaythrough must not consume timers");
|
||||
assert!(page.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
).is_none(), "the terminal phase must finish the load sequence exactly once");
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}).await.expect("canplaythrough task ordering and timer isolation");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn media_canplaythrough_from_a_retired_document_does_not_checkpoint_the_replacement() {
|
||||
run_page_vm_async_test(async move {
|
||||
let loader =
|
||||
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
||||
let (mut page, _source, _wake) = page_vm_with_bound_task_sources_and_owner_wake(
|
||||
&loader,
|
||||
Url::parse("https://retired-media-canplaythrough.test/")?,
|
||||
);
|
||||
page.vm_mut()
|
||||
.set_layout_policy(moli_page_types::LayoutPolicy::Mock);
|
||||
page.vm_mut().eval(
|
||||
r#"
|
||||
globalThis.retiredMediaEvents = [];
|
||||
const media = document.createElement('video');
|
||||
media.oncanplaythrough = () => retiredMediaEvents.push('stale canplaythrough');
|
||||
document.body.append(media);
|
||||
media.src = 'data:video/mp4;base64,AAAAGGZ0eXBtcDQyAAAAAG1wNDJtcDQx';
|
||||
"#,
|
||||
)?;
|
||||
for _ in 0..4 {
|
||||
let turn = page
|
||||
.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
)
|
||||
.expect("load phase before Document retirement");
|
||||
page.run_claimed_selected_page_task_for_test(turn, &loader)
|
||||
.await?;
|
||||
}
|
||||
let retired = page
|
||||
.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
)
|
||||
.expect("capture the terminal task before its Document is retired");
|
||||
page.vm_mut().eval(
|
||||
"document.open();document.write('<!doctype html><body>replacement</body>');document.close();",
|
||||
)?;
|
||||
page.vm_mut().eval_without_microtask_checkpoint_for_test(
|
||||
r#"
|
||||
globalThis.replacementMediaEvents = [];
|
||||
Promise.resolve().then(() => replacementMediaEvents.push('replacement microtask'));
|
||||
"#,
|
||||
)?;
|
||||
page.vm_mut().enqueue_test_pending_runtime_source_load();
|
||||
page.run_claimed_selected_page_task_for_test(retired, &loader)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
page.vm_mut().eval_without_microtask_checkpoint_for_test(
|
||||
"retiredMediaEvents.join('|') + ':' + replacementMediaEvents.join('|')",
|
||||
)?,
|
||||
":"
|
||||
);
|
||||
assert_eq!(
|
||||
page.vm()
|
||||
.document_runtime
|
||||
.runtime_script_work()
|
||||
.dynamic_scripts
|
||||
.pending_source_load_count_for_test(),
|
||||
1,
|
||||
"retired terminal events must not advance replacement script work"
|
||||
);
|
||||
assert!(
|
||||
page.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.await
|
||||
.expect("retired canplaythrough owner and completion isolation");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn media_canplaythrough_is_cancelled_when_canplay_replaces_the_load_sequence() {
|
||||
run_page_vm_async_test(async move {
|
||||
let loader =
|
||||
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
|
||||
let (mut page, _source, _wake) = page_vm_with_bound_task_sources_and_owner_wake(
|
||||
&loader,
|
||||
Url::parse("https://replaced-media-canplaythrough.test/")?,
|
||||
);
|
||||
page.vm_mut()
|
||||
.set_layout_policy(moli_page_types::LayoutPolicy::Mock);
|
||||
page.vm_mut().eval(
|
||||
r#"
|
||||
globalThis.replacedMediaEvents = [];
|
||||
globalThis.media = document.createElement('video');
|
||||
let first = true;
|
||||
media.oncanplay = () => {
|
||||
replacedMediaEvents.push('canplay');
|
||||
if (first) {
|
||||
first = false;
|
||||
Promise.resolve().then(() => {
|
||||
media.src = 'data:video/mp4;base64,AAAAGGZ0eXBtcDQyAAAAAG1wNDJtcDQxAA==';
|
||||
});
|
||||
}
|
||||
};
|
||||
media.oncanplaythrough = () => replacedMediaEvents.push('canplaythrough');
|
||||
document.body.append(media);
|
||||
media.src = 'data:video/mp4;base64,AAAAGGZ0eXBtcDQyAAAAAG1wNDJtcDQx';
|
||||
"#,
|
||||
)?;
|
||||
for _ in 0..4 {
|
||||
let turn = page
|
||||
.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
)
|
||||
.expect("first load phase");
|
||||
page.run_claimed_selected_page_task_for_test(turn, &loader)
|
||||
.await?;
|
||||
}
|
||||
assert_eq!(
|
||||
page.vm_mut().eval("replacedMediaEvents.join('|')")?,
|
||||
"canplay"
|
||||
);
|
||||
let restarted = page
|
||||
.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
)
|
||||
.expect("consume the old terminal retained ahead of the replacement");
|
||||
page.run_claimed_selected_page_task_for_test(restarted, &loader)
|
||||
.await?;
|
||||
assert_eq!(page.vm_mut().eval("media.readyState")?, "0");
|
||||
assert_eq!(
|
||||
page.vm_mut().eval("replacedMediaEvents.join('|')")?,
|
||||
"canplay"
|
||||
);
|
||||
let loadstart = page
|
||||
.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
)
|
||||
.expect("replacement loadstart follows the discarded old terminal");
|
||||
page.run_claimed_selected_page_task_for_test(loadstart, &loader)
|
||||
.await?;
|
||||
assert_eq!(page.vm_mut().eval("media.readyState")?, "0");
|
||||
for _ in 0..4 {
|
||||
let turn = page
|
||||
.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
)
|
||||
.expect("replacement load phase");
|
||||
page.run_claimed_selected_page_task_for_test(turn, &loader)
|
||||
.await?;
|
||||
}
|
||||
assert_eq!(
|
||||
page.vm_mut().eval("replacedMediaEvents.join('|')")?,
|
||||
"canplay|canplay|canplaythrough"
|
||||
);
|
||||
assert!(
|
||||
page.claim_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MediaElementEvent,
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.await
|
||||
.expect("load sequence replacement cancels only the old terminal event");
|
||||
}
|
||||
|
||||
@@ -1336,7 +1336,7 @@ async fn main_static_media_load_delays_complete_until_loadeddata_owner_turn() {
|
||||
r#"
|
||||
globalThis.__mainMediaLifecycleEvents = [];
|
||||
const clip = document.getElementById("clip");
|
||||
for (const type of ["loadstart", "loadedmetadata", "loadeddata", "canplay"]) {
|
||||
for (const type of ["loadstart", "loadedmetadata", "loadeddata", "canplay", "canplaythrough"]) {
|
||||
clip.addEventListener(type, () => {
|
||||
__mainMediaLifecycleEvents.push(`${type}:${document.readyState}`);
|
||||
});
|
||||
@@ -1421,6 +1421,24 @@ async fn main_static_media_load_delays_complete_until_loadeddata_owner_turn() {
|
||||
));
|
||||
|
||||
run_next_page_media_element_event_for_test(&mut vm, &loader, "canplay owner turn").await;
|
||||
assert!(
|
||||
vm._context_host
|
||||
.borrow()
|
||||
.pending_media_load_sequence(media)
|
||||
.is_some(),
|
||||
"canplay must leave the terminal event owned by this sequence"
|
||||
);
|
||||
assert_eq!(
|
||||
vm.eval("__mainMediaLifecycleEvents.some(event => event.startsWith('canplaythrough:'))")
|
||||
.unwrap(),
|
||||
"false"
|
||||
);
|
||||
run_next_page_media_element_event_for_test(&mut vm, &loader, "canplaythrough owner turn").await;
|
||||
assert_eq!(
|
||||
vm.eval("__mainMediaLifecycleEvents.at(-1).startsWith('canplaythrough:')")
|
||||
.unwrap(),
|
||||
"true"
|
||||
);
|
||||
assert!(
|
||||
vm._context_host
|
||||
.borrow()
|
||||
@@ -1985,7 +2003,7 @@ async fn main_text_track_network_terminal_gates_canplay_without_delaying_complet
|
||||
globalThis.__trackMediaEvents = [];
|
||||
const clip = document.getElementById("clip");
|
||||
const captions = document.getElementById("captions");
|
||||
for (const type of ["loadstart", "loadedmetadata", "loadeddata", "canplay"]) {
|
||||
for (const type of ["loadstart", "loadedmetadata", "loadeddata", "canplay", "canplaythrough"]) {
|
||||
clip.addEventListener(type, () => __trackMediaEvents.push(type));
|
||||
}
|
||||
captions.addEventListener("load", () => {
|
||||
@@ -2131,6 +2149,23 @@ async fn main_text_track_network_terminal_gates_canplay_without_delaying_complet
|
||||
.expect("canplay event trace"),
|
||||
"loadstart|loadedmetadata|loadeddata|track-load:2:1|canplay"
|
||||
);
|
||||
assert!(
|
||||
vm._context_host
|
||||
.borrow()
|
||||
.pending_media_load_sequence(media)
|
||||
.is_some(),
|
||||
"canplay must retain its canplaythrough continuation"
|
||||
);
|
||||
run_next_page_media_element_event_for_test(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"media canplaythrough follow-up owner turn",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
vm.eval("__trackMediaEvents.join('|')").unwrap(),
|
||||
"loadstart|loadedmetadata|loadeddata|track-load:2:1|canplay|canplaythrough"
|
||||
);
|
||||
assert!(
|
||||
vm._context_host
|
||||
.borrow()
|
||||
|
||||
@@ -476,7 +476,7 @@ async fn default_video_fetch_policy_synthesizes_lifecycle_without_a_network_requ
|
||||
globalThis.__defaultVideoPolicyEvents = [];
|
||||
const video = document.createElement("video");
|
||||
video.id = "default-video-policy";
|
||||
for (const type of ["loadstart", "loadedmetadata", "loadeddata", "canplay", "error"]) {
|
||||
for (const type of ["loadstart", "loadedmetadata", "loadeddata", "canplay", "canplaythrough", "error"]) {
|
||||
video.addEventListener(type, () => __defaultVideoPolicyEvents.push(type));
|
||||
}
|
||||
video.src = "https://media-policy.test/large-video.mp4";
|
||||
@@ -511,7 +511,13 @@ async fn default_video_fetch_policy_synthesizes_lifecycle_without_a_network_requ
|
||||
"policy-skipped video must not emit a requestWillBeSent source record"
|
||||
);
|
||||
|
||||
for phase in ["loadstart", "loadedmetadata", "loadeddata", "canplay"] {
|
||||
for phase in [
|
||||
"loadstart",
|
||||
"loadedmetadata",
|
||||
"loadeddata",
|
||||
"canplay",
|
||||
"canplaythrough",
|
||||
] {
|
||||
run_next_page_media_element_event_for_test(
|
||||
&mut vm,
|
||||
&loader,
|
||||
@@ -524,7 +530,7 @@ async fn default_video_fetch_policy_synthesizes_lifecycle_without_a_network_requ
|
||||
"JSON.stringify({events: __defaultVideoPolicyEvents, ready: document.getElementById('default-video-policy').readyState, network: document.getElementById('default-video-policy').networkState})"
|
||||
)
|
||||
.expect("default-policy video terminal state"),
|
||||
r#"{"events":["loadstart","loadedmetadata","loadeddata","canplay"],"ready":4,"network":1}"#
|
||||
r#"{"events":["loadstart","loadedmetadata","loadeddata","canplay","canplaythrough"],"ready":4,"network":1}"#
|
||||
);
|
||||
assert!(
|
||||
vm._context_host
|
||||
|
||||
Reference in New Issue
Block a user