From 5556be0504e2caeaef25176cd902b9703a93aa30 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Wed, 17 Jun 2026 09:25:50 +0200 Subject: [PATCH] Fix macOS player window staying visible when starting PiP after a stop MPVBackend.stop() clears the PiP bridge callbacks on macOS to prevent crashes during window close, but the backend is reused across videos and setupPiPIfNeeded is guarded by isPiPSetUp, so the callbacks were never wired again. The next PiP session then started without notifying PlayerService (no onPiPStatusChanged), leaving the player window visible alongside the PiP window, both playing. Extract the callback wiring into wirePiPBridgeCallbacks() and re-run it in startPiP() and in setupPiPIfNeeded when already set up. --- Yattee/Services/Player/MPVBackend.swift | 32 ++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Yattee/Services/Player/MPVBackend.swift b/Yattee/Services/Player/MPVBackend.swift index 27ba16eb..18e6dd4d 100644 --- a/Yattee/Services/Player/MPVBackend.swift +++ b/Yattee/Services/Player/MPVBackend.swift @@ -1243,9 +1243,14 @@ final class MPVBackend: PlayerBackend { // Only proceed with actual setup if not already done and view is in window guard !isPiPSetUp, containerView.window != nil else { - // If already set up but playerState changed, update isPiPPossible - if isPiPSetUp, let playerState { - playerState.isPiPPossible = isPiPPossible + if isPiPSetUp { + // Re-wire bridge callbacks in case stop() cleared them since + // setup (the backend is reused across videos) + wirePiPBridgeCallbacks() + // If playerState changed, update isPiPPossible + if let playerState { + playerState.isPiPPossible = isPiPPossible + } } return } @@ -1278,6 +1283,20 @@ final class MPVBackend: PlayerBackend { // Set up the bridge with this backend and the container pipBridge.setup(backend: self, in: containerView) + wirePiPBridgeCallbacks() + + // Manually notify current state now that callbacks are set up + pipBridge.notifyPiPPossibleState() + } + + /// (Re)connect the PiP bridge callbacks. stop() clears them to prevent + /// crashes during window close, but the backend is reused across videos, + /// so they must be wired again before the next PiP session — otherwise + /// PiP starts without notifying PlayerService and the player window + /// stays visible alongside the PiP window. + private func wirePiPBridgeCallbacks() { + guard let pipBridge else { return } + // Use the restore callback set by PlayerService pipBridge.onRestoreUserInterface = { [weak self] in await self?.onRestoreFromPiP?() @@ -1331,9 +1350,6 @@ final class MPVBackend: PlayerBackend { pipBridge.onPiPRenderSizeChanged = { [weak self] size in self?._playerView?.updatePiPTargetSize(size) } - - // Manually notify current state now that callbacks are set up - pipBridge.notifyPiPPossibleState() } /// Start Picture-in-Picture. @@ -1343,6 +1359,10 @@ final class MPVBackend: PlayerBackend { return } + // Re-wire bridge callbacks in case stop() cleared them since setup + // (the backend is reused across videos) + wirePiPBridgeCallbacks() + // Enable frame capture for PiP - start capturing BEFORE requesting PiP _playerView?.captureFramesForPiP = true