From 2ff248e262b63002691d5d3bb3fdc52fe377e023 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 23 Jul 2026 23:06:48 +0200 Subject: [PATCH] Harden player view detach and time-update gate heal from #957 Guard the deinit detach by superview so a container whose render view was stolen by a newer, not-yet-windowed container cannot rip the shared view out of it. Narrow the loading-gate heal to .ready only: the old video still playing during the details/streams fetch can emit .playing, which would open the gate to stale time updates and break the stale-error suppression in play()'s catch. --- Yattee/Services/Player/PlayerService.swift | 12 ++++++++---- Yattee/Views/Player/MPVRenderViewRepresentable.swift | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Yattee/Services/Player/PlayerService.swift b/Yattee/Services/Player/PlayerService.swift index bf541b47..3f2b47bc 100644 --- a/Yattee/Services/Player/PlayerService.swift +++ b/Yattee/Services/Player/PlayerService.swift @@ -2852,10 +2852,14 @@ extension PlayerService: PlayerBackendDelegate { func backend(_ backend: any PlayerBackend, didChangeState playbackState: PlaybackState) { LoggingService.shared.debug("Backend state changed to: \(playbackState)", category: .player) - // Clear the time-update gate once playback is ready/playing, in case the - // load flow exited before doing so (issue #956). Safe: stale updates from a - // previous video can only arrive before the new video's ready transition. - if playbackState == .ready || playbackState == .playing, loadingVideoID != nil { + // Clear the time-update gate once the new video is loaded, in case the load + // flow wedged before doing so (issue #956). Only .ready: it comes from mpv's + // file-loaded event, which only the new load can emit. The previous video + // (still playing on the reused backend during the details/streams fetch) can + // emit .playing via buffering recovery or unpause — healing on that would + // open the gate to stale time updates and break the stale-error check in + // play()'s catch, silently swallowing genuine load failures. + if playbackState == .ready, loadingVideoID != nil { loadingVideoID = nil } diff --git a/Yattee/Views/Player/MPVRenderViewRepresentable.swift b/Yattee/Views/Player/MPVRenderViewRepresentable.swift index b576ce8c..a4d00512 100644 --- a/Yattee/Views/Player/MPVRenderViewRepresentable.swift +++ b/Yattee/Views/Player/MPVRenderViewRepresentable.swift @@ -91,7 +91,11 @@ private class MPVContainerView: UIView { // deallocating container: otherwise SwiftUI keeps reconciling a // half-alive view and spins the main-thread trait update loop (100% // CPU hang when opening a settings detail during playback, issue #956). - playerView.removeFromSuperview() + // Guard by superview: currentPlayerView is a stale weak ref when another + // (not yet windowed) container has stolen the view — don't rip it out. + if playerView.superview === self { + playerView.removeFromSuperview() + } } }