From 8139fc2b1742279ceb48e5a6493fc8f78f0b4d60 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 6 Jun 2026 14:17:52 +0200 Subject: [PATCH] fix(tvos): make HDR toggle disable Dolby Vision HDR-off now forces SDR display criteria for DV like it does for HDR10, and the runtime toggle re-applies the criteria. Refs #1262 (part A). --- ios/Runner/MpvPlayer/MpvPlayerCore.swift | 19 ++++++++++---- .../apple/MpvPlayer/MpvPlayerCoreBase.swift | 26 +++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/ios/Runner/MpvPlayer/MpvPlayerCore.swift b/ios/Runner/MpvPlayer/MpvPlayerCore.swift index 06272af2..8a3cf14f 100644 --- a/ios/Runner/MpvPlayer/MpvPlayerCore.swift +++ b/ios/Runner/MpvPlayer/MpvPlayerCore.swift @@ -268,10 +268,19 @@ class MpvPlayerCore: MpvPlayerCoreBase { doviCompatibilityId: doviCompatibilityId ) let sourceRange: DisplayDynamicRange = sourceHasDolbyVision ? .dolbyVision : sourceBaseRange - var displayRange: DisplayDynamicRange = - sourceHasDolbyVision - ? .dolbyVision - : Self.supportedDisplayDynamicRange(for: sourceBaseRange) + // The HDR toggle (`hdrEnabled`) is authoritative on tvOS: when it's off, + // drive the HDMI link in SDR regardless of the source — Dolby Vision + // included — so turning HDR off actually leaves DV mode (issue #1262). + // This is the only path that gates the HDMI mode on tvOS; + // target-colorspace-hint is inert in the avfoundation VO and EDR is iOS. + var displayRange: DisplayDynamicRange + if !hdrEnabled { + displayRange = .sdr + } else if sourceHasDolbyVision { + displayRange = .dolbyVision + } else { + displayRange = Self.supportedDisplayDynamicRange(for: sourceBaseRange) + } guard displayManager.isDisplayCriteriaMatchingEnabled else { clearDisplayCriteria(displayManager, reason: "matching disabled") return false @@ -288,7 +297,7 @@ class MpvPlayerCore: MpvPlayerCoreBase { doviProfile: doviProfile, doviLevel: doviLevel, doviCompatibilityId: doviCompatibilityId) - if formatDescription == nil, sourceHasDolbyVision { + if formatDescription == nil, sourceHasDolbyVision, hdrEnabled { displayRange = sourceBaseRange formatDescription = Self.makeDisplayFormatDescription( dynamicRange: displayRange, diff --git a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift index 36bcdbfd..f7d2e541 100644 --- a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift +++ b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift @@ -97,6 +97,7 @@ class MpvPlayerCoreBase: NSObject { private var cachedVideoPrimaries: String? private var cachedVideoColorMatrix: String? private var serverDisplayCriteriaActive = false + private var lastServerCriteria: ServerDisplayCriteria? private var cachedDvConversionMode = "auto" private var cachedDvConversionLogEnabled = false var hdrEnabled: Bool { @@ -231,6 +232,7 @@ class MpvPlayerCoreBase: NSObject { func setServerDisplayCriteria(_ criteria: ServerDisplayCriteria?) { cacheLock.lock() serverDisplayCriteriaActive = criteria != nil + lastServerCriteria = criteria cacheLock.unlock() let apply = { [weak self] in @@ -278,6 +280,21 @@ class MpvPlayerCoreBase: NSObject { } } + /// Re-evaluate the tvOS HDMI display mode using the most recent criteria. + /// On tvOS the HDR toggle only reaches the display through this path, so the + /// runtime toggle calls this to switch DV/HDR ⇄ SDR without reloading. + func reapplyDisplayCriteria() { + cacheLock.lock() + let criteria = lastServerCriteria + cacheLock.unlock() + + if let criteria { + setServerDisplayCriteria(criteria) + } else { + scheduleDisplayCriteriaUpdate() + } + } + func setupMpv() -> Bool { #if os(macOS) guard let renderLayer = metalLayer else { return false } @@ -509,6 +526,15 @@ class MpvPlayerCoreBase: NSObject { DispatchQueue.main.async { self.updateEDRMode(sigPeak: sigPeak) } + + // On tvOS the toggle only takes effect through the HDMI display-mode path + // (target-colorspace-hint is inert in the avfoundation VO and EDR is + // iOS-only), so re-evaluate the display criteria with the new flag. + #if os(tvOS) + DispatchQueue.main.async { + self.reapplyDisplayCriteria() + } + #endif } /// PiP presents the AVSampleBufferDisplayLayer directly, so subtitles must