fix(player): three defects native video exposed, and the logs to see them
Each of these was invisible while Linux video played in the webview, and each became reachable the moment mpv started rendering. DR-238 — a transcoded seek re-negotiates the stream on every renderer, not just the webview. `determine_video_seek_strategy` treated `is_hls` as a proxy for "seekable in place", which held only because hls.js was always the HLS renderer: it seeks within the VOD playlist it is handed and lets the server catch up. mpv's HLS demuxer cannot make Jellyfin transcode from a new offset, so with native video on, every transcoded seek became a backend seek that silently did nothing. One cell of the truth table changes; all four webview cells are byte-identical. DR-239 — properties the mpv event loop handles are now observed. libmpv delivers PropertyChange only for properties registered with observe_property, so the `pause` arm was unreachable code that read as implemented: StateChanged was never emitted and the play/pause control never moved. UT-218 asserts the two lists agree, so the class cannot recur. DR-240 — fullscreen moves whatever owns the pixels. requestFullscreen() fullscreens the *document*, which sufficed while the <video> element lived inside it and WebKit scaled it. A native surface is drawn behind the webview at window size, so a document-only fullscreen expanded the page and left the picture at its old size — on WebKitGTK, a maximised window with decorations still holding a strip of the screen. Measured on a 3440x1440 panel: 1361 tall before, 1440 after. DR-241 — a seek issued before mpv has a file to seek in is honoured rather than dropped. loadfile returns as soon as the command is queued, so `time-pos` does not resolve yet and setting it fails. The two callers that always hit that window are resume and a transcoded seek, both of which re-open the stream and then ask for a position; the failed seek was discarded and playback began at zero. Also adds the instrumentation that made the diagnosis possible rather than speculative: an entry log on player_stop, a render-size log that re-fires on change instead of latching once, and decoded-vs-display video geometry on file load. The last of those retired a wrong theory — a picture that does not fill an ultrawide turned out to be a 16:9 source with its letterbox baked in, not a rendering fault.
This commit is contained in:
@@ -34,6 +34,19 @@ pub struct MpvBackend {
|
||||
/// through reported 0.0 / unknown exactly when end-of-file handling needed to
|
||||
/// know where playback reached. See [`ObservedTime`].
|
||||
observed: Arc<Mutex<ObservedTime>>,
|
||||
/// A seek that arrived before MPV had a file to seek in.
|
||||
///
|
||||
/// `loadfile` is asynchronous: it returns as soon as the command is queued,
|
||||
/// so `time-pos` is not yet a resolvable property and setting it fails. A
|
||||
/// seek issued in that window used to be dropped on the floor, and the two
|
||||
/// callers that do exactly this are the ones a viewer notices — resume, and
|
||||
/// a transcoded seek, both of which re-open the stream and then ask for a
|
||||
/// position. The stream reloaded and played from zero.
|
||||
///
|
||||
/// Held here and applied by the `FileLoaded` arm.
|
||||
///
|
||||
/// TRACES: UR-040, UR-005 | DR-241
|
||||
pending_seek: Arc<Mutex<Option<f64>>>,
|
||||
}
|
||||
|
||||
struct InternalState {
|
||||
@@ -237,6 +250,7 @@ impl MpvBackend {
|
||||
playback_reporter,
|
||||
position_throttler,
|
||||
last_seek_time: Arc::new(AtomicU64::new(0)),
|
||||
pending_seek: Arc::new(Mutex::new(None)),
|
||||
observed: Arc::new(Mutex::new(ObservedTime::default())),
|
||||
};
|
||||
|
||||
@@ -254,6 +268,7 @@ impl MpvBackend {
|
||||
let state = self.state.clone();
|
||||
let reporter = self.playback_reporter.clone();
|
||||
let throttler = self.position_throttler.clone();
|
||||
let pending_seek_for_events = self.pending_seek.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
info!("[MpvBackend] Event loop started");
|
||||
@@ -263,6 +278,30 @@ impl MpvBackend {
|
||||
error!("[MpvBackend] Failed to disable deprecated events: {:?}", e);
|
||||
});
|
||||
|
||||
// libmpv delivers PropertyChange only for properties registered
|
||||
// here. Every name matched in the loop below needs a line in this
|
||||
// block or its handler is unreachable — an omission that reads as
|
||||
// working code, because the handler is sitting right there.
|
||||
// UT-218 holds the two lists together.
|
||||
//
|
||||
// `pause` drives the play/pause control: the UI consumes
|
||||
// StateChanged rather than tracking playback itself, per the
|
||||
// one-directional state rule. Unobserved, the event never came and
|
||||
// the button never moved. Invisible until native video shipped,
|
||||
// because the webview <video> element's own DOM events drove that
|
||||
// control on Linux.
|
||||
//
|
||||
// TRACES: UR-005 | DR-239
|
||||
ev_ctx
|
||||
.observe_property("pause", libmpv::Format::Flag, 0)
|
||||
.unwrap_or_else(|e| {
|
||||
error!(
|
||||
"[MpvBackend] Failed to observe 'pause': {:?} — the play/pause \
|
||||
control will not follow the player",
|
||||
e
|
||||
);
|
||||
});
|
||||
|
||||
loop {
|
||||
match ev_ctx.wait_event(1.0) {
|
||||
Some(Ok(event)) => match event {
|
||||
@@ -272,6 +311,43 @@ impl MpvBackend {
|
||||
libmpv::events::Event::FileLoaded => {
|
||||
info!("[MpvBackend] File loaded");
|
||||
|
||||
// Apply a seek that arrived while there was nothing
|
||||
// to seek in. TRACES: UR-040, UR-005 | DR-241
|
||||
{
|
||||
let target = pending_seek_for_events.lock_safe().take();
|
||||
if let Some(position) = target {
|
||||
match mpv.set_property("time-pos", position) {
|
||||
Ok(()) => info!(
|
||||
"[MpvBackend] applied deferred seek to {position}"
|
||||
),
|
||||
Err(e) => warn!(
|
||||
"[MpvBackend] deferred seek to {position} failed: {:?}",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Geometry, so "the picture does not fill the screen"
|
||||
// can be attributed rather than guessed at. `width`/
|
||||
// `height` are the decoded frame; `dwidth`/`dheight`
|
||||
// are what mpv will *display* after aspect
|
||||
// correction. A file that carries its letterbox
|
||||
// baked into the picture reports a 16:9 dwidth and
|
||||
// is then pillarboxed on a wider panel — which looks
|
||||
// identical to a rendering bug from outside.
|
||||
{
|
||||
let n = |k: &str| mpv.get_property::<i64>(k).unwrap_or(-1);
|
||||
info!(
|
||||
"[MpvBackend] video geometry: {}x{} decoded, {}x{} display, aspect {:?}",
|
||||
n("width"),
|
||||
n("height"),
|
||||
n("dwidth"),
|
||||
n("dheight"),
|
||||
mpv.get_property::<f64>("video-params/aspect").ok(),
|
||||
);
|
||||
}
|
||||
|
||||
// Get duration
|
||||
if let Ok(duration) = mpv.get_property::<f64>("duration") {
|
||||
if let Some(emitter) = &event_emitter {
|
||||
@@ -574,11 +650,24 @@ impl PlayerBackend for MpvBackend {
|
||||
.as_millis() as u64;
|
||||
self.last_seek_time.store(now, Ordering::Relaxed);
|
||||
|
||||
self.mpv
|
||||
.set_property("time-pos", position)
|
||||
.map_err(|e| PlayerError {
|
||||
message: format!("Failed to seek: {:?}", e),
|
||||
})?;
|
||||
// `time-pos` only resolves while a file is loaded. `loadfile` is
|
||||
// asynchronous, so a seek issued straight after a reload — resume, or a
|
||||
// transcoded seek — lands in a window where this fails, and dropping it
|
||||
// there is what makes the stream play from zero instead of the position
|
||||
// that was asked for. Hold it and let `FileLoaded` apply it.
|
||||
// TRACES: UR-040, UR-005 | DR-241
|
||||
if let Err(e) = self.mpv.set_property("time-pos", position) {
|
||||
debug!(
|
||||
"[MpvBackend] seek to {position} deferred until the file loads ({:?})",
|
||||
e
|
||||
);
|
||||
*self.pending_seek.lock_safe() = Some(position);
|
||||
self.observed.lock_safe().record_position(position);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// A seek that lands clears any earlier deferred one: the newer intent wins.
|
||||
*self.pending_seek.lock_safe() = None;
|
||||
|
||||
// The poll thread suppresses updates for 150ms after a seek, so without
|
||||
// this a file ending inside that window would report the pre-seek time.
|
||||
|
||||
Reference in New Issue
Block a user