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
+35 -19
View File
@@ -25,12 +25,14 @@ pub enum VideoSeekStrategy {
///
/// # Arguments
/// * `is_local` - Whether the file is a local download
/// * `is_hls` - Whether the stream URL contains ".m3u8" (HLS stream)
/// * `seeks_transcoded_in_place` - Whether the engine rendering this stream
/// can seek a server-side transcode without re-opening it. Declared by the
/// engine via `Capabilities`, never inferred from the URL or the renderer.
/// * `needs_transcoding` - Whether the content needs transcoding
/// * `use_html5` - Whether frontend is using HTML5 video element
pub fn determine_video_seek_strategy(
is_local: bool,
is_hls: bool,
seeks_transcoded_in_place: bool,
needs_transcoding: bool,
use_html5: bool,
) -> VideoSeekStrategy {
@@ -53,14 +55,15 @@ pub fn determine_video_seek_strategy(
// native video on routed every transcoded seek into a backend seek that
// silently does nothing, and presents as "resume does not work".
if needs_transcoding {
return if use_html5 {
if is_hls {
VideoSeekStrategy::Html5NativeSeek
} else {
VideoSeekStrategy::Html5ReloadStream
}
} else {
VideoSeekStrategy::BackendReloadStream
// Whether a transcode can be seeked in place is a property of the
// engine, and the engine states it. This used to be inferred from
// `is_hls`, which held only while hls.js was the sole HLS renderer —
// and stopped holding the moment mpv became one (DR-238).
return match (seeks_transcoded_in_place, use_html5) {
(true, true) => VideoSeekStrategy::Html5NativeSeek,
(true, false) => VideoSeekStrategy::BackendNativeSeek,
(false, true) => VideoSeekStrategy::Html5ReloadStream,
(false, false) => VideoSeekStrategy::BackendReloadStream,
};
}
@@ -235,20 +238,21 @@ mod tests {
);
}
/// Test video seek strategy for HLS streams
/// Non-transcoded streams seek in place regardless of the engine's
/// transcode ability, which only applies to transcodes.
#[test]
fn test_seek_strategy_hls_stream() {
// HLS with HTML5 - frontend handles seek, don't call backend
fn test_seek_strategy_direct_stream() {
// HTML5 renders, so the frontend seeks the element
assert_eq!(
determine_video_seek_strategy(false, true, false, true),
VideoSeekStrategy::Html5NativeSeek
);
// HLS with native backend - backend handles seek
// The native engine renders, so it seeks
assert_eq!(
determine_video_seek_strategy(false, true, false, false),
VideoSeekStrategy::BackendNativeSeek
);
// HLS even with needs_transcoding flag - still native seek (HLS supports it)
// A transcode an engine says it can move: seek in place
assert_eq!(
determine_video_seek_strategy(false, true, true, true),
VideoSeekStrategy::Html5NativeSeek
@@ -265,18 +269,30 @@ mod tests {
/// every transcoded seek into a native seek that silently does nothing,
/// which presents as "resume does not work".
///
/// TRACES: UR-040 | DR-238 | UT-217
/// TRACES: UR-040 | DR-238, DR-246 | UT-217
#[test]
fn test_seek_strategy_transcoded_hls_native_backend() {
fn test_transcoded_seek_follows_the_engines_declared_ability() {
// An engine that cannot move a server-side transcode re-opens it,
// whichever side is rendering.
assert_eq!(
determine_video_seek_strategy(false, true, true, false),
determine_video_seek_strategy(false, false, true, false),
VideoSeekStrategy::BackendReloadStream
);
// The HTML5 side of the same case is unchanged: hls.js seeks in-playlist.
assert_eq!(
determine_video_seek_strategy(false, false, true, true),
VideoSeekStrategy::Html5ReloadStream
);
// hls.js can, and says so, so it seeks in place.
assert_eq!(
determine_video_seek_strategy(false, true, true, true),
VideoSeekStrategy::Html5NativeSeek
);
// The container the stream arrives in no longer decides anything: the
// same declared ability gives the same answer on the native side.
assert_eq!(
determine_video_seek_strategy(false, true, true, false),
VideoSeekStrategy::BackendNativeSeek
);
}
/// Test video seek strategy for direct play (non-transcoded) streams