mirror of
https://github.com/yattee/yattee.git
synced 2026-07-21 06:42:01 +00:00
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:
@@ -2048,15 +2048,64 @@ extension MPVBackend: MPVClientDelegate {
|
||||
guard playbackStatsTask == nil else { return }
|
||||
|
||||
playbackStatsTask = Task { @MainActor [weak self] in
|
||||
#if os(macOS)
|
||||
var previousHealth: MPVRenderHealth?
|
||||
var previousVoDrops: Int?
|
||||
var watchdogFiredLastTick = false
|
||||
#endif
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(10))
|
||||
guard let self, !Task.isCancelled, let client = self.mpvClient else { return }
|
||||
|
||||
let props = await client.getDebugPropertiesAsync()
|
||||
|
||||
#if os(macOS)
|
||||
// Render watchdog — runs every tick regardless of the verbose
|
||||
// gate. Detects a stalled video output: playback advancing and
|
||||
// frames being consumed (skip-render) or dropped by the VO,
|
||||
// but not a single successful draw since the last tick. That
|
||||
// is the "permanent black video" signature (shared render view
|
||||
// parented in a non-visible window); attempt recovery by
|
||||
// re-attaching the view to a visible container.
|
||||
let health = self._playerView?.renderHealthSnapshot()
|
||||
if let health {
|
||||
let drawsDelta = health.draws &- (previousHealth?.draws ?? health.draws)
|
||||
let skipsDelta = health.skips &- (previousHealth?.skips ?? health.skips)
|
||||
let voDrops = props.frameDropCount
|
||||
let voDropsDelta = (voDrops ?? 0) - (previousVoDrops ?? voDrops ?? 0)
|
||||
|
||||
if watchdogFiredLastTick {
|
||||
let outcome = drawsDelta > 0 ? "succeeded" : "failed"
|
||||
LoggingService.shared.warning(
|
||||
"MPV render watchdog: recovery \(outcome) (draws=+\(drawsDelta), skips=+\(skipsDelta), attach: \(self._playerView?.attachmentDescription ?? "no view"))",
|
||||
category: .mpv
|
||||
)
|
||||
watchdogFiredLastTick = false
|
||||
}
|
||||
|
||||
if self.isPlaying, !self.isPiPActive, previousHealth != nil,
|
||||
drawsDelta == 0, skipsDelta > 20 || voDropsDelta > 50 {
|
||||
LoggingService.shared.warning(
|
||||
"MPV render watchdog: video output stalled (draws=+0, skips=+\(skipsDelta), voDrops=+\(voDropsDelta), attach: \(self._playerView?.attachmentDescription ?? "no view")) - attempting recovery",
|
||||
category: .mpv
|
||||
)
|
||||
MPVContainerNSView.recoverSharedPlayerViewIfNeeded()
|
||||
self._playerView?.resumeRendering()
|
||||
watchdogFiredLastTick = true
|
||||
}
|
||||
|
||||
previousVoDrops = voDrops ?? previousVoDrops
|
||||
}
|
||||
#endif
|
||||
|
||||
// Gate on the verbose setting each tick (not at task start) so
|
||||
// toggling it mid-playback takes effect without a reload
|
||||
guard MPVLogging.verboseEnabled else { continue }
|
||||
|
||||
let props = await client.getDebugPropertiesAsync()
|
||||
guard MPVLogging.verboseEnabled else {
|
||||
#if os(macOS)
|
||||
previousHealth = health
|
||||
#endif
|
||||
continue
|
||||
}
|
||||
|
||||
func megabytes(_ bytes: Int64) -> String {
|
||||
String(format: "%.1fMiB", Double(bytes) / 1_048_576)
|
||||
@@ -2081,6 +2130,19 @@ extension MPVBackend: MPVClientDelegate {
|
||||
}
|
||||
if !cache.isEmpty { parts.append("cache(\(cache.joined(separator: " ")))") }
|
||||
|
||||
#if os(macOS)
|
||||
if let health {
|
||||
let drawsDelta = health.draws &- (previousHealth?.draws ?? health.draws)
|
||||
let skipsDelta = health.skips &- (previousHealth?.skips ?? health.skips)
|
||||
let droppedDelta = health.droppedDraws &- (previousHealth?.droppedDraws ?? health.droppedDraws)
|
||||
parts.append("render(draws=+\(drawsDelta) skips=+\(skipsDelta) dropped=+\(droppedDelta))")
|
||||
}
|
||||
if let view = self._playerView {
|
||||
parts.append("attach(\(view.attachmentDescription))")
|
||||
}
|
||||
previousHealth = health
|
||||
#endif
|
||||
|
||||
LoggingService.shared.info("MPV playback stats: \(parts.joined(separator: " "))", category: .mpv)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user