From d952a2ae5530e4ab8ac963632b9d1345c0626c82 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 23 Aug 2026 08:33:23 +0200 Subject: [PATCH] fix(player): ExoPlayer can seek a transcode in place; mpv cannot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A regression I introduced in DR-246 and did not catch, because the capability was declared once for "native engines" as though being native were the property that mattered. It is not. Speaking HLS is. ExoPlayer is a full HLS client: like hls.js it seeks within the VOD playlist it was handed and lets the server catch up. mpv's HLS demuxer will not make the server produce segments from a new offset, so it has to re-open the stream. Grouping them together declared false for both, so on Android a transcoded seek began re-opening the stream where it previously seeked in place — the same class of defect DR-238 was about, reintroduced on the platform I had not exercised. Capabilities::native() is gone, replaced by mpv() and exoplayer(), and the composition root chooses per platform through engine_capabilities(). Treating a category as a proxy for an ability is precisely the inference this design removes; a helper named after the category invited it straight back in. Not yet verified on a device. The conformance cases run against JellyTauPlayer in isolation and do not cover a transcoded seek, PiP, background audio or the media session — none of which have been exercised since the controller port. --- src-tauri/src/conformance_runner.rs | 2 +- src-tauri/src/lib.rs | 26 ++++++++++++++++++++++---- src-tauri/src/player/media_player.rs | 28 +++++++++++++++++++++++----- src-tauri/src/player/mod.rs | 2 +- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/conformance_runner.rs b/src-tauri/src/conformance_runner.rs index 985bf5e9..44724218 100644 --- a/src-tauri/src/conformance_runner.rs +++ b/src-tauri/src/conformance_runner.rs @@ -152,7 +152,7 @@ pub fn run_engine(url: &str, engine: Engine) -> u32 { std::sync::Arc::new(crate::playback_reporting::throttle::EventThrottler::new()), ) .expect("could not create the legacy backend"), - crate::player::media_player::Capabilities::native(), + crate::player::media_player::Capabilities::mpv(), )); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fc87d201..7d624e98 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -738,6 +738,27 @@ fn create_player_backend( /// Construct the tauri-specta command builder. Shared by `run()` and the /// bindings-export test so the TypeScript bindings always match the handler. +/// What the engine built for this platform can do. +/// +/// Declared per engine, not per category. ExoPlayer speaks HLS and can seek a +/// server-side transcode in place; mpv cannot, because its HLS demuxer will not +/// make the server produce segments from a new offset. Grouping them as "native +/// engines" gets that backwards — being native is not the property that +/// matters, speaking HLS is — and treating a category as a proxy for an ability +/// is exactly the inference DR-246 removed. +/// +/// TRACES: UR-081 | DR-246 +fn engine_capabilities() -> crate::player::media_player::Capabilities { + #[cfg(target_os = "android")] + { + crate::player::media_player::Capabilities::exoplayer() + } + #[cfg(not(target_os = "android"))] + { + crate::player::media_player::Capabilities::mpv() + } +} + fn specta_builder() -> Builder { Builder::::new() // Throw on error so generated `commands.*` return Promise and throw, @@ -1416,10 +1437,7 @@ pub fn run() { let player_controller = PlayerController::new( Box::new(crate::player::LegacyPlayer::new( backend, - // Both native engines decode the stream themselves; the - // webview path declares its own abilities when it becomes - // an engine (DR-248). - crate::player::media_player::Capabilities::native(), + engine_capabilities(), )), playback_reporter.clone(), position_throttler.clone(), diff --git a/src-tauri/src/player/media_player.rs b/src-tauri/src/player/media_player.rs index e379e85b..89ac6c6d 100644 --- a/src-tauri/src/player/media_player.rs +++ b/src-tauri/src/player/media_player.rs @@ -139,12 +139,12 @@ pub struct Capabilities { } impl Capabilities { - /// What a native engine of this project's kind can do. + /// mpv. /// - /// `seeks_transcoded_in_place` is false: both native engines decode the - /// stream themselves and neither can make the server transcode from a new - /// offset. hls.js is the exception, and says so for itself. - pub fn native() -> Self { + /// Cannot seek a server-side transcode in place: its HLS demuxer will not + /// make the server produce segments from a new offset, so the stream has to + /// be re-opened. + pub fn mpv() -> Self { Self { video: true, audio_settings: true, @@ -154,6 +154,24 @@ impl Capabilities { } } + /// ExoPlayer. + /// + /// **Can** seek a transcode in place. It is a full HLS client, so like + /// hls.js it seeks within the VOD playlist it was handed and lets the + /// server catch up. Grouping it with mpv as "a native engine" gets this + /// exactly backwards — being native is not the property that matters here, + /// speaking HLS is, and that is the whole reason this is declared per + /// engine rather than inferred from a category. + pub fn exoplayer() -> Self { + Self { + video: true, + audio_settings: true, + subtitle_switching: true, + audio_track_switching: true, + seeks_transcoded_in_place: true, + } + } + /// An engine that renders through the webview element, where hls.js seeks /// within the playlist it was handed. pub fn webview() -> Self { diff --git a/src-tauri/src/player/mod.rs b/src-tauri/src/player/mod.rs index 210d2a9f..cbca0101 100644 --- a/src-tauri/src/player/mod.rs +++ b/src-tauri/src/player/mod.rs @@ -2253,7 +2253,7 @@ impl Default for PlayerController { Self::new( Box::new(LegacyPlayer::new( NullBackend::new(), - crate::player::media_player::Capabilities::native(), + crate::player::media_player::Capabilities::mpv(), )), playback_reporter, position_throttler,