fix(player): closing the player stops every renderer, not the believed one
DR-250. The user's diagnosis, and a better fix than modelling the handoff more carefully: a close that stops only what we believe is playing is fragile by construction. A close that stops everything is correct whatever the bookkeeping thinks. Two places it did not. The teardown's stop was gated on `didStartNativePlayback && !didStopBackendEarly` — flags describing what *this component* started. A background-audio handoff swaps the renderer underneath them, so after one they describe a player that is no longer making sound and the stop was skipped entirely. The audio stream kept running and the mini player adopted it, which is exactly why a movie reappeared as an audio track. It is unconditional now; `playerStop` is idempotent, so the cost of calling it when nothing plays is a no-op round trip, against the alternative of silently leaving audio running. And `PlayerController::stop` never cleared the handoff. Leaving the base offset and the active flag behind lets a later position read be interpreted against a handoff that no longer exists. Stopping now clears both. `didStartNativePlayback` had no remaining reader and is deleted rather than silenced — dead bookkeeping about which renderer was in charge is precisely the frontend playback state this contract is meant to remove, and the eslint ratchet caught it going one over. The underlying unconfirmed state swap is still there and still worth fixing — it is written up in media-player-controller.md. This makes the symptom impossible while that lands. 789 Rust tests, 1088 frontend, lint back at 158, clippy clean both ways.
This commit is contained in:
@@ -789,6 +789,28 @@ impl PlayerController {
|
||||
drop(backend);
|
||||
self.clear_reported_time();
|
||||
|
||||
// Stopping means *nothing is playing*, from any renderer — not "the
|
||||
// thing we currently believe owns playback has been asked to stop".
|
||||
//
|
||||
// A background-audio handoff swaps which renderer that is, and the swap
|
||||
// is bookkeeping that can be mid-flight: `exit_background_audio` marks
|
||||
// the webview element the player again the moment it is called, while
|
||||
// the element has not reloaded yet. A stop aimed at what the flags say
|
||||
// is playing therefore misses the audio stream that actually is, and it
|
||||
// resurfaces in the mini player as an audio track.
|
||||
//
|
||||
// Clearing the handoff here is the other half of that: a stop that
|
||||
// leaves the base offset and the active flag behind lets the next
|
||||
// position read be interpreted against a handoff that no longer exists.
|
||||
//
|
||||
// TRACES: UR-040, UR-005 | DR-250
|
||||
if self.is_background_audio_active() {
|
||||
debug!("[PlayerController] stop: clearing an active background-audio handoff");
|
||||
}
|
||||
*self.background_audio_active.lock_safe() = false;
|
||||
self.set_background_audio_base(0.0);
|
||||
*self.html5_playing.lock_safe() = None;
|
||||
|
||||
if let Some(jellyfin_id) = jellyfin_id {
|
||||
self.report_stopped_at(jellyfin_id, position);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user