Add tvOS A/V sync diagnostics on its own settings page

Surface audio-delay (±10/±100 ms) and video-sync-mode controls behind an
"A/V Sync" navigation row in Advanced settings rather than inline, keeping the
Advanced page uncluttered. Audio delay applies live to the running MPV
instance; sync mode takes effect on next playback.
This commit is contained in:
Arkadiusz Fal
2026-05-14 09:43:25 +02:00
parent 579df26284
commit c168fbae02
11 changed files with 494 additions and 2 deletions

View File

@@ -439,8 +439,13 @@ final class MPVClient: @unchecked Sendable {
// Use display-vdrop: drops/repeats frames to match display timing
// This is lighter weight than display-resample (no interpolation overhead)
// and handles both hardware and software decode gracefully
// and handles both hardware and software decode gracefully.
// On tvOS, the user may override this via the debug Video Sync Mode setting.
#if os(tvOS)
setOptionSync("video-sync", SettingsManager.tvVideoSyncModeSync().rawValue)
#else
setOptionSync("video-sync", "display-vdrop")
#endif
// Allow frame dropping when decoder can't keep up (essential for software decode)
// decoder+vo: drop at decoder level first, then at video output if still behind
@@ -480,10 +485,30 @@ final class MPVClient: @unchecked Sendable {
// Apply subtitle appearance settings
applySubtitleSettings()
// Apply user-configured fixed audio delay on tvOS (in seconds; setting is ms)
#if os(tvOS)
let delaySeconds = SettingsManager.tvAudioDelayMsSync() / 1000.0
setOptionSync("audio-delay", String(delaySeconds))
#endif
// Apply user's custom MPV options (these can override defaults)
applyCustomOptions()
}
/// Update the `audio-delay` property on a running MPV instance. Safe to call
/// while playback is active. Value is in milliseconds; converted to seconds
/// before being sent to MPV.
func updateAudioDelay(milliseconds: Double) {
mpvQueue.async { [weak self] in
guard let self, let mpv = self.mpv, !self.isDestroyed else { return }
let seconds = milliseconds / 1000.0
let str = String(seconds)
str.withCString { ptr in
_ = mpv_set_property_string(mpv, "audio-delay", ptr)
}
}
}
/// Apply subtitle appearance settings from user preferences.
private func applySubtitleSettings() {
let subtitleSettings = SettingsManager.subtitleSettingsSync()
@@ -981,6 +1006,14 @@ final class MPVClient: @unchecked Sendable {
var videoSpeedCorrection: Double?
var audioSpeedCorrection: Double?
var framedrop: String?
var audioDelay: Double?
var currentVo: String?
var currentAo: String?
var audioDevice: String?
var displayWidth: Int?
var displayHeight: Int?
var displayNames: String?
var decoderFrameDropCount: Int?
}
/// Fetch all debug properties in a single sync block to avoid multiple lock acquisitions.
@@ -1052,6 +1085,14 @@ final class MPVClient: @unchecked Sendable {
props.videoSpeedCorrection = getDouble("video-speed-correction")
props.audioSpeedCorrection = getDouble("audio-speed-correction")
props.framedrop = getString("framedrop")
props.audioDelay = getDouble("audio-delay")
props.currentVo = getString("current-vo")
props.currentAo = getString("current-ao")
props.audioDevice = getString("audio-device")
props.displayWidth = getInt("display-width")
props.displayHeight = getInt("display-height")
props.displayNames = getString("display-names")
props.decoderFrameDropCount = getInt("decoder-frame-drop-count")
#endif
return props
@@ -1281,8 +1322,16 @@ final class MPVClient: @unchecked Sendable {
props.videoSpeedCorrection = getDouble("video-speed-correction")
props.audioSpeedCorrection = getDouble("audio-speed-correction")
props.framedrop = getString("framedrop")
props.audioDelay = getDouble("audio-delay")
props.currentVo = getString("current-vo")
props.currentAo = getString("current-ao")
props.audioDevice = getString("audio-device")
props.displayWidth = getInt("display-width")
props.displayHeight = getInt("display-height")
props.displayNames = getString("display-names")
props.decoderFrameDropCount = getInt("decoder-frame-drop-count")
#endif
continuation.resume(returning: props)
}
}

View File

@@ -902,6 +902,12 @@ final class MPVBackend: PlayerBackend {
mpvClient?.updateSubtitleSettings()
}
/// Push the current tvOS audio-delay setting into the running MPV instance.
/// Caller passes milliseconds; MPVClient converts to seconds.
func updateAudioDelay(milliseconds: Double) {
mpvClient?.updateAudioDelay(milliseconds: milliseconds)
}
/// Get the actual video track dimensions from MPV.
/// Returns (width, height) or nil if not available.
func getVideoSize() -> (width: Int, height: Int)? {
@@ -963,6 +969,14 @@ final class MPVBackend: PlayerBackend {
stats.audioSpeedCorrection = props.audioSpeedCorrection
stats.framedrop = props.framedrop
stats.displayLinkFps = renderView?.displayLinkTargetFPS
stats.audioDelay = props.audioDelay
stats.currentVo = props.currentVo
stats.currentAo = props.currentAo
stats.audioDevice = props.audioDevice
stats.displayWidth = props.displayWidth
stats.displayHeight = props.displayHeight
stats.displayNames = props.displayNames
stats.decoderFrameDropCount = props.decoderFrameDropCount
#endif
return stats