//! 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 -- //! //! 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 { player: P, url: String, } impl Harness for EngineHarness

{ 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 { 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()), ) .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) }