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.
173 lines
6.0 KiB
Rust
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)
|
|
}
|