From 5f49f2d022c0d334ff08f05e9c75abb5476b4b96 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 25 May 2026 19:46:35 +0200 Subject: [PATCH] Fix macOS black video after re-parenting the shared player view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The player video surface is a single shared MPVOGLView that is re-parented between the mini-bar container (main window) and the expanded-player sheet (a separate sheet window). When presenting the sheet, the new container attaches the shared view before the old mini-bar container tears down, so the old container's viewWillMove(toSuperview: nil) was ripping the shared view out of its new home via removeFromSuperview() — leaving it with no window and a black surface. Symmetric, so it also blanked the mini-bar preview after collapsing. Only detach the shared view if it still belongs to the container (superview === self). Also force a repaint on window reattach so a paused frame redraws, and extend the onAppear resumeRendering to macOS. --- Yattee/Services/Player/MPV/MPVOGLView.swift | 6 ++++++ .../Player/MPVRenderViewRepresentable.swift | 17 ++++++++++++++--- Yattee/Views/Player/MPVVideoView.swift | 5 +++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Yattee/Services/Player/MPV/MPVOGLView.swift b/Yattee/Services/Player/MPV/MPVOGLView.swift index 844ca97e..642e2be4 100644 --- a/Yattee/Services/Player/MPV/MPVOGLView.swift +++ b/Yattee/Services/Player/MPV/MPVOGLView.swift @@ -170,6 +170,12 @@ final class MPVOGLView: NSView { // Update contents scale for new window videoLayer?.contentsScale = window.backingScaleFactor + + // Reattaching (e.g. macOS player sheet dismissed via ESC then re-expanded) + // detaches this shared view without stopping playback. macOS drawing is + // pull-based, so force a redraw to repaint the current frame — otherwise + // the layer stays black until MPV emits a new frame (never, if paused). + resumeRendering() } } diff --git a/Yattee/Views/Player/MPVRenderViewRepresentable.swift b/Yattee/Views/Player/MPVRenderViewRepresentable.swift index e86eb6b5..6b1f006d 100644 --- a/Yattee/Views/Player/MPVRenderViewRepresentable.swift +++ b/Yattee/Views/Player/MPVRenderViewRepresentable.swift @@ -184,10 +184,21 @@ private class MPVContainerNSView: NSView { override func viewWillMove(toSuperview newSuperview: NSView?) { super.viewWillMove(toSuperview: newSuperview) - // When being removed from superview, detach player view first - // This ensures proper cleanup order during window destruction + // When being removed from superview, detach the player view first for a clean + // teardown order during window destruction. + // + // The player view is a single SHARED instance (backend.playerView) that is + // re-parented between containers (mini bar ⇄ expanded sheet). When presenting + // the sheet, the new container attaches the shared view (AppKit auto-removes it + // from the old container) BEFORE this old container is torn down. So by the time + // we get here, `currentPlayerView` may already live in another container. Only + // remove it if it still actually belongs to us — otherwise we would rip the + // shared view out of its new home and blank the video (black screen on re-open, + // and a black mini-bar preview after collapse). if newSuperview == nil { - currentPlayerView?.removeFromSuperview() + if let playerView = currentPlayerView, playerView.superview === self { + playerView.removeFromSuperview() + } currentPlayerView = nil onDidMoveToWindow = nil } diff --git a/Yattee/Views/Player/MPVVideoView.swift b/Yattee/Views/Player/MPVVideoView.swift index 90360855..9366c2c5 100644 --- a/Yattee/Views/Player/MPVVideoView.swift +++ b/Yattee/Views/Player/MPVVideoView.swift @@ -143,11 +143,12 @@ struct MPVVideoView: View { applies: appliesAspectRatio )) .onAppear { - #if os(iOS) + #if os(iOS) || os(macOS) // Resume the MPV display link when the expanded video view appears. // MiniPlayerView pauses rendering when its video preview hides during expand; // PlayerService.playerSheetDidAppear is gated behind background playback, so it - // can't be relied on to resume. + // can't be relied on to resume. On macOS this also recovers the sheet re-open + // black screen when the shared render view reappears without a window re-parent. backend.resumeRendering() #endif // Start debug updates if overlay is already visible when view appears