mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 18:35:05 +00:00
Add tvOS display frame rate and dynamic range matching
Lets the Apple TV switch its HDMI output to match the playing video's frame rate and dynamic range via AVDisplayManager.preferredDisplayCriteria, driven from MPV's container-fps and video-params/gamma. Two opt-in toggles (default off) live under Playback → Display on tvOS; both are no-ops on other platforms. Anchor an AVKit class symbol so the linker keeps AVKit linked — Swift only autolinks AVFoundation here, and without AVKit the UIWindow.avDisplayManager category isn't loaded at runtime.
This commit is contained in:
@@ -108,6 +108,8 @@ enum SettingsKey: String, CaseIterable {
|
|||||||
case mpvBufferSeconds
|
case mpvBufferSeconds
|
||||||
case mpvUseEDLStreams
|
case mpvUseEDLStreams
|
||||||
case zoomTransitionsEnabled
|
case zoomTransitionsEnabled
|
||||||
|
case tvMatchDisplayFrameRate
|
||||||
|
case tvMatchDisplayDynamicRange
|
||||||
|
|
||||||
// Details panel
|
// Details panel
|
||||||
case floatingDetailsPanelSide // Landscape only - which side the panel appears on
|
case floatingDetailsPanelSide // Landscape only - which side the panel appears on
|
||||||
|
|||||||
@@ -141,6 +141,36 @@ extension SettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether tvOS should request the Apple TV switch its HDMI output to match the
|
||||||
|
/// playing video's frame rate. Has no effect unless the user also enables
|
||||||
|
/// "Match Content → Frame Rate" in tvOS Settings → Video and Audio.
|
||||||
|
/// Default is true on tvOS (no-op on other platforms).
|
||||||
|
var tvMatchDisplayFrameRate: Bool {
|
||||||
|
get {
|
||||||
|
if let cached = _tvMatchDisplayFrameRate { return cached }
|
||||||
|
return bool(for: .tvMatchDisplayFrameRate, default: false)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
_tvMatchDisplayFrameRate = newValue
|
||||||
|
set(newValue, for: .tvMatchDisplayFrameRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether tvOS should request the Apple TV switch its HDMI output to match the
|
||||||
|
/// playing video's dynamic range (SDR / HDR10 / HLG). Has no effect unless the
|
||||||
|
/// user also enables "Match Content → Dynamic Range" in tvOS Settings.
|
||||||
|
/// Default is true on tvOS (no-op on other platforms).
|
||||||
|
var tvMatchDisplayDynamicRange: Bool {
|
||||||
|
get {
|
||||||
|
if let cached = _tvMatchDisplayDynamicRange { return cached }
|
||||||
|
return bool(for: .tvMatchDisplayDynamicRange, default: false)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
_tvMatchDisplayDynamicRange = newValue
|
||||||
|
set(newValue, for: .tvMatchDisplayDynamicRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether zoom navigation transitions are enabled (iOS only).
|
/// Whether zoom navigation transitions are enabled (iOS only).
|
||||||
/// When enabled, navigating to video/channel/playlist details shows a zoom animation
|
/// When enabled, navigating to video/channel/playlist details shows a zoom animation
|
||||||
/// from the source thumbnail. Disable if experiencing visual glitches with swipe-back gestures.
|
/// from the source thumbnail. Disable if experiencing visual glitches with swipe-back gestures.
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ final class SettingsManager {
|
|||||||
var _mpvBufferSeconds: Double?
|
var _mpvBufferSeconds: Double?
|
||||||
var _mpvUseEDLStreams: Bool?
|
var _mpvUseEDLStreams: Bool?
|
||||||
var _zoomTransitionsEnabled: Bool?
|
var _zoomTransitionsEnabled: Bool?
|
||||||
|
var _tvMatchDisplayFrameRate: Bool?
|
||||||
|
var _tvMatchDisplayDynamicRange: Bool?
|
||||||
|
|
||||||
// Details panel settings
|
// Details panel settings
|
||||||
var _floatingDetailsPanelSide: FloatingPanelSide?
|
var _floatingDetailsPanelSide: FloatingPanelSide?
|
||||||
@@ -492,6 +494,8 @@ final class SettingsManager {
|
|||||||
_mpvBufferSeconds = nil
|
_mpvBufferSeconds = nil
|
||||||
_mpvUseEDLStreams = nil
|
_mpvUseEDLStreams = nil
|
||||||
_zoomTransitionsEnabled = nil
|
_zoomTransitionsEnabled = nil
|
||||||
|
_tvMatchDisplayFrameRate = nil
|
||||||
|
_tvMatchDisplayDynamicRange = nil
|
||||||
_floatingDetailsPanelSide = nil
|
_floatingDetailsPanelSide = nil
|
||||||
_floatingDetailsPanelWidth = nil
|
_floatingDetailsPanelWidth = nil
|
||||||
_landscapeDetailsPanelVisible = nil
|
_landscapeDetailsPanelVisible = nil
|
||||||
|
|||||||
@@ -12372,6 +12372,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings.playback.tvDisplayMatching.dynamicRange" : {
|
||||||
|
"comment" : "Toggle label (tvOS only)",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Match Dynamic Range"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.playback.tvDisplayMatching.footer" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Asks the Apple TV to switch its HDMI output mode to match the video being played. Also requires Match Content → Frame Rate / Dynamic Range to be enabled in tvOS Settings → Video and Audio."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.playback.tvDisplayMatching.frameRate" : {
|
||||||
|
"comment" : "Toggle label (tvOS only)",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Match Frame Rate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.playback.tvDisplayMatching.header" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Display"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings.playback.tvOSMenuButtonClosesVideo" : {
|
"settings.playback.tvOSMenuButtonClosesVideo" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@@ -545,6 +545,8 @@ final class MPVClient: @unchecked Sendable {
|
|||||||
observeProperty("video-codec", format: MPV_FORMAT_STRING)
|
observeProperty("video-codec", format: MPV_FORMAT_STRING)
|
||||||
observeProperty("hwdec-current", format: MPV_FORMAT_STRING)
|
observeProperty("hwdec-current", format: MPV_FORMAT_STRING)
|
||||||
observeProperty("hwdec-interop", format: MPV_FORMAT_STRING)
|
observeProperty("hwdec-interop", format: MPV_FORMAT_STRING)
|
||||||
|
// Color transfer for tvOS dynamic-range matching (SDR / HDR10 / HLG)
|
||||||
|
observeProperty("video-params/gamma", format: MPV_FORMAT_STRING)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Options
|
// MARK: - Options
|
||||||
|
|||||||
@@ -133,6 +133,9 @@ final class MPVBackend: PlayerBackend {
|
|||||||
private var videoCodec: String = ""
|
private var videoCodec: String = ""
|
||||||
private var hwdecCurrent: String = ""
|
private var hwdecCurrent: String = ""
|
||||||
private var hwdecInterop: String = ""
|
private var hwdecInterop: String = ""
|
||||||
|
#if os(tvOS)
|
||||||
|
private var videoGamma: String = ""
|
||||||
|
#endif
|
||||||
|
|
||||||
// Async initialization tracking
|
// Async initialization tracking
|
||||||
private var setupTask: Task<Void, Error>?
|
private var setupTask: Task<Void, Error>?
|
||||||
@@ -753,6 +756,10 @@ final class MPVBackend: PlayerBackend {
|
|||||||
duration = 0
|
duration = 0
|
||||||
bufferedTime = 0
|
bufferedTime = 0
|
||||||
currentStream = nil
|
currentStream = nil
|
||||||
|
#if os(tvOS)
|
||||||
|
videoGamma = ""
|
||||||
|
clearTVDisplayCriteria()
|
||||||
|
#endif
|
||||||
delegate?.backend(self, didChangeState: .idle)
|
delegate?.backend(self, didChangeState: .idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1631,8 +1638,19 @@ extension MPVBackend: MPVClientDelegate {
|
|||||||
if let fps = value as? Double, fps > 0 {
|
if let fps = value as? Double, fps > 0 {
|
||||||
containerFps = fps
|
containerFps = fps
|
||||||
updateRenderViewFPS()
|
updateRenderViewFPS()
|
||||||
|
#if os(tvOS)
|
||||||
|
applyTVDisplayCriteria()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
case "video-params/gamma":
|
||||||
|
if let gamma = value as? String {
|
||||||
|
videoGamma = gamma
|
||||||
|
applyTVDisplayCriteria()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
case "paused-for-cache":
|
case "paused-for-cache":
|
||||||
if let isPausedForCache = value as? Bool {
|
if let isPausedForCache = value as? Bool {
|
||||||
pausedForCache = isPausedForCache
|
pausedForCache = isPausedForCache
|
||||||
@@ -1706,6 +1724,25 @@ extension MPVBackend: MPVClientDelegate {
|
|||||||
checkAndMarkReadyIfVideoAvailable()
|
checkAndMarkReadyIfVideoAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
/// Apply tvOS display-mode criteria (frame rate / dynamic range) based on the
|
||||||
|
/// currently-cached MPV video parameters. Safe to call repeatedly as more info
|
||||||
|
/// arrives (fps may land before gamma or vice versa).
|
||||||
|
private func applyTVDisplayCriteria() {
|
||||||
|
let fps = containerFps > 0 ? containerFps : nil
|
||||||
|
let gamma = videoGamma.isEmpty ? nil : videoGamma
|
||||||
|
Task { @MainActor in
|
||||||
|
TVDisplayModeManager.shared.apply(fps: fps, gamma: gamma)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearTVDisplayCriteria() {
|
||||||
|
Task { @MainActor in
|
||||||
|
TVDisplayModeManager.shared.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Update render view's video FPS for display link frame rate matching
|
/// Update render view's video FPS for display link frame rate matching
|
||||||
private func updateRenderViewFPS() {
|
private func updateRenderViewFPS() {
|
||||||
// Use cached container-fps (set via property observation to avoid sync fetch on main thread)
|
// Use cached container-fps (set via property observation to avoid sync fetch on main thread)
|
||||||
@@ -1771,6 +1808,7 @@ extension MPVBackend: MPVClientDelegate {
|
|||||||
let hwdec = hwdecCurrent.isEmpty ? "none" : hwdecCurrent
|
let hwdec = hwdecCurrent.isEmpty ? "none" : hwdecCurrent
|
||||||
let interop = hwdecInterop.isEmpty ? "none" : hwdecInterop
|
let interop = hwdecInterop.isEmpty ? "none" : hwdecInterop
|
||||||
LoggingService.shared.debug("MPV: Video codec: \(codec), hwdec-current: \(hwdec), hwdec-interop: \(interop)", category: .mpv)
|
LoggingService.shared.debug("MPV: Video codec: \(codec), hwdec-current: \(hwdec), hwdec-interop: \(interop)", category: .mpv)
|
||||||
|
applyTVDisplayCriteria()
|
||||||
#endif
|
#endif
|
||||||
#if os(iOS) || os(tvOS)
|
#if os(iOS) || os(tvOS)
|
||||||
reactivateAudioSession()
|
reactivateAudioSession()
|
||||||
|
|||||||
187
Yattee/Services/Player/TVDisplayModeManager.swift
Normal file
187
Yattee/Services/Player/TVDisplayModeManager.swift
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
//
|
||||||
|
// TVDisplayModeManager.swift
|
||||||
|
// Yattee
|
||||||
|
//
|
||||||
|
// Drives AVDisplayManager.preferredDisplayCriteria on tvOS so the Apple TV
|
||||||
|
// switches its HDMI output to match the playing video's frame rate and
|
||||||
|
// dynamic range. Independent of the playback engine — works alongside MPV.
|
||||||
|
//
|
||||||
|
// The user must also have "Match Content → Frame Rate / Dynamic Range" enabled
|
||||||
|
// in tvOS Settings → Video and Audio for the system to honor these criteria.
|
||||||
|
//
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
import AVFoundation
|
||||||
|
import AVKit
|
||||||
|
import CoreMedia
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum TVDisplayDynamicRange {
|
||||||
|
case sdr
|
||||||
|
case hdr10
|
||||||
|
case hlg
|
||||||
|
|
||||||
|
var transferFunction: CFString {
|
||||||
|
switch self {
|
||||||
|
case .sdr: return kCMFormatDescriptionTransferFunction_ITU_R_709_2
|
||||||
|
case .hdr10: return kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ
|
||||||
|
case .hlg: return kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var colorPrimaries: CFString {
|
||||||
|
switch self {
|
||||||
|
case .sdr: return kCMFormatDescriptionColorPrimaries_ITU_R_709_2
|
||||||
|
case .hdr10, .hlg: return kCMFormatDescriptionColorPrimaries_ITU_R_2020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var yCbCrMatrix: CFString {
|
||||||
|
switch self {
|
||||||
|
case .sdr: return kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2
|
||||||
|
case .hdr10, .hlg: return kCMFormatDescriptionYCbCrMatrix_ITU_R_2020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps an MPV `video-params/gamma` string to the closest tvOS dynamic-range bucket.
|
||||||
|
/// MPV exposes gammas like `bt.1886`, `srgb`, `pq`, `hlg`. Anything we don't recognize
|
||||||
|
/// is treated as SDR.
|
||||||
|
func tvDisplayDynamicRange(fromMPVGamma gamma: String?) -> TVDisplayDynamicRange {
|
||||||
|
switch gamma?.lowercased() {
|
||||||
|
case "pq", "smpte2084", "st2084":
|
||||||
|
return .hdr10
|
||||||
|
case "hlg", "arib-std-b67":
|
||||||
|
return .hlg
|
||||||
|
default:
|
||||||
|
return .sdr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class TVDisplayModeManager {
|
||||||
|
static let shared = TVDisplayModeManager()
|
||||||
|
|
||||||
|
/// Anchors a real AVKit ObjC class symbol so the linker keeps AVKit linked.
|
||||||
|
/// Without this, AVKit's UIWindow category that adds `avDisplayManager` is
|
||||||
|
/// not loaded at runtime and the selector crashes — Swift only autolinks
|
||||||
|
/// AVFoundation here because `AVDisplayCriteria` lives there.
|
||||||
|
private static let _avkitLinkAnchor: AnyClass = AVDisplayManager.self
|
||||||
|
|
||||||
|
private var hasAppliedCriteria = false
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
_ = Self._avkitLinkAnchor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply preferred display criteria for the given video parameters.
|
||||||
|
/// 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")
|
||||||
|
|
||||||
|
guard matchFrameRate || matchDynamicRange else {
|
||||||
|
clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshRate: Float = (matchFrameRate ? Float(fps ?? 0) : 0)
|
||||||
|
let dynamicRange = matchDynamicRange ? tvDisplayDynamicRange(fromMPVGamma: gamma) : nil
|
||||||
|
|
||||||
|
// If we have neither dimension to actually request, no-op.
|
||||||
|
guard refreshRate > 0 || dynamicRange != nil else { return }
|
||||||
|
|
||||||
|
guard let manager = activeDisplayManager() else {
|
||||||
|
LoggingService.shared.debug(
|
||||||
|
"TVDisplayMode: no AVDisplayManager available (no UIWindowScene yet)",
|
||||||
|
category: .mpv
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let criteria: AVDisplayCriteria
|
||||||
|
if let dynamicRange {
|
||||||
|
let formatDescription = makeFormatDescription(for: dynamicRange)
|
||||||
|
criteria = AVDisplayCriteria(
|
||||||
|
refreshRate: refreshRate,
|
||||||
|
formatDescription: formatDescription
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Frame-rate only: use BT.709/SDR format description so the system
|
||||||
|
// doesn't switch dynamic range.
|
||||||
|
let formatDescription = makeFormatDescription(for: .sdr)
|
||||||
|
criteria = AVDisplayCriteria(
|
||||||
|
refreshRate: refreshRate,
|
||||||
|
formatDescription: formatDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.preferredDisplayCriteria = criteria
|
||||||
|
hasAppliedCriteria = true
|
||||||
|
LoggingService.shared.debug(
|
||||||
|
"TVDisplayMode: applied refreshRate=\(refreshRate), dynamicRange=\(dynamicRange.map(String.init(describing:)) ?? "nil")",
|
||||||
|
category: .mpv
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear any previously-applied criteria so tvOS reverts to the user's default mode.
|
||||||
|
func clear() {
|
||||||
|
guard hasAppliedCriteria else { return }
|
||||||
|
hasAppliedCriteria = false
|
||||||
|
guard let manager = activeDisplayManager() else { return }
|
||||||
|
manager.preferredDisplayCriteria = nil
|
||||||
|
LoggingService.shared.debug("TVDisplayMode: cleared", category: .mpv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private func readBoolDefaultFalse(key: String) -> 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func activeDisplayManager() -> AVDisplayManager? {
|
||||||
|
let scenes = UIApplication.shared.connectedScenes
|
||||||
|
.compactMap { $0 as? UIWindowScene }
|
||||||
|
let scene = scenes.first { $0.activationState == .foregroundActive }
|
||||||
|
?? scenes.first
|
||||||
|
let window = scene?.windows.first { $0.isKeyWindow } ?? scene?.windows.first
|
||||||
|
return window?.avDisplayManager
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeFormatDescription(for dynamicRange: TVDisplayDynamicRange) -> CMVideoFormatDescription {
|
||||||
|
let extensions: [CFString: Any] = [
|
||||||
|
kCMFormatDescriptionExtension_TransferFunction: dynamicRange.transferFunction,
|
||||||
|
kCMFormatDescriptionExtension_ColorPrimaries: dynamicRange.colorPrimaries,
|
||||||
|
kCMFormatDescriptionExtension_YCbCrMatrix: dynamicRange.yCbCrMatrix
|
||||||
|
]
|
||||||
|
|
||||||
|
var description: CMVideoFormatDescription?
|
||||||
|
CMVideoFormatDescriptionCreate(
|
||||||
|
allocator: kCFAllocatorDefault,
|
||||||
|
codecType: kCMVideoCodecType_HEVC,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
extensions: extensions as CFDictionary,
|
||||||
|
formatDescriptionOut: &description
|
||||||
|
)
|
||||||
|
// CMVideoFormatDescriptionCreate cannot meaningfully fail with these inputs,
|
||||||
|
// but if it does, fall back to a minimal description AVDisplayCriteria can use.
|
||||||
|
if let description {
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
var fallback: CMVideoFormatDescription?
|
||||||
|
CMVideoFormatDescriptionCreate(
|
||||||
|
allocator: kCFAllocatorDefault,
|
||||||
|
codecType: kCMVideoCodecType_HEVC,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
extensions: nil,
|
||||||
|
formatDescriptionOut: &fallback
|
||||||
|
)
|
||||||
|
return fallback!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -17,6 +17,9 @@ struct PlaybackSettingsView: View {
|
|||||||
AudioSection(settings: settings)
|
AudioSection(settings: settings)
|
||||||
SubtitlesSection(settings: settings)
|
SubtitlesSection(settings: settings)
|
||||||
BehaviorSection(settings: settings)
|
BehaviorSection(settings: settings)
|
||||||
|
#if os(tvOS)
|
||||||
|
TVDisplayMatchingSection(settings: settings)
|
||||||
|
#endif
|
||||||
QueueSection(settings: settings)
|
QueueSection(settings: settings)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
OrientationSection(settings: settings)
|
OrientationSection(settings: settings)
|
||||||
@@ -286,6 +289,31 @@ private struct BehaviorSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - TV Display Matching Section
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
private struct TVDisplayMatchingSection: View {
|
||||||
|
@Bindable var settings: SettingsManager
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
SettingsFormSection(
|
||||||
|
"settings.playback.tvDisplayMatching.header",
|
||||||
|
footer: "settings.playback.tvDisplayMatching.footer"
|
||||||
|
) {
|
||||||
|
Toggle(
|
||||||
|
String(localized: "settings.playback.tvDisplayMatching.frameRate"),
|
||||||
|
isOn: $settings.tvMatchDisplayFrameRate
|
||||||
|
)
|
||||||
|
|
||||||
|
Toggle(
|
||||||
|
String(localized: "settings.playback.tvDisplayMatching.dynamicRange"),
|
||||||
|
isOn: $settings.tvMatchDisplayDynamicRange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// MARK: - Queue Section
|
// MARK: - Queue Section
|
||||||
|
|
||||||
private struct QueueSection: View {
|
private struct QueueSection: View {
|
||||||
|
|||||||
Reference in New Issue
Block a user