DR-246. The strategy used to turn on `is_hls` and `use_html5`, decided in a
command handler on behalf of engines it does not own. That is how "who
renders" came to mean "how do I seek", and why a transcoded seek silently did
nothing the moment native video changed the renderer (DR-238).
Engines now declare `Capabilities::seeks_transcoded_in_place` — true for
hls.js, which seeks within the VOD playlist it was handed and lets the server
catch up; false for mpv, whose HLS demuxer cannot make the server transcode
from a new offset. The command asks whichever engine is rendering. Adding an
engine no longer means editing a shared truth table.
The item's transport is not read at the seek site any more; the compiler
flagged it unused, which is the URL-shape input finally disappearing.
A deviation from the spec, recorded deliberately: it called for the engine to
own the decision outright. It cannot. Re-negotiating a stream needs the
repository, which sits above the engine, so the engine states the ability and
the caller acts on it. That still removes the defect — nobody guesses on
another component's behalf — without pretending an engine can reach upward.
Also fixes a latent race in the conformance suite, found by running it: the
seek case asserted immediately, which passes on an engine that records the
target when it accepts a seek and races on one that waits for the decoder to
move. `Harness::await_seek` polls instead, the way the Android suite already
did. It failed with machine load rather than with the code, which is the kind
of test that teaches people to re-run until green.
MpvPlayer 9/9
LegacyPlayer 8/9 - still only the mute/rate gap in the old trait
789 tests, clippy -D warnings clean with and without the feature.
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.
onSkipToNext/onSkipToPrevious forwarded a bare next/previous to Rust, which
always advanced the queue. Correct for music, wrong for a video whose audio is
running through a background-audio handoff (UR-040): pressing skip to re-hear a
line jumped to the next episode instead of scrubbing.
resolve_skip_action in player/seek.rs maps the command to Advance or SeekTo, and
is_background_audio_active() is the whole test — the handoff exists only for
video, and an episode played through it reports MediaType::Audio, so media type
cannot distinguish the case. Forward 30s, back 10s, both clamped to [0, duration]
so a skip near either end cannot seek negative or read as EOF and advance.
Routed through the same spawn-then-seek_absolute path as the scrubber, because a
handoff seek re-opens the stream and must not run under the blocking lock
(DR-159). Kotlin keeps sending the opaque command; it only gains FAST_FORWARD/
REWIND in the PlaybackStateCompat so the system stops drawing skip arrows for a
control that scrubs. The remote-volume action block is deliberately untouched:
the handoff never applies to cast sessions, where skip really does mean advance.
Tests written first and watched fail (left: Advance, right: SeekTo). 706 Rust
tests pass, clippy 0, coverage 90%.
Move the pure determine_video_seek_strategy function and its VideoSeekStrategy
enum (plus the 5 seek-strategy unit tests) out of the command layer into
player/seek.rs, where they belong and are testable without the Tauri State
harness. commands/player.rs now imports them from crate::player. No behavior
change.