feat(player): seek strategy follows what the engine says it can do

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.
This commit is contained in:
2026-08-22 22:21:42 +02:00
parent 5fcf58fa78
commit 6b3d853442
11 changed files with 161 additions and 49 deletions
+11 -1
View File
@@ -976,6 +976,13 @@ impl PlayerController {
}
}
/// What the engine currently rendering can do.
///
/// TRACES: UR-081 | DR-246
pub fn capabilities(&self) -> crate::player::media_player::Capabilities {
self.backend.lock_safe().capabilities()
}
/// Get current position
pub fn position(&self) -> f64 {
self.backend.lock_safe().snapshot().position.as_secs_f64()
@@ -2244,7 +2251,10 @@ impl Default for PlayerController {
let playback_reporter = Arc::new(TokioMutex::new(None));
let position_throttler = Arc::new(EventThrottler::new());
Self::new(
Box::new(LegacyPlayer::new(NullBackend::new())),
Box::new(LegacyPlayer::new(
NullBackend::new(),
crate::player::media_player::Capabilities::native(),
)),
playback_reporter,
position_throttler,
)