fix(player): recover a failed stream on Linux instead of stopping (DR-130)

A recoverable player error meant "playback is over": the frontend's error
handler stopped the player unconditionally, so a wifi blip killed the
track. Android already decides in its JNI callback, but MpvBackend is
constructed before PlayerController exists, so its event thread has no
controller to ask.

So MPV reports the failure and the frontend echoes it into the new
player_recover_stream command — the same shape as PlaybackEnded ->
player_on_playback_ended, keeping the decision in Rust. The command
re-opens the stream where it stopped, with the existing attempt budget
and backoff, and returns whether it handled it; only a false answer
falls through to the old stop path.

Android now reports the errors it has already declined as
*unrecoverable*, so the echo never asks the same question twice.

TRACES: UR-004, UR-040 | DR-130 | UT-117
This commit is contained in:
2026-08-04 20:15:28 +02:00
parent 32f8de5c91
commit 30dc3ba7f6
10 changed files with 186 additions and 11 deletions
+23
View File
@@ -243,6 +243,29 @@ async playerPlayNextEpisode(item: PlayItemRequest) : Promise<PlayerStatus> {
async playerOnPlaybackEnded(itemId: string | null, repositoryHandle: string | null) : Promise<null> {
return await TAURI_INVOKE("player_on_playback_ended", { itemId, repositoryHandle });
},
/**
* Try to recover playback after a **recoverable** player error, reporting
* whether it was handled.
*
* The frontend's error handler stops the player, which is right for a real
* failure and wrong for a network blip — it turned every hiccup into "playback
* died". This is the echo path for backends that cannot decide in-process:
* MpvBackend is constructed before `PlayerController` exists ([`lib.rs`]), so
* its event thread has no controller to ask. It emits the error, the frontend
* echoes it here, and the decision stays in Rust — the same shape as
* `PlaybackEnded` → `player_on_playback_ended`.
*
* Returns `true` when the stream was re-opened and the caller must NOT stop the
* player; `false` when the error is real and should be surfaced as before.
* Android decides inside its JNI callback and only emits errors it has already
* declined to recover, so this reports `false` for those without a second
* opinion — the shared attempt budget is spent by then either way.
*
* TRACES: UR-004, UR-040 | DR-130 | UT-117
*/
async playerRecoverStream() : Promise<boolean> {
return await TAURI_INVOKE("player_recover_stream");
},
/**
* Report an HTML5 <video> state change (playing/paused/loading/stopped/idle).
*/