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.
DR-245. PlayerController now holds a MediaPlayer instead of a PlayerBackend,
and every engine reaches it through that contract.
Deliberately a seam swap, not four rewrites: the existing backends are carried
across by LegacyPlayer, so MPV keeps its EQ and normalisation, ExoPlayer keeps
its media session, and nothing loses a feature to the migration. MpvPlayer
stays available for conformance until it grows the audio-settings half.
The substantive change is at the load site. Where the controller used to call
load() and then play(), it now issues one open() carrying the item and where
to begin — so the window a start position could be lost in is gone from the
controller as well as from the engines.
`state()` maps the engine's Phase back onto PlayerState using the queue, which
is what knows the item. External behaviour is unchanged.
Supporting pieces:
- The contract gains set_audio_settings/audio_settings as *provided*
methods. Engines that cannot honour them say so through Capabilities and
inherit a no-op, rather than every implementation carrying an Ok(()) it
does not mean.
- PlayerBackend is implemented for Box<dyn PlayerBackend>, without which the
boxed engine built at the composition root cannot be handed to anything
generic over the trait.
- StreamSelection::for_queued_item rebuilds a selection for an item already
in the queue, without re-negotiating. The transport falls back rather than
being sniffed out of the URL — that substring check is what DR-230 removed
— and needs_transcoding is an exact stand-in because every transcode this
app requests is HLS (DR-140).
- default-run = "jellytau". The conformance binary made a bare `cargo run`
ambiguous, which broke `tauri dev` outright. Caught by running the app
rather than by any suite, which is the argument for doing both.
789 tests, clippy -D warnings clean with and without the feature.
DR-242. Intent, not device operations.
`open` carries the start position, so no caller sequences load-then-seek and
none can race an engine's asynchronous load — the engine is the only layer
that knows when its pipeline can accept a position, and it absorbs that
internally by deferring or re-opening.
`seek` states a destination and nothing else. Whether that is an in-place seek
or a re-opened stream is the engine's business: hls.js seeks within a VOD
playlist, mpv's HLS demuxer cannot make a server transcode from a new offset.
Callers stop guessing on behalf of engines they do not own.
`snapshot` is one coherent read rather than a dozen getters, because reading
position and duration separately is how a player reported <position> / 0.0
when a file unloaded between the two calls.
`Phase::Opening` names the state the previous design could not express, and is
the direct cause of DR-241: a seek arriving with nothing loaded had no phase
to be queued against, so it was discarded.
`Capabilities` exists so callers adapt without naming engines. If a caller
ever branches on which engine it holds, this struct is missing something —
engine identity leaking into callers is the coupling DR-238 came from.
Nothing consumes it yet; PlayerController is ported in DR-245. Carries an
explicit allow(dead_code) tied to that step rather than being hidden behind
cfg(test), because it is production code being built in shippable pieces.