diff --git a/crates/compositor/src/pipeline_windows.rs b/crates/compositor/src/pipeline_windows.rs index 4c59cf73c..3a0fa0492 100644 --- a/crates/compositor/src/pipeline_windows.rs +++ b/crates/compositor/src/pipeline_windows.rs @@ -2274,6 +2274,25 @@ mod tests { out } + /// A 64x64, 25 fps, 0.6 s clip whose frames around the two cut points get their own colour: + /// blue before 0.36 s, magenta 0.36-0.399 s (the last frame a 0.4 s cut keeps), lime from + /// 0.4 s, yellow from 0.56 s (the file's last frame). + fn encode_end_frame_colours(codec_args: &[&str], filename: &str) -> std::path::PathBuf { + let boxes = [ + ("magenta", "between(t,0.36,0.399)"), + ("lime", "between(t,0.4,0.559)"), + ("yellow", "gte(t,0.56)"), + ]; + let filter = boxes + .iter() + .map(|(c, en)| format!("drawbox=x=0:y=0:w=iw:h=ih:color={c}:t=fill:enable='{en}'")) + .collect::>() + .join(","); + let mut args = vec!["-vf", filter.as_str()]; + args.extend_from_slice(codec_args); + encode_named_color(&args, filename, "blue", "0.6") + } + unsafe fn allocated_decoder_open_resources(filename: &str) -> DecoderOpenResources { let path = encode_color(&["-c:v", "libopenh264", "-b:v", "200k"], filename); let cpath = CString::new(path.to_string_lossy().as_bytes()).expect("fixture path CString"); @@ -3074,7 +3093,9 @@ mod tests { let h264 = ["-c:v", "libopenh264", "-b:v", "200k"]; // Long enough that a view which wrapped onto it is still there when the test looks. let first = encode_named_color(&h264, "997-first.mp4", "red", "3.0"); - let last = encode_named_color(&h264, "997-last.mp4", "blue", "0.6"); + // The last frame before each end has its own colour (magenta before the 0.4 s cut, yellow + // for the file's last frame), so the held picture says which frame the view stopped on. + let last = encode_end_frame_colours(&h264, "997-last.mp4"); // The paths travel in JSON, and the view matches them against its requests as strings. let first = first.to_str().expect("utf8").replace('\\', "/"); let last = last.to_str().expect("utf8").replace('\\', "/"); @@ -3105,6 +3126,8 @@ mod tests { let first_frame = wait_for_frame(&view, 0, |p| p.clip_index == 2); let mut gen = first_frame.0; let mut held = first_frame.5; + let mut held_rgba = first_frame.3; + let (mut held_w, mut held_h) = (first_frame.1, first_frame.2); // Play to the end of the last clip, then about as long again. view.set_playing(true); @@ -3114,20 +3137,44 @@ mod tests { while let Some(frame) = view.latest_frame_since(gen) { gen = frame.0; held = frame.5; + held_rgba = frame.3; + (held_w, held_h) = (frame.1, frame.2); } - // The frame held at the end is the clip's own last frame, not the first one its cut - // removed (the 25 fps file has a frame at exactly 0.4 s). + // The published time is the hold clock: it runs on to the clip's end even if decoding + // stopped early. The bounds check the clock (reached the end, never past it); the + // colour below checks which frame is held. assert!( - held.source_time_sec < last_end_sec, - "end {last_end_sec}: held a frame past the clip's end, at {:.3} s", + held.source_time_sec <= last_end_sec + 1e-6, + "end {last_end_sec}: the clock ran past the clip's end, at {:.3} s", held.source_time_sec ); - // And it did play up to there: a view stalled at its start would pass the bound above. assert!( held.source_time_sec > last_end_sec - 0.1, - "end {last_end_sec}: playback did not reach the clip's end, held at {:.3} s", + "end {last_end_sec}: the clock did not reach the clip's end, held at {:.3} s", held.source_time_sec ); + // The colour proves which frame is held: the last one the cut keeps (magenta), or the + // file's last frame (yellow). Stuck on an earlier frame shows blue or lime instead. + let center = (((held_h / 2) * held_w + held_w / 2) * 4) as usize; + let rgb = &held_rgba[center..center + 3]; + let (r, g, b) = (i32::from(rgb[0]), i32::from(rgb[1]), i32::from(rgb[2])); + if last_end_sec < 0.5 { + assert!( + r > 150 && b > 150 && g < 100, + "end {last_end_sec}: expected magenta, the last frame the cut keeps: {rgb:?}" + ); + } else { + assert!( + r > 150 && g > 150 && b < 100, + "end {last_end_sec}: expected yellow, the file's last frame: {rgb:?}" + ); + } + + assert_eq!( + held.clip_index, 2, + "end {last_end_sec}: held a frame from clip {} instead of the last clip", + held.clip_index + ); // A seek inside the last clip, as the app sends it while paused. view.set_time(0.2);