From a26a853f01a708ed5a7856609f4e2103083ee37e Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 2 Aug 2026 18:10:18 +0200 Subject: [PATCH] fix(player): advance background audio-only episodes in the backend (UR-040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An episode played audio-only while the app was backgrounded stalled at the episode boundary instead of advancing, and ExoPlayer parked in STATE_ENDED — where any later play intent (lockscreen, headset, Bluetooth reconnect) replays the ended item, surfacing as the episode randomly restarting. End-of-playback is dispatched from two places and they disagreed. The Android JNI callback carried the background-audio branch but can never reach it: load_and_play sets EndReason::NewTrackLoaded at every load and nothing clears it, so the first real end consumes it and the decision is always Stop. The call that actually decides is the frontend's echo of the resulting PlaybackEnded into player_on_playback_ended — and that path had no background-audio case at all, so it started a countdown whose advance is a webview goto() that cannot start audio while backgrounded. Both dispatchers now share PlayerController::auto_advance_to_next_episode, so they cannot drift apart again. The handoff base offset moves from the BackgroundAudioOffset Tauri state onto the controller, and the advance clears it: the next episode's stream is built without StartTimeTicks, so its timeline is already absolute and a stale base made player_exit_background_audio return old_base + position_in_new_episode. Unreachable until the advance actually worked. Tests (red before the fix): - test_auto_advance_background_audio_episode_advances_in_backend - test_auto_advance_foreground_video_episode_uses_countdown - test_advance_to_next_episode_audio_only_clears_handoff_base Bump to 0.2.9. --- docs/traceability.md | 298 +++++++++++++----------- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands/player/mod.rs | 38 +-- src-tauri/src/commands/player/timers.rs | 11 +- src-tauri/src/lib.rs | 3 - src-tauri/src/player/android/mod.rs | 46 ++-- src-tauri/src/player/mod.rs | 206 +++++++++++++++- src-tauri/tauri.conf.json | 2 +- src/lib/api/bindings.ts | 2 + 11 files changed, 401 insertions(+), 211 deletions(-) diff --git a/docs/traceability.md b/docs/traceability.md index 99b31cd2..e8879b48 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -1,11 +1,11 @@ # Code Traceability Matrix -**Generated:** 8/1/2026, 10:40:16 AM +**Generated:** 8/2/2026, 6:10:09 PM ## Summary - **Total Files Scanned:** 305 -- **Total TRACES Found:** 337 +- **Total TRACES Found:** 338 - **Requirements Covered:** - User Requirements (UR): 58 - Integration Requirements (IR): 15 @@ -229,11 +229,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 17 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2256) - - **Line:** 2256 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2258) + - **Line:** 2258 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2313) - - **Line:** 2313 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2315) + - **Line:** 2315 - **Context:** `Unknown` - **File:** [`src/lib/stores/player.ts`](src/lib/stores/player.ts#L8) - **Line:** 8 @@ -285,8 +285,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 2 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2134) - - **Line:** 2134 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2136) + - **Line:** 2136 - **Context:** `export type PlaybackProgress = { itemId: string;` - **File:** [`src-tauri/src/player/media.rs`](src-tauri/src/player/media.rs#L42) - **Line:** 42 @@ -296,8 +296,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 2 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2238) - - **Line:** 2238 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2240) + - **Line:** 2240 - **Context:** `Unknown` - **File:** [`src-tauri/src/player/media.rs`](src-tauri/src/player/media.rs#L124) - **Line:** 124 @@ -339,8 +339,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 13 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2510) - - **Line:** 2510 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2512) + - **Line:** 2512 - **Context:** `export type RemoteSessionStatus = { position: number; duration: number...` - **File:** [`src/lib/stores/queue.test.ts`](src/lib/stores/queue.test.ts#L4) - **Line:** 4 @@ -475,11 +475,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 9 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L531) - - **Line:** 531 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L533) + - **Line:** 533 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L547) - - **Line:** 547 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L549) + - **Line:** 549 - **Context:** `Unknown` - **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L2) - **Line:** 2 @@ -738,8 +738,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 18 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2313) - - **Line:** 2313 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2315) + - **Line:** 2315 - **Context:** `Unknown` - **File:** [`src/lib/utils/duration.test.ts`](src/lib/utils/duration.test.ts#L4) - **Line:** 4 @@ -797,8 +797,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 10 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2570) - - **Line:** 2570 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2572) + - **Line:** 2572 - **Context:** `export type SessionInfo = { id?: string | null; userId?: string | null...` - **File:** [`src/lib/components/player/SleepTimerModal.svelte`](src/lib/components/player/SleepTimerModal.svelte#L1) - **Line:** 1 @@ -838,8 +838,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L181) - **Line:** 181 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1778) - - **Line:** 1778 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1780) + - **Line:** 1780 - **Context:** `export type DownloadsResponse = { downloads: DownloadInfo[]; stats: Do...` - **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L545) - **Line:** 545 @@ -1137,10 +1137,13 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-047 -**Locations:** 13 file(s) +**Locations:** 15 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2313) - - **Line:** 2313 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L241) + - **Line:** 241 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2315) + - **Line:** 2315 - **Context:** `Unknown` - **File:** [`src/lib/stores/nextEpisode.ts`](src/lib/stores/nextEpisode.ts#L9) - **Line:** 9 @@ -1178,6 +1181,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L145) + - **Line:** 145 + - **Context:** `Unknown` ### DR-048 @@ -1234,7 +1240,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### DR-052 -**Locations:** 14 file(s) +**Locations:** 15 file(s) - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L38) - **Line:** 38 @@ -1242,6 +1248,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L51) - **Line:** 51 - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L241) + - **Line:** 241 + - **Context:** `Unknown` - **File:** [`src/lib/components/player/backgroundAudioHandoff.test.ts`](src/lib/components/player/backgroundAudioHandoff.test.ts#L10) - **Line:** 10 - **Context:** `Unknown` @@ -1269,14 +1278,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L1264) - **Line:** 1264 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L68) - - **Line:** 68 - - **Context:** `pub struct VideoSettingsWrapper(pub Mutex);` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L583) - - **Line:** 583 +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L571) + - **Line:** 571 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L675) - - **Line:** 675 +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L663) + - **Line:** 663 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L145) + - **Line:** 145 - **Context:** `Unknown` ### DR-053 @@ -1381,11 +1390,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/routes/search/+page.svelte`](src/routes/search/+page.svelte#L14) - **Line:** 14 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1298) - - **Line:** 1298 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1300) + - **Line:** 1300 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2545) - - **Line:** 2545 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2547) + - **Line:** 2547 - **Context:** `export type SearchResult = { items: MediaItem[]; totalRecordCount: num...` - **File:** [`src/lib/stores/library.ts`](src/lib/stores/library.ts#L236) - **Line:** 236 @@ -1502,17 +1511,17 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 17 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L948) - - **Line:** 948 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L950) + - **Line:** 950 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L959) - - **Line:** 959 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L961) + - **Line:** 961 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1970) - - **Line:** 1970 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1972) + - **Line:** 1972 - **Context:** `export type MergedMediaItem = { id: string; title: string; artist: str...` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1979) - - **Line:** 1979 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1981) + - **Line:** 1981 - **Context:** `export type NetworkStateWrapperArg = { networkType: NetworkType; unmet...` - **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L81) - **Line:** 81 @@ -1653,11 +1662,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L105) - **Line:** 105 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1221) - - **Line:** 1221 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1223) + - **Line:** 1223 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1229) - - **Line:** 1229 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1231) + - **Line:** 1231 - **Context:** `Unknown` - **File:** [`src/lib/components/downloads/DownloadedBrowse.svelte`](src/lib/components/downloads/DownloadedBrowse.svelte#L10) - **Line:** 10 @@ -1715,11 +1724,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L105) - **Line:** 105 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L805) - - **Line:** 805 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L807) + - **Line:** 807 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1229) - - **Line:** 1229 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1231) + - **Line:** 1231 - **Context:** `Unknown` - **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L24) - **Line:** 24 @@ -1788,11 +1797,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L113) - **Line:** 113 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1237) - - **Line:** 1237 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1239) + - **Line:** 1239 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1723) - - **Line:** 1723 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1725) + - **Line:** 1725 - **Context:** `Unknown` - **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L17) - **Line:** 17 @@ -2267,8 +2276,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L203) - **Line:** 203 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1324) - - **Line:** 1324 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1326) + - **Line:** 1326 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L520) - **Line:** 520 @@ -2293,8 +2302,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 16 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2238) - - **Line:** 2238 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2240) + - **Line:** 2240 - **Context:** `Unknown` - **File:** [`src/lib/stores/connectivity.ts`](src/lib/stores/connectivity.ts#L10) - **Line:** 10 @@ -2346,11 +2355,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 20 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2134) - - **Line:** 2134 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2136) + - **Line:** 2136 - **Context:** `export type PlaybackProgress = { itemId: string;` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2238) - - **Line:** 2238 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2240) + - **Line:** 2240 - **Context:** `Unknown` - **File:** [`src/lib/components/player/VideoPlayer.svelte`](src/lib/components/player/VideoPlayer.svelte#L1) - **Line:** 1 @@ -2411,11 +2420,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 15 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2134) - - **Line:** 2134 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2136) + - **Line:** 2136 - **Context:** `export type PlaybackProgress = { itemId: string;` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2238) - - **Line:** 2238 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2240) + - **Line:** 2240 - **Context:** `Unknown` - **File:** [`src/lib/components/player/AudioPlayer.svelte`](src/lib/components/player/AudioPlayer.svelte#L1) - **Line:** 1 @@ -2461,14 +2470,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 66 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2256) - - **Line:** 2256 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2258) + - **Line:** 2258 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2313) - - **Line:** 2313 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2315) + - **Line:** 2315 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2510) - - **Line:** 2510 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2512) + - **Line:** 2512 - **Context:** `export type RemoteSessionStatus = { position: number; duration: number...` - **File:** [`src/lib/components/player/Controls.svelte`](src/lib/components/player/Controls.svelte#L1) - **Line:** 1 @@ -2714,11 +2723,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 15 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L531) - - **Line:** 531 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L533) + - **Line:** 533 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L547) - - **Line:** 547 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L549) + - **Line:** 549 - **Context:** `Unknown` - **File:** [`src/lib/stores/auth.test.ts`](src/lib/stores/auth.test.ts#L4) - **Line:** 4 @@ -2764,8 +2773,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 11 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2238) - - **Line:** 2238 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2240) + - **Line:** 2240 - **Context:** `Unknown` - **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L2) - **Line:** 2 @@ -2842,8 +2851,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 12 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2313) - - **Line:** 2313 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2315) + - **Line:** 2315 - **Context:** `Unknown` - **File:** [`src/lib/services/playerEvents.test.ts`](src/lib/services/playerEvents.test.ts#L4) - **Line:** 4 @@ -2881,7 +2890,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### UR-023 -**Locations:** 22 file(s) +**Locations:** 24 file(s) - **File:** [`src/routes/library/movies/+page.svelte`](src/routes/library/movies/+page.svelte#L1) - **Line:** 1 @@ -2892,8 +2901,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/routes/settings/+page.svelte`](src/routes/settings/+page.svelte#L1) - **Line:** 1 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2313) - - **Line:** 2313 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L241) + - **Line:** 241 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2315) + - **Line:** 2315 - **Context:** `Unknown` - **File:** [`src/lib/stores/nextEpisode.ts`](src/lib/stores/nextEpisode.ts#L9) - **Line:** 9 @@ -2946,19 +2958,25 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L145) + - **Line:** 145 + - **Context:** `Unknown` - **File:** [`src-tauri/src/settings.rs`](src-tauri/src/settings.rs#L1) - **Line:** 1 - **Context:** `Unknown` ### UR-026 -**Locations:** 17 file(s) +**Locations:** 19 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2313) - - **Line:** 2313 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L241) + - **Line:** 241 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2570) - - **Line:** 2570 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2315) + - **Line:** 2315 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2572) + - **Line:** 2572 - **Context:** `export type SessionInfo = { id?: string | null; userId?: string | null...` - **File:** [`src/lib/components/player/SleepTimerModal.svelte`](src/lib/components/player/SleepTimerModal.svelte#L1) - **Line:** 1 @@ -3005,6 +3023,9 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L3) - **Line:** 3 - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L145) + - **Line:** 145 + - **Context:** `Unknown` ### UR-027 @@ -3016,8 +3037,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L181) - **Line:** 181 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1778) - - **Line:** 1778 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1780) + - **Line:** 1780 - **Context:** `export type DownloadsResponse = { downloads: DownloadInfo[]; stats: Do...` - **File:** [`src-tauri/src/player/android/mod.rs`](src-tauri/src/player/android/mod.rs#L545) - **Line:** 545 @@ -3176,7 +3197,7 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, ### UR-040 -**Locations:** 26 file(s) +**Locations:** 27 file(s) - **File:** [`src/lib/api/repository-client.test.ts`](src/lib/api/repository-client.test.ts#L434) - **Line:** 434 @@ -3193,8 +3214,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L51) - **Line:** 51 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1324) - - **Line:** 1324 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L241) + - **Line:** 241 + - **Context:** `Unknown` +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1326) + - **Line:** 1326 - **Context:** `Unknown` - **File:** [`src/lib/components/player/backgroundAudioHandoff.test.ts`](src/lib/components/player/backgroundAudioHandoff.test.ts#L10) - **Line:** 10 @@ -3229,14 +3253,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src-tauri/src/player/mod.rs`](src-tauri/src/player/mod.rs#L1264) - **Line:** 1264 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L68) - - **Line:** 68 - - **Context:** `pub struct VideoSettingsWrapper(pub Mutex);` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L583) - - **Line:** 583 +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L571) + - **Line:** 571 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L675) - - **Line:** 675 +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L663) + - **Line:** 663 + - **Context:** `Unknown` +- **File:** [`src-tauri/src/commands/player/timers.rs`](src-tauri/src/commands/player/timers.rs#L145) + - **Line:** 145 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L520) - **Line:** 520 @@ -3298,11 +3322,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/routes/search/+page.svelte`](src/routes/search/+page.svelte#L22) - **Line:** 22 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1298) - - **Line:** 1298 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1300) + - **Line:** 1300 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2545) - - **Line:** 2545 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L2547) + - **Line:** 2547 - **Context:** `export type SearchResult = { items: MediaItem[]; totalRecordCount: num...` - **File:** [`src/lib/components/search/SearchScopeChips.svelte`](src/lib/components/search/SearchScopeChips.svelte#L7) - **Line:** 7 @@ -3342,8 +3366,8 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 9 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1298) - - **Line:** 1298 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1300) + - **Line:** 1300 - **Context:** `Unknown` - **File:** [`src/lib/components/search/SearchResults.svelte`](src/lib/components/search/SearchResults.svelte#L9) - **Line:** 9 @@ -3374,17 +3398,17 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, **Locations:** 17 file(s) -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L948) - - **Line:** 948 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L950) + - **Line:** 950 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L959) - - **Line:** 959 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L961) + - **Line:** 961 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1970) - - **Line:** 1970 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1972) + - **Line:** 1972 - **Context:** `export type MergedMediaItem = { id: string; title: string; artist: str...` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1979) - - **Line:** 1979 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1981) + - **Line:** 1981 - **Context:** `export type NetworkStateWrapperArg = { networkType: NetworkType; unmet...` - **File:** [`src/lib/stores/downloads.ts`](src/lib/stores/downloads.ts#L81) - **Line:** 81 @@ -3439,14 +3463,14 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L105) - **Line:** 105 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L805) - - **Line:** 805 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L807) + - **Line:** 807 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1221) - - **Line:** 1221 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1223) + - **Line:** 1223 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1229) - - **Line:** 1229 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1231) + - **Line:** 1231 - **Context:** `Unknown` - **File:** [`src/lib/components/library/MediaCard.svelte`](src/lib/components/library/MediaCard.svelte#L24) - **Line:** 24 @@ -3522,11 +3546,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/repository-client.ts`](src/lib/api/repository-client.ts#L113) - **Line:** 113 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1237) - - **Line:** 1237 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1239) + - **Line:** 1239 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1723) - - **Line:** 1723 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1725) + - **Line:** 1725 - **Context:** `Unknown` - **File:** [`src/lib/components/library/LibraryGrid.svelte`](src/lib/components/library/LibraryGrid.svelte#L17) - **Line:** 17 @@ -3611,11 +3635,11 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L51) - **Line:** 51 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L583) - - **Line:** 583 +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L571) + - **Line:** 571 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L675) - - **Line:** 675 +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L663) + - **Line:** 663 - **Context:** `Unknown` ### IT-016 @@ -3903,17 +3927,17 @@ JA-001, JA-002, JA-003, JA-004, JA-005, JA-007, JA-010, JA-011, JA-012, JA-016, - **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L51) - **Line:** 51 - **Context:** `Unknown` -- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1324) - - **Line:** 1324 +- **File:** [`src/lib/api/bindings.ts`](src/lib/api/bindings.ts#L1326) + - **Line:** 1326 - **Context:** `Unknown` - **File:** [`src/lib/player/backgroundAudio.commands.test.ts`](src/lib/player/backgroundAudio.commands.test.ts#L7) - **Line:** 7 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L583) - - **Line:** 583 +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L571) + - **Line:** 571 - **Context:** `Unknown` -- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L675) - - **Line:** 675 +- **File:** [`src-tauri/src/commands/player/mod.rs`](src-tauri/src/commands/player/mod.rs#L663) + - **Line:** 663 - **Context:** `Unknown` - **File:** [`src-tauri/src/commands/repository.rs`](src-tauri/src/commands/repository.rs#L520) - **Line:** 520 diff --git a/package.json b/package.json index 0fe3614e..1fcbfbfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jellytau", - "version": "0.2.8", + "version": "0.2.9", "description": "", "type": "module", "packageManager": "bun@1.3.5", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f22ef0e1..513ee164 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1994,7 +1994,7 @@ dependencies = [ [[package]] name = "jellytau" -version = "0.2.8" +version = "0.2.9" dependencies = [ "aes-gcm", "async-trait", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2ffa429c..d330bd0c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jellytau" -version = "0.2.8" +version = "0.2.9" description = "A Tauri App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/commands/player/mod.rs b/src-tauri/src/commands/player/mod.rs index 24c79093..efc40db2 100644 --- a/src-tauri/src/commands/player/mod.rs +++ b/src-tauri/src/commands/player/mod.rs @@ -57,18 +57,6 @@ pub struct MediaSessionManagerWrapper(pub Mutex); /// @req: DR-048 - Video settings (auto-play toggle, countdown duration) pub struct VideoSettingsWrapper(pub Mutex); -/// Base offset (seconds) for the active background-audio handoff. -/// -/// The audio-only stream is requested with `StartTimeTicks` = the handoff -/// position, so the server makes that point the stream's zero. ExoPlayer then -/// reports position RELATIVE to that zero. To convert back to an absolute -/// position on exit (so the video resumes where the audio actually reached), we -/// add this stored base to the native player's reported position. -/// -/// TRACES: UR-040 | DR-052 -#[derive(Default)] -pub struct BackgroundAudioOffset(pub Mutex); - /// Response for player state queries #[derive(specta::Type, Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -586,7 +574,6 @@ pub async fn player_play_item( pub async fn player_enter_background_audio( player: State<'_, PlayerStateWrapper>, session: State<'_, MediaSessionManagerWrapper>, - bg_offset: State<'_, BackgroundAudioOffset>, item: PlayItemRequest, position_seconds: f64, ) -> Result { @@ -636,17 +623,18 @@ pub async fn player_enter_background_audio( session_mgr.start_audio_session(media_item.clone()); } - // Remember where the video was: the audio stream's zero == this position - // (the URL was built with StartTimeTicks=position_seconds), so on exit we add - // this base to the native player's relative position to get the absolute one. - *bg_offset.0.lock().map_err(|e| e.to_string())? = position_seconds.max(0.0); - // Same base offset drives the lockscreen scrubber: ExoPlayer reports position // relative to the stream's StartTimeTicks zero, but the metadata duration is // absolute, so shift the reported position back to absolute for the scrubber. let _ = crate::player::set_lockscreen_position_offset(position_seconds.max(0.0)); let controller = player.0.lock().await; + // Remember where the video was: the audio stream's zero == this position + // (the URL was built with StartTimeTicks=position_seconds), so on exit we add + // this base to the native player's relative position to get the absolute one. + // The controller owns it so a backend-driven advance to the next episode + // clears it along with the stream it described. + controller.set_background_audio_base(position_seconds); controller .play_item(media_item) .map_err(|e| e.to_string())?; @@ -677,21 +665,15 @@ pub async fn player_enter_background_audio( #[specta::specta] pub async fn player_exit_background_audio( player: State<'_, PlayerStateWrapper>, - bg_offset: State<'_, BackgroundAudioOffset>, ) -> Result { - // The base offset (handoff position) + native player's relative position = - // the absolute position to resume the video at. Read/reset the base first. - let base = { - let mut off = bg_offset.0.lock().map_err(|e| e.to_string())?; - let b = *off; - *off = 0.0; - b - }; - // Back to foreground playback: the lockscreen scrubber is absolute again. let _ = crate::player::set_lockscreen_position_offset(0.0); let controller = player.0.lock().await; + // The base offset (handoff position) + native player's relative position = + // the absolute position to resume the video at. Zero after a backend-driven + // episode advance, whose stream already starts at its own zero. + let base = controller.take_background_audio_base(); // Capture position into a `let` BEFORE stop() — never hold work across a lock // re-entrant call (deadlock discipline, CLAUDE.md). let relative = controller.position(); diff --git a/src-tauri/src/commands/player/timers.rs b/src-tauri/src/commands/player/timers.rs index bd6be656..ca56ac64 100644 --- a/src-tauri/src/commands/player/timers.rs +++ b/src-tauri/src/commands/player/timers.rs @@ -141,6 +141,8 @@ pub async fn player_play_next_episode( /// - Frontend when HTML5 video ends (Linux/desktop) - passes itemId + repositoryHandle for the video /// - Frontend when audio track ends via backend event - no itemId/repositoryHandle needed /// - Android JNI callback also triggers this logic directly +/// +/// TRACES: UR-023, UR-026, UR-040 | DR-047, DR-052 #[tauri::command] #[specta::specta] pub async fn player_on_playback_ended( @@ -242,12 +244,17 @@ pub async fn player_on_playback_ended( }); } - // Start countdown if auto_advance enabled + // Advance if auto_advance is enabled. This is the path that actually + // runs on Android: the JNI callback's own decision is swallowed by the + // NewTrackLoaded end reason set at load, so it returns Stop, emits + // PlaybackEnded, and the frontend echoes it back into this command — + // which is where the real decision lands. if auto_advance { controller_arc .lock() .await - .start_autoplay_countdown(next_episode, countdown_seconds); + .auto_advance_to_next_episode(next_episode, countdown_seconds) + .await; } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index abb94839..3f676199 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1196,9 +1196,6 @@ pub fn run() { let video_settings = VideoSettingsWrapper(Mutex::new(VideoSettings::default())); app.manage(video_settings); - // Background-audio handoff base offset (UR-040). - app.manage(commands::player::BackgroundAudioOffset::default()); - // Initialize thumbnail cache info!("[INIT] Initializing thumbnail cache..."); let app_data_dir = if let Ok(test_data_dir) = std::env::var("JELLYTAU_DATA_DIR") { diff --git a/src-tauri/src/player/android/mod.rs b/src-tauri/src/player/android/mod.rs index a081f367..3a637381 100644 --- a/src-tauri/src/player/android/mod.rs +++ b/src-tauri/src/player/android/mod.rs @@ -915,39 +915,19 @@ pub extern "system" fn Java_com_dtourolle_jellytau_player_JellyTauPlayer_nativeO } if auto_advance { - // Background audio-only episode: the frontend that normally - // performs the advance (goto /player/) is suspended, so - // the backend must load the next episode's audio-only stream - // itself — otherwise playback just stops at the boundary. - let is_bg_audio_episode = - controller.lock().await.current_is_audio_episode(); - if is_bg_audio_episode { - log::info!( - "[Autoplay] Background audio episode — advancing to {} in backend", - next_episode.id - ); - let ctrl = controller.lock().await; - if let Err(e) = ctrl - .advance_to_next_episode_audio_only(&next_episode.id) - .await - { - log::error!( - "[Autoplay] Background audio advance failed: {} — stopping", - e - ); - if let Some(emitter) = EVENT_EMITTER.get() { - emitter.emit(PlayerStatusEvent::PlaybackEnded); - } - } else { - ctrl.emit_queue_changed(); - } - } else { - // Foreground: frontend drives the advance off the countdown. - controller - .lock() - .await - .start_autoplay_countdown(next_episode, countdown_seconds); - } + // Shared with the frontend-invoked command path + // (player_on_playback_ended) so the two dispatchers cannot + // disagree about how a background audio-only episode + // advances — they did, and the command's copy was missing + // the case entirely. That copy is the one that actually + // decides here: the end reason set at load makes this + // callback's own decision Stop, and the frontend echoes the + // resulting PlaybackEnded back into the command. + controller + .lock() + .await + .auto_advance_to_next_episode(next_episode, countdown_seconds) + .await; } } Err(e) => { diff --git a/src-tauri/src/player/mod.rs b/src-tauri/src/player/mod.rs index c2b9775d..431d286f 100644 --- a/src-tauri/src/player/mod.rs +++ b/src-tauri/src/player/mod.rs @@ -105,7 +105,7 @@ pub fn set_lockscreen_position_offset(_offset_seconds: f64) -> Result<(), String } use crate::utils::lock::MutexSafe; -use log::{debug, error, warn}; +use log::{debug, error, info, warn}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::sync::Mutex as TokioMutex; @@ -153,6 +153,21 @@ pub struct PlayerController { // Auto-play episode counter (session-based, resets on manual play) autoplay_episode_count: Arc>, + // Base offset (seconds) of the active background-audio handoff. + // + // The audio-only stream is requested with `StartTimeTicks` = the position the + // video was handed off at, so the server makes that point the stream's zero + // and the native player reports position RELATIVE to it. Adding this base back + // yields the absolute position to resume the video at on the way out. + // + // Lives on the controller (not beside the command) because the queue and this + // offset describe the same stream: whenever the controller loads a different + // one — notably the backend-driven advance to the next episode — the base has + // to move with it. + // + // TRACES: UR-040 | DR-052 + background_audio_base: Arc>, + // Last state reported by a webview-rendered HTML5