mirror of
https://github.com/yattee/yattee.git
synced 2026-07-24 16:22:04 +00:00
Fix black video, freezes, autoplay and clock on Apple TV HD (A8)
Fixes four bugs behind yattee#956 on Apple TV HD (AppleTV5,3, A8, tvOS 26.5), where video played audio only with a permanently black picture, never autoplayed, the clock stayed at 0:00, and navigating during/after playback could freeze the whole app. 1. A8 GL driver deadlock (device-specific). glTexImage2D uploading float32 data into a float16 texture hangs in libGLImage's glgProcessPixelsWithProcessor (a dispatch_group_wait that never returns), permanently wedging mpv_render_context_render. mpv 0.37 (commit 703f1588) turned on dithering (dither-depth=auto) and LUT-based scalers by default; on the A8 (no GL_EXT_texture_norm16) both upload float weights via that fatal conversion, and per-frame CPU plane uploads (videotoolbox-copy / software decode) hit the same path. Yattee 1.x shipped mpv 0.36 with these off, which is why the device worked there. Worked around on AppleTV5,3 only: dither-depth=no, hwdec=videotoolbox (zero-copy via IOSurface), and bilinear scalers. Confirmed by two on-device lldb backtraces. 2. Swift cooperative-pool starvation (all platforms). MPVClient's event loop ran as Task.detached and blocked a cooperative-pool thread for the client's entire lifetime. With two clients alive (active + pre-warmed) on a 2-core A8, both pool threads were held and every await in the app starved: the load pipeline froze at its first Task.sleep, killing autoplay, Now Playing and progress saving. Moved the event loop to a dedicated Thread; EAGLContext creation now bridges through GCD instead of Task.detached. 3. Leaked time-update gate (all platforms). PlayerService dropped all time updates while loadingVideoID was set; when a load task died mid-flight the flag leaked and the clock stayed at 0:00 during playback. Heal the gate on the ready/playing transition and leak-proof play() with a defer; log the previously silent load cancellation. 4. Orphaned player view (all platforms). Detach the shared render view in MPVContainerView.deinit when no successor container exists, and skip framebuffer recreation while the view is windowless, to stop a 100% CPU SwiftUI trait-update loop when opening a settings detail during playback.
This commit is contained in:
@@ -250,6 +250,13 @@ final class PlayerService {
|
||||
// Mark that we're loading this video - time updates will be ignored until loading completes
|
||||
loadingVideoID = video.id
|
||||
|
||||
// Release the gate on any exit path (a newer play() claims it with its own
|
||||
// id, so only clear our own). Prevents the time-update gate from leaking if
|
||||
// the load flow exits early (issue #956).
|
||||
defer {
|
||||
if loadingVideoID == video.id { loadingVideoID = nil }
|
||||
}
|
||||
|
||||
state.setPlaybackState(.loading)
|
||||
state.isFirstFrameReady = false // Reset until first frame of new video is rendered
|
||||
state.isBufferReady = false // Reset until buffer is ready for smooth playback
|
||||
@@ -2844,6 +2851,14 @@ extension PlayerService: PlayerBackendDelegate {
|
||||
|
||||
func backend(_ backend: any PlayerBackend, didChangeState playbackState: PlaybackState) {
|
||||
LoggingService.shared.debug("Backend state changed to: \(playbackState)", category: .player)
|
||||
|
||||
// Clear the time-update gate once playback is ready/playing, in case the
|
||||
// load flow exited before doing so (issue #956). Safe: stale updates from a
|
||||
// previous video can only arrive before the new video's ready transition.
|
||||
if playbackState == .ready || playbackState == .playing, loadingVideoID != nil {
|
||||
loadingVideoID = nil
|
||||
}
|
||||
|
||||
state.setPlaybackState(playbackState)
|
||||
delegate?.playerService(self, didChangeState: playbackState)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user