mirror of
https://github.com/yattee/yattee.git
synced 2026-07-21 06:42:01 +00:00
Fix tvOS GL render pipeline defects behind A10X IOFence crash
Apple TV 4K 1st gen on tvOS 26 crashes to the home screen with a "blocked by IOFence" gpuEvent at playback start (#949) and stutters with a starved demuxer cache (#947). Defensive fixes in the shared iOS/tvOS OpenGL path: - Drain in-flight GPU work (glFinish) before destroying the renderbuffer, so a pending presentRenderbuffer fence can't hold the CAEAGLLayer IOSurface being torn down or reallocated - Never leave the EAGLContext current on the main thread: framebuffer create/destroy and the background glFinish now unbind on exit, and setup no longer binds the context on main at all - Disable retained backing on tvOS; it exists only for iOS PiP frame capture and adds per-frame IOSurface fence pressure - Report swaps to mpv after each present so display-vdrop (the tvOS default video-sync) gets swap-timing feedback, matching macOS Add verbose-gated diagnostics for remote triage: playback stats every 10s (cache fill/rate, dropped frames, hwdec, avsync) and a rate-limited slow-frame warning in performRender. Addresses #949 and #947
This commit is contained in:
@@ -120,6 +120,10 @@ final class MPVBackend: PlayerBackend {
|
||||
private let bufferStallTimeout: TimeInterval = 30 // Trigger refresh after 30 seconds of stall
|
||||
private var bufferStallCheckTask: Task<Void, Never>?
|
||||
|
||||
// Periodic playback stats logging so exported user logs can diagnose
|
||||
// stuttering / cache starvation remotely (GitHub #947/#949)
|
||||
private var playbackStatsTask: Task<Void, Never>?
|
||||
|
||||
// Video dimensions for aspect ratio detection
|
||||
private var videoWidth: Int = 0
|
||||
private var videoHeight: Int = 0
|
||||
@@ -378,6 +382,7 @@ final class MPVBackend: PlayerBackend {
|
||||
private func cleanup() {
|
||||
// Stop buffer stall detection
|
||||
stopBufferStallDetection()
|
||||
stopPlaybackStatsLogging()
|
||||
|
||||
#if os(macOS)
|
||||
// Clear MPV client callbacks before destroying - these reference the render view
|
||||
@@ -749,6 +754,7 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
// Stop buffer stall detection
|
||||
stopBufferStallDetection()
|
||||
stopPlaybackStatsLogging()
|
||||
|
||||
mpvClient?.stop()
|
||||
isPlaying = false
|
||||
@@ -1836,6 +1842,7 @@ extension MPVBackend: MPVClientDelegate {
|
||||
switch event {
|
||||
case MPV_EVENT_FILE_LOADED:
|
||||
LoggingService.shared.debug("MPV: File loaded", category: .mpv)
|
||||
startPlaybackStatsLogging()
|
||||
// Log hwdec diagnostics on tvOS (use cached values to avoid sync fetch)
|
||||
#if os(tvOS)
|
||||
let codec = videoCodec.isEmpty ? "unknown" : videoCodec
|
||||
@@ -1992,4 +1999,57 @@ extension MPVBackend: MPVClientDelegate {
|
||||
bufferStallStartTime = nil
|
||||
LoggingService.shared.debug("MPV: Buffer stall detection stopped", category: .mpv)
|
||||
}
|
||||
|
||||
// MARK: - Playback Stats Logging
|
||||
|
||||
/// Log cache/frame-drop/pacing stats every 10s while a file is loaded, at INFO level
|
||||
/// so they appear in user-exported logs (diagnostics for GitHub #947/#949).
|
||||
/// Only logs while verbose MPV logging is enabled in settings.
|
||||
private func startPlaybackStatsLogging() {
|
||||
guard playbackStatsTask == nil else { return }
|
||||
|
||||
playbackStatsTask = Task { @MainActor [weak self] in
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(10))
|
||||
guard let self, !Task.isCancelled, let client = self.mpvClient else { return }
|
||||
|
||||
// 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()
|
||||
|
||||
func megabytes(_ bytes: Int64) -> String {
|
||||
String(format: "%.1fMiB", Double(bytes) / 1_048_576)
|
||||
}
|
||||
|
||||
var parts: [String] = ["paused=\(!self.isPlaying)"]
|
||||
if let hwdec = props.hwdecCurrent { parts.append("hwdec=\(hwdec)") }
|
||||
if let fps = props.estimatedVfFps { parts.append(String(format: "vf-fps=%.2f", fps)) }
|
||||
if let avsync = props.avsync { parts.append(String(format: "avsync=%.3f", avsync)) }
|
||||
var drops: [String] = []
|
||||
if let vo = props.frameDropCount { drops.append("vo=\(vo)") }
|
||||
if let dec = props.decoderFrameDropCount { drops.append("dec=\(dec)") }
|
||||
if let mistimed = props.mistimedFrameCount { drops.append("mistimed=\(mistimed)") }
|
||||
if let delayed = props.voDelayedFrameCount { drops.append("delayed=\(delayed)") }
|
||||
if !drops.isEmpty { parts.append("dropped(\(drops.joined(separator: " ")))") }
|
||||
var cache: [String] = []
|
||||
if let duration = props.demuxerCacheDuration { cache.append(String(format: "dur=%.1fs", duration)) }
|
||||
if let state = props.cacheState {
|
||||
cache.append("fw=\(megabytes(state.forwardBytes))")
|
||||
cache.append("rate=\(megabytes(state.inputRate))/s")
|
||||
cache.append("eof=\(state.eofCached)")
|
||||
}
|
||||
if !cache.isEmpty { parts.append("cache(\(cache.joined(separator: " ")))") }
|
||||
|
||||
LoggingService.shared.info("MPV playback stats: \(parts.joined(separator: " "))", category: .mpv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop periodic playback stats logging.
|
||||
private func stopPlaybackStatsLogging() {
|
||||
playbackStatsTask?.cancel()
|
||||
playbackStatsTask = nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user