Background-audio handoff for video + repository/player refactor

Hand video playback off to a native audio-only stream when the app is
backgrounded or locked, with no on-device video decode (UR-040). Adds
player_enter/exit_background_audio commands, an audio-only stream URL
for video items across the repository layer, and the frontend handoff
state machine wired into VideoPlayer. Includes accompanying
repository/offline/player refactoring and regenerates the traceability
matrix.
This commit is contained in:
2026-07-22 21:52:07 +02:00
parent 4e6ab017d4
commit 3fbf6afdbc
72 changed files with 6728 additions and 2338 deletions
+49 -1
View File
@@ -19,6 +19,40 @@ export const commands = {
async playerPlayItem(item: PlayItemRequest) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_play_item", { item });
},
/**
* Enter background-audio mode: hand playback of the currently-watched video off
* to the native ExoPlayer *audio* path so the audio keeps playing while the app
* is backgrounded/locked, with no client-side video decode (UR-040).
*
* `stream_url` MUST be an audio-only URL (see
* `get_audio_only_stream_url_for_video`). The item is created as
* `MediaType::Audio` so it starts an audio session and loads into the native
* backend with `mediaType="audio"` — the WebView `<video>` is torn down on the
* frontend side, so exactly one audio source is ever active.
*
* This deliberately goes through the queue-based `play_item` path (NOT a
* side-channel) so end-of-track lands in `on_playback_ended`, which already
* honors the sleep timer (Time/Episodes/EndOfTrack) and drives autoplay-next.
* The sleep-timer state is intentionally left untouched by the handoff.
*
* TRACES: UR-040 | DR-052 | UT-061, IT-013
*/
async playerEnterBackgroundAudio(item: PlayItemRequest, positionSeconds: number) : Promise<PlayerStatus> {
return await TAURI_INVOKE("player_enter_background_audio", { item, positionSeconds });
},
/**
* Exit background-audio mode: stop the native audio player and return its final
* position so the frontend can reload the WebView `<video>` there (UR-040).
*
* Returns the position in seconds. The sleep timer is intentionally left
* untouched — if it fired while backgrounded, playback is already stopped and
* this simply reports the last position.
*
* TRACES: UR-040 | DR-052 | UT-061, IT-013
*/
async playerExitBackgroundAudio() : Promise<number> {
return await TAURI_INVOKE("player_exit_background_audio");
},
/**
* Play a queue of media items
*
@@ -1206,6 +1240,14 @@ async repositoryGetVideoStreamUrl(handle: string, itemId: string, mediaSourceId:
async repositoryGetAudioStreamUrl(handle: string, itemId: string) : Promise<string> {
return await TAURI_INVOKE("repository_get_audio_stream_url", { handle, itemId });
},
/**
* Get an audio-only stream URL for a *video* item (background-audio handoff).
*
* TRACES: UR-040 | JA-032 | UT-061
*/
async repositoryGetAudioOnlyStreamUrlForVideo(handle: string, itemId: string, mediaSourceId: string | null, startTimeSeconds: number | null, audioStreamIndex: number | null) : Promise<string> {
return await TAURI_INVOKE("repository_get_audio_only_stream_url_for_video", { handle, itemId, mediaSourceId, startTimeSeconds, audioStreamIndex });
},
/**
* Get Live TV channels (broadcast / IPTV) for browsing
*/
@@ -1760,7 +1802,13 @@ videoCodec: string;
/**
* Whether the video requires server-side transcoding
*/
needsTranscoding: boolean }
needsTranscoding: boolean;
/**
* Optional now-playing metadata. Used by the background-audio handoff so the
* lockscreen/miniplayer show the item (title/subtitle/artwork). Defaulted so
* existing video-only callers need not send them.
*/
artist?: string | null; primaryImageTag?: string | null; serverId?: string | null }
/**
* Queue context for remote transfer - what type of queue is this?
*/