Files
jellytau/src-tauri/src/conformance_runner.rs
T
dtourolle bb3ab1edd7 feat(windows): mpv draws Windows video into the app window
DR-237's video half. mpv now renders Windows video the way
tauri-plugin-libmpv does on Windows: MpvBackend is handed the main
window's HWND and sets it as `wid` before mpv initialises, so mpv draws
as a child of the app window beneath the transparent WebView2, with
vo=gpu-next,gpu. osc, default bindings, VO keyboard and cursor handling
are off, so the Svelte controls drawn over the picture are the only ones.

- video_output() decides per platform (UT-274): Window(hwnd) on Windows,
  RenderApi on Linux, Off without native video. Windows with no handle
  draws nothing rather than letting mpv open a top-level window.
- native_video::enabled() is true on Windows as well, so the frontend
  takes the native path there: NativePlayerAdapter, and the page clears
  its background while video is on screen.
- enableNativeVideoCompositing() no longer logs a missing Android bridge
  as an error on the desktop, where there is no bridge to have.

Verified: every option, wid included, is accepted by the real libmpv on
Linux and by the shipped Windows DLL under wine; the Windows unit suite
passes under wine (949). Not yet seen on real Windows hardware.
2026-09-24 22:42:42 -04:00

173 lines
6.0 KiB
Rust

//! Runs the `MediaPlayer` conformance suite against a real engine.
//!
//! A separate binary on purpose: it links libmpv and nothing else, so a wrapper
//! can be verified without building or launching the app — which is what made
//! the previous round of playback debugging so slow. Every failure here is a
//! wrapper bug, with no UI, no webview and no server in the way.
//!
//! cargo run --features conformance --bin player-conformance -- <media-file>
//!
//! Audio and video are routed to null, so it is safe on a headless runner and
//! does not claim the speakers.
//!
//! TRACES: UR-081 | DR-244
use std::time::{Duration, Instant};
use crate::player::conformance::Harness;
use crate::player::legacy_player::LegacyPlayer;
use crate::player::media::MediaItem;
use crate::player::media_player::{MediaPlayer, OpenRequest, Phase};
use crate::player::mpv_backend::MpvBackend;
use crate::player::mpv_player::{MpvPlayer, Output};
use crate::repository::stream_selection::StreamSelection;
struct EngineHarness<P: MediaPlayer> {
player: P,
url: String,
}
impl<P: MediaPlayer> Harness for EngineHarness<P> {
type Player = P;
fn player(&mut self) -> &mut P {
&mut self.player
}
fn request(&self, start: Duration) -> OpenRequest {
let selection = StreamSelection::local_file(self.url.clone());
let media = MediaItem::sample("conformance", &self.url);
OpenRequest::new(media, selection).starting_at(start)
}
/// Wait for mpv to leave `Opening`.
///
/// Polling a phase the engine publishes, not a fixed sleep: a suite whose
/// result depends on how fast the machine is will eventually be ignored.
fn settle(&mut self) {
let deadline = Instant::now() + Duration::from_secs(15);
while Instant::now() < deadline {
if self.player.snapshot().phase != Phase::Opening {
// Let the deferred seek land and one position tick arrive.
std::thread::sleep(Duration::from_millis(300));
return;
}
std::thread::sleep(Duration::from_millis(25));
}
eprintln!(" ! settle timed out - engine stayed in Opening");
}
/// mpv is on a null audio device here, so silence cannot be observed.
/// Reporting `None` skips those assertions rather than passing them
/// vacuously — an assertion that cannot fail is worse than an absent one.
fn audible(&mut self) -> Option<bool> {
None
}
/// Keyframe granularity: mpv lands on the nearest one, not on the request.
fn seek_tolerance(&self) -> Duration {
Duration::from_secs(10)
}
/// Poll until the decoder reports the new position, rather than assuming a
/// seek is visible the instant it is accepted.
fn await_seek(&mut self, target: Duration) {
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
let pos = self.player.snapshot().position;
if pos.abs_diff(target) <= self.seek_tolerance() {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
}
}
macro_rules! run {
($failed:ident, $url:expr, $make:expr, $case:path) => {{
let name = stringify!($case).rsplit("::").next().unwrap();
print!(" {name:.<52}");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut h = EngineHarness {
player: $make,
url: $url.to_string(),
};
$case(&mut h);
// Leave nothing playing behind for the next case.
let _ = h.player.close();
}));
match result {
Ok(()) => println!(" ok"),
Err(_) => {
println!(" FAILED");
$failed += 1;
}
}
}};
}
/// Which engine to interrogate.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Engine {
/// The `MediaPlayer` implementation.
Mpv,
/// The old `PlayerBackend`, driven through `LegacyPlayer`.
///
/// Present so the difference between the two designs can be *demonstrated*
/// on the same engine and the same media, rather than argued.
Legacy,
}
/// Run every conformance case against `engine`. Returns the failure count.
pub fn run_engine(url: &str, engine: Engine) -> u32 {
println!("MediaPlayer conformance - {engine:?}");
println!("media: {url}\n");
let mut failed = 0u32;
use crate::player::conformance as c;
macro_rules! all_cases {
($make:expr) => {
run!(failed, url, $make, c::opens_from_the_beginning);
run!(failed, url, $make, c::opens_at_a_start_position);
run!(failed, url, $make, c::seek_while_opening_is_honoured);
run!(failed, url, $make, c::seek_while_opening_overrides_start);
run!(failed, url, $make, c::seeks_after_open);
run!(failed, url, $make, c::pause_and_play_are_observable);
run!(failed, url, $make, c::close_is_silent_and_idempotent);
run!(failed, url, $make, c::close_during_open_never_plays);
run!(failed, url, $make, c::transport_settings_round_trip);
};
}
match engine {
Engine::Mpv => {
all_cases!(MpvPlayer::new(Output::Null).expect("could not create mpv"));
}
Engine::Legacy => {
all_cases!(LegacyPlayer::new(
MpvBackend::new(
None,
std::sync::Arc::new(tokio::sync::Mutex::new(None)),
std::sync::Arc::new(crate::playback_reporting::throttle::EventThrottler::new()),
None,
)
.expect("could not create the legacy backend"),
crate::player::media_player::Capabilities::mpv(),
));
}
}
if failed == 0 {
println!("\nall cases passed");
} else {
println!("\n{failed} case(s) failed");
}
failed
}
/// Default entry point: the new engine.
pub fn run(url: &str) -> u32 {
run_engine(url, Engine::Mpv)
}