mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 10:25:02 +00:00
Fix tvOS MPV startup playback stability
This commit is contained in:
@@ -456,8 +456,19 @@ final class MPVClient: @unchecked Sendable {
|
||||
|
||||
// Cache settings for network streams
|
||||
setOptionSync("cache", "yes")
|
||||
#if os(tvOS)
|
||||
// Apple TV can decode 1080p60 easily, but aggressively filling a large
|
||||
// demuxer cache during startup competes with the first seconds of
|
||||
// rendering. Keep enough readahead for smooth playback without racing
|
||||
// hundreds of seconds ahead immediately after load/seek.
|
||||
setOptionSync("cache-secs", "30")
|
||||
setOptionSync("demuxer-readahead-secs", "20")
|
||||
setOptionSync("demuxer-max-bytes", "24MiB")
|
||||
setOptionSync("demuxer-max-back-bytes", "8MiB")
|
||||
#else
|
||||
setOptionSync("demuxer-max-bytes", "50MiB")
|
||||
setOptionSync("demuxer-max-back-bytes", "25MiB")
|
||||
#endif
|
||||
|
||||
// Logging - minimal logging for release builds
|
||||
setOptionSync("terminal", "no")
|
||||
|
||||
@@ -464,6 +464,8 @@ final class MPVBackend: PlayerBackend {
|
||||
currentTime = 0
|
||||
duration = 0
|
||||
bufferedTime = 0
|
||||
containerFps = 0
|
||||
renderView?.videoFPS = 60
|
||||
// When loading with external audio (non-EDL mode), wait for PLAYBACK_RESTART after audio is added
|
||||
// With EDL, both streams load atomically so no waiting needed
|
||||
isWaitingForExternalAudio = audioStream != nil && !useEDL
|
||||
@@ -474,6 +476,7 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
// Reset first-frame tracking for new content
|
||||
renderView?.resetFirstFrameTracking()
|
||||
applyStreamFrameRateHint(stream)
|
||||
|
||||
// Pause MPV before loading new content to prevent audio from playing
|
||||
// before the thumbnail hides. This is critical when reusing the backend
|
||||
@@ -1743,6 +1746,17 @@ extension MPVBackend: MPVClientDelegate {
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Seed render/display cadence from API metadata before MPV finishes probing.
|
||||
/// MPV will overwrite this later with the exact container FPS when available.
|
||||
private func applyStreamFrameRateHint(_ stream: Stream) {
|
||||
guard let fps = stream.fps, fps > 0 else { return }
|
||||
containerFps = Double(fps)
|
||||
updateRenderViewFPS()
|
||||
#if os(tvOS)
|
||||
applyTVDisplayCriteria()
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Update render view's video FPS for display link frame rate matching
|
||||
private func updateRenderViewFPS() {
|
||||
// Use cached container-fps (set via property observation to avoid sync fetch on main thread)
|
||||
@@ -1778,6 +1792,12 @@ extension MPVBackend: MPVClientDelegate {
|
||||
/// This is a fallback when no video frames or dimensions are available
|
||||
private func checkAndMarkReadyIfAudioOnlyDetected() {
|
||||
guard !isReady, !isSeeking else { return }
|
||||
// Only apply this path for streams the user actually selected as audio-only.
|
||||
// For video streams with a separate audio track (DASH), PLAYBACK_RESTART can
|
||||
// fire before mpv has demuxed the video — videoWidth/codec are still 0 — and
|
||||
// we'd incorrectly mark ready as audio-only and start playback before video
|
||||
// decoding catches up, producing an audible startup A/V desync.
|
||||
guard currentStream?.isAudioOnly == true else { return }
|
||||
// Detect audio-only: no video codec and no video dimensions from MPV
|
||||
guard videoCodec.isEmpty, videoWidth == 0, videoHeight == 0 else { return }
|
||||
// Ensure stream metadata is loaded
|
||||
|
||||
@@ -404,7 +404,9 @@ final class PlayerService {
|
||||
// Check for cancellation after stream load completes
|
||||
try Task.checkCancellation()
|
||||
|
||||
if seekTime > 0 {
|
||||
await backend.seek(to: seekTime, showLoading: false)
|
||||
}
|
||||
|
||||
// Wait for player sheet animation to complete before starting playback
|
||||
await navigationCoordinator?.waitForPlayerSheetAnimation()
|
||||
|
||||
@@ -78,8 +78,8 @@ final class TVDisplayModeManager {
|
||||
/// Pass `nil` for fields you don't have yet; this method is safe to call
|
||||
/// repeatedly as more info becomes available (e.g. fps arrives before gamma).
|
||||
func apply(fps: Double?, gamma: String?) {
|
||||
let matchFrameRate = readBoolDefaultFalse(key: "tvMatchDisplayFrameRate")
|
||||
let matchDynamicRange = readBoolDefaultFalse(key: "tvMatchDisplayDynamicRange")
|
||||
let matchFrameRate = readBool(key: "tvMatchDisplayFrameRate", default: false)
|
||||
let matchDynamicRange = readBool(key: "tvMatchDisplayDynamicRange", default: false)
|
||||
|
||||
guard matchFrameRate || matchDynamicRange else {
|
||||
clear()
|
||||
@@ -136,10 +136,13 @@ final class TVDisplayModeManager {
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func readBoolDefaultFalse(key: String) -> Bool {
|
||||
private func readBool(key: String, default defaultValue: Bool) -> Bool {
|
||||
// The SettingsManager stores these unprefixed (not platform-specific keys),
|
||||
// so we can read them directly from standard UserDefaults. Missing key = off.
|
||||
UserDefaults.standard.bool(forKey: key)
|
||||
// so we can read them directly from standard UserDefaults.
|
||||
guard UserDefaults.standard.object(forKey: key) != nil else {
|
||||
return defaultValue
|
||||
}
|
||||
return UserDefaults.standard.bool(forKey: key)
|
||||
}
|
||||
|
||||
private func activeDisplayManager() -> AVDisplayManager? {
|
||||
|
||||
Reference in New Issue
Block a user