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.
This commit is contained in:
Arkadiusz Fal
2026-07-23 23:06:48 +02:00
parent faf0964746
commit 2ff248e262
2 changed files with 13 additions and 5 deletions

View File

@@ -2852,10 +2852,14 @@ extension PlayerService: PlayerBackendDelegate {
func backend(_ backend: any PlayerBackend, didChangeState playbackState: PlaybackState) { func backend(_ backend: any PlayerBackend, didChangeState playbackState: PlaybackState) {
LoggingService.shared.debug("Backend state changed to: \(playbackState)", category: .player) LoggingService.shared.debug("Backend state changed to: \(playbackState)", category: .player)
// Clear the time-update gate once playback is ready/playing, in case the // Clear the time-update gate once the new video is loaded, in case the load
// load flow exited before doing so (issue #956). Safe: stale updates from a // flow wedged before doing so (issue #956). Only .ready: it comes from mpv's
// previous video can only arrive before the new video's ready transition. // file-loaded event, which only the new load can emit. The previous video
if playbackState == .ready || playbackState == .playing, loadingVideoID != nil { // (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 loadingVideoID = nil
} }

View File

@@ -91,9 +91,13 @@ private class MPVContainerView: UIView {
// deallocating container: otherwise SwiftUI keeps reconciling a // deallocating container: otherwise SwiftUI keeps reconciling a
// half-alive view and spins the main-thread trait update loop (100% // half-alive view and spins the main-thread trait update loop (100%
// CPU hang when opening a settings detail during playback, issue #956). // CPU hang when opening a settings detail during playback, issue #956).
// 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() playerView.removeFromSuperview()
} }
} }
}
func setPlayerView(_ playerView: UIView) { func setPlayerView(_ playerView: UIView) {
// Skip only if same view AND actually our subview // Skip only if same view AND actually our subview