#![cfg(export_backend)]
mod common;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::time::{Duration, Instant};
use common::engine_with_frame;
use mpv_engine::{ExportOptions, ExportedFrame};
#[test]
fn continuous_source_keeps_delivering_frames() {
let Some(engine) = common::video_engine() else {
return;
};
let dir = tempfile::tempdir().expect("tempdir");
let clip = dir.path().join("loop.nut");
if generate_fast_clip(&clip).is_none() {
return;
}
let (tx, rx) = mpsc::channel();
if engine
.attach_exported_render(ExportOptions::new(128, 128), move || {
let _ = tx.send(());
})
.is_err()
{
return;
}
engine.set_property("loop-file", "inf").expect("loop-file");
engine
.load_when_ready(clip.to_str().expect("utf-8 path"))
.expect("load");
let deadline = Instant::now() + Duration::from_secs(5);
let mut acquired = 0;
while acquired < 10 && Instant::now() < deadline {
let _ = rx.recv_timeout(Duration::from_millis(100));
if engine.acquire_frame().expect("acquire").is_some() {
acquired += 1;
}
}
assert!(
acquired >= 5,
"continuous source starved the consumer: only {acquired} frame(s) acquired \
(pool never grew past the published buffer?)"
);
engine.detach_render();
}
fn generate_fast_clip(target: &std::path::Path) -> Option<()> {
let status = std::process::Command::new("ffmpeg")
.args([
"-y",
"-f",
"lavfi",
"-i",
"testsrc=size=128x128:rate=60",
"-t",
"2",
"-c:v",
"rawvideo",
"-pix_fmt",
"yuv420p",
])
.arg(target)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
matches!(status, Ok(s) if s.success()).then_some(())
}
#[test]
fn forced_render_survives_pool_exhaustion() {
let Some((engine, first, rx)) = engine_with_frame() else {
return;
};
engine
.set_property("pause", "yes")
.expect("pause for a quiescent render thread");
let mut held: Vec<ExportedFrame> = vec![first];
for size in [60u32, 56] {
engine.set_export_size(size, size).expect("resize");
let deadline = Instant::now() + Duration::from_secs(10);
loop {
assert!(
Instant::now() < deadline,
"no {size}x{size} frame while filling the pool"
);
let _ = rx.recv_timeout(Duration::from_millis(100));
if let Some(frame) = engine.acquire_frame().expect("acquire") {
if (frame.width(), frame.height()) == (size, size) {
held.push(frame);
break;
}
}
}
}
assert_eq!(held.len(), 3, "pool should now be fully in our hands");
engine
.set_export_size(32, 32)
.expect("resize while exhausted");
std::thread::sleep(Duration::from_millis(300));
drop(held.remove(0));
let deadline = Instant::now() + Duration::from_secs(10);
loop {
assert!(
Instant::now() < deadline,
"forced render was lost: no 32x32 frame after releasing a buffer"
);
let _ = rx.recv_timeout(Duration::from_millis(200));
match engine.acquire_frame().expect("acquire") {
Some(frame) if (frame.width(), frame.height()) == (32, 32) => break,
_ => {}
}
}
engine.detach_render();
}
#[test]
fn detach_from_on_update_does_not_deadlock() {
let Some((engine, frame, _rx)) = engine_with_frame() else {
return;
};
drop(frame);
let engine = Arc::new(engine);
let armed = Arc::new(AtomicBool::new(false));
let detached = Arc::new(AtomicBool::new(false));
{
let cb_engine = Arc::clone(&engine);
let armed = Arc::clone(&armed);
let detached = Arc::clone(&detached);
engine
.set_render_update_callback(move || {
if armed.load(Ordering::SeqCst) && !detached.swap(true, Ordering::SeqCst) {
cb_engine.detach_render();
}
})
.expect("replace callback");
}
armed.store(true, Ordering::SeqCst);
engine.set_export_size(48, 48).expect("force a publish");
let deadline = Instant::now() + Duration::from_secs(10);
while !detached.load(Ordering::SeqCst) || engine.attached_render().is_some() {
assert!(
Instant::now() < deadline,
"detach from on_update did not complete"
);
std::thread::sleep(Duration::from_millis(50));
}
std::thread::sleep(Duration::from_millis(300));
drop(engine);
}
#[test]
fn reattach_after_callback_detach_succeeds() {
let Some((engine, frame, _rx)) = engine_with_frame() else {
return;
};
drop(frame);
let engine = Arc::new(engine);
let armed = Arc::new(AtomicBool::new(false));
let detached = Arc::new(AtomicBool::new(false));
{
let cb_engine = Arc::clone(&engine);
let armed = Arc::clone(&armed);
let detached = Arc::clone(&detached);
engine
.set_render_update_callback(move || {
if armed.load(Ordering::SeqCst) && !detached.swap(true, Ordering::SeqCst) {
cb_engine.detach_render();
std::thread::sleep(Duration::from_millis(500));
}
})
.expect("replace callback");
}
armed.store(true, Ordering::SeqCst);
engine.set_export_size(48, 48).expect("force a publish");
let deadline = Instant::now() + Duration::from_secs(10);
while engine.attached_render().is_some() {
assert!(
Instant::now() < deadline,
"detach from on_update did not complete"
);
std::thread::sleep(Duration::from_millis(10));
}
let (tx, rx) = std::sync::mpsc::channel();
engine
.attach_exported_render(mpv_engine::ExportOptions::new(64, 64), move || {
let _ = tx.send(());
})
.expect("re-attach after a callback detach should succeed first try");
engine.set_export_size(40, 40).expect("resize");
let deadline = Instant::now() + Duration::from_secs(10);
loop {
assert!(
Instant::now() < deadline,
"no frame from the re-attached backend"
);
let _ = rx.recv_timeout(Duration::from_millis(200));
match engine.acquire_frame().expect("acquire") {
Some(frame) if (frame.width(), frame.height()) == (40, 40) => break,
_ => {}
}
}
engine.detach_render();
}