Fix macOS permanent black video after player collapse/expand cycles

In separate-window mode, hide() keeps the ordered-out window's SwiftUI
hierarchy alive (nilling contentViewController crashes AVKit), and its
updateNSView unconditionally re-attached the shared MPVOGLView -
stealing it into a window CoreAnimation never composites. The layer's
silent skip-render fallback then consumed every frame flag, so video
stayed black (climbing vo drops) until app restart.

- Decline stealing the shared view from a visible container into a
  non-visible one; owner-initiated transfers bypass the guard
- Only transfer to containers in visible windows or the window still
  tracked by ExpandedPlayerWindowManager; park the view otherwise and
  reclaim it when a container gains a window
- Log skip-render frame consumption (rate-limited) and track render
  health counters (draws/skips/dropped draws)
- Add a watchdog to the playback stats task that detects a stalled
  video output and re-attaches the view to a visible container
- Don't mount player layouts during zero-size layout passes
This commit is contained in:
Arkadiusz Fal
2026-06-21 21:55:37 +02:00
parent 1cd566ae29
commit b82241564d
6 changed files with 270 additions and 15 deletions

View File

@@ -272,6 +272,26 @@ final class MPVOGLView: NSView {
videoLayer?.resetFirstFrameTracking()
}
/// Thread-safe snapshot of the layer's render-health counters.
func renderHealthSnapshot() -> MPVRenderHealth? {
videoLayer?.renderHealthSnapshot()
}
/// Human-readable description of where this shared view is currently
/// attached, for render-health diagnostics. Main thread only.
var attachmentDescription: String {
let superviewDesc: String
if let container = superview as? MPVContainerNSView {
superviewDesc = "container=\(container.shortID)"
} else if let superview {
superviewDesc = "superview=\(type(of: superview))"
} else {
superviewDesc = "superview=nil"
}
let windowDesc = window.map { "window=#\($0.windowNumber) visible=\($0.isVisible)" } ?? "window=nil"
return "\(superviewDesc) \(windowDesc) bounds=\(Int(bounds.width))x\(Int(bounds.height))"
}
/// Clear the view to black.
func clearToBlack() {
videoLayer?.clearToBlack()