diff --git a/Yattee/Core/Settings/SettingsKey.swift b/Yattee/Core/Settings/SettingsKey.swift index 328db37c..fe2c119d 100644 --- a/Yattee/Core/Settings/SettingsKey.swift +++ b/Yattee/Core/Settings/SettingsKey.swift @@ -25,6 +25,7 @@ enum SettingsKey: String, CaseIterable { case preferredSubtitlesLanguage case resumeAction case tvOSMenuButtonClosesVideo + case allowSoftwareDecodedFormats // SponsorBlock case sponsorBlockEnabled @@ -129,7 +130,7 @@ enum SettingsKey: String, CaseIterable { /// in both UserDefaults and iCloud, so each platform family syncs independently. var isPlatformSpecific: Bool { switch self { - case .preferredQuality, .cellularQuality, .macPlayerMode, .listStyle, + case .preferredQuality, .cellularQuality, .allowSoftwareDecodedFormats, .macPlayerMode, .listStyle, // Home layout — different UI paradigms per platform .homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout, .homeSectionOrder, .homeSectionVisibility, .homeSectionItemsLimit, .homeSectionLayout, diff --git a/Yattee/Core/Settings/SettingsManager+Playback.swift b/Yattee/Core/Settings/SettingsManager+Playback.swift index e40d37ce..c20c5e82 100644 --- a/Yattee/Core/Settings/SettingsManager+Playback.swift +++ b/Yattee/Core/Settings/SettingsManager+Playback.swift @@ -75,6 +75,20 @@ extension SettingsManager { } } + /// When enabled, the auto stream selector will consider video formats whose codec + /// is not hardware-decodable on this device. Disabled by default. Useful on Apple TV + /// where 4K VP9/AV1 is otherwise excluded from auto-selection. + var allowSoftwareDecodedFormats: Bool { + get { + if let cached = _allowSoftwareDecodedFormats { return cached } + return bool(for: .allowSoftwareDecodedFormats, default: false) + } + set { + _allowSoftwareDecodedFormats = newValue + set(newValue, for: .allowSoftwareDecodedFormats) + } + } + /// Preferred audio language code (e.g., "en", "de", "ja"). /// When set, audio streams in this language will be auto-selected and shown first. /// nil means no preference (use original/default audio). diff --git a/Yattee/Core/SettingsManager.swift b/Yattee/Core/SettingsManager.swift index f23b8324..519e8e1e 100644 --- a/Yattee/Core/SettingsManager.swift +++ b/Yattee/Core/SettingsManager.swift @@ -40,6 +40,7 @@ final class SettingsManager { var _playerVolume: Float? var _resumeAction: ResumeAction? var _tvOSMenuButtonClosesVideo: Bool? + var _allowSoftwareDecodedFormats: Bool? // SponsorBlock var _sponsorBlockEnabled: Bool? @@ -413,6 +414,7 @@ final class SettingsManager { _playerVolume = nil _resumeAction = nil _tvOSMenuButtonClosesVideo = nil + _allowSoftwareDecodedFormats = nil _sponsorBlockEnabled = nil _sponsorBlockCategories = nil _sponsorBlockAPIURL = nil diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index 42546299..d0a0486a 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -12101,6 +12101,26 @@ } } }, + "settings.playback.quality.allowSoftwareDecoded" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allow Software-Decoded Formats" + } + } + } + }, + "settings.playback.quality.allowSoftwareDecoded.footer" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lets the player auto-select formats that aren't hardware decoded on this device. Playback may stutter on weaker devices." + } + } + } + }, "settings.playback.quality.best" : { "localizations" : { "en" : { @@ -18047,4 +18067,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Yattee/Services/Player/PlayerService.swift b/Yattee/Services/Player/PlayerService.swift index 67ee8c16..6815c44e 100644 --- a/Yattee/Services/Player/PlayerService.swift +++ b/Yattee/Services/Player/PlayerService.swift @@ -1906,9 +1906,17 @@ final class PlayerService { filteredVideoStreams = videoOnlyStreams } - // Filter out codecs with priority 0 (software decode) if hardware options exist - let hardwareDecodableStreams = filteredVideoStreams.filter { videoCodecPriority($0.videoCodec) > 0 } - let streamsToConsider = hardwareDecodableStreams.isEmpty ? filteredVideoStreams : hardwareDecodableStreams + // Filter out codecs with priority 0 (software decode) if hardware options exist, + // unless the user opted in to software-decoded formats (e.g. to unlock 4K VP9/AV1 + // on Apple TV models without hardware decoders for those codecs). + let allowSoftware = settingsManager?.allowSoftwareDecodedFormats ?? false + let streamsToConsider: [Stream] + if allowSoftware { + streamsToConsider = filteredVideoStreams + } else { + let hardwareDecodableStreams = filteredVideoStreams.filter { videoCodecPriority($0.videoCodec) > 0 } + streamsToConsider = hardwareDecodableStreams.isEmpty ? filteredVideoStreams : hardwareDecodableStreams + } // Sort by resolution first, then by codec priority let sortedVideo = streamsToConsider.sorted { s1, s2 in diff --git a/Yattee/Views/Player/QualitySelectorView+StreamHelpers.swift b/Yattee/Views/Player/QualitySelectorView+StreamHelpers.swift index be69d5f8..02a79dd4 100644 --- a/Yattee/Views/Player/QualitySelectorView+StreamHelpers.swift +++ b/Yattee/Views/Player/QualitySelectorView+StreamHelpers.swift @@ -92,17 +92,21 @@ extension QualitySelectorView { } /// Recommended video streams (hardware-decodable codecs). + /// When `allowSoftwareDecodedFormats` is ON, all video streams are considered recommended. var recommendedVideoStreams: [Stream] { videoStreams.filter { (stream: Stream) -> Bool in if stream.url.isFileURL { return true } if stream.isMuxed { return true } + if allowSoftwareDecodedFormats { return true } return !requiresSoftwareDecode(stream.videoCodec) } } /// Other video streams (software decode required). + /// Empty when `allowSoftwareDecodedFormats` is ON — those streams are now recommended. var otherVideoStreams: [Stream] { - videoStreams.filter { (stream: Stream) -> Bool in + if allowSoftwareDecodedFormats { return [] } + return videoStreams.filter { (stream: Stream) -> Bool in if stream.url.isFileURL { return false } if stream.isMuxed { return false } return requiresSoftwareDecode(stream.videoCodec) diff --git a/Yattee/Views/Player/QualitySelectorView.swift b/Yattee/Views/Player/QualitySelectorView.swift index 502cab07..7e72cbc8 100644 --- a/Yattee/Views/Player/QualitySelectorView.swift +++ b/Yattee/Views/Player/QualitySelectorView.swift @@ -71,6 +71,12 @@ struct QualitySelectorView: View { appEnvironment?.settingsManager.showAdvancedStreamDetails ?? false } + /// Whether the user has opted in to software-decoded formats during auto-selection. + /// When enabled, software-decoded streams are treated as recommended (no split). + var allowSoftwareDecodedFormats: Bool { + appEnvironment?.settingsManager.allowSoftwareDecodedFormats ?? false + } + // MARK: - Computed Properties /// Available tabs based on streams diff --git a/Yattee/Views/Settings/PlaybackSettingsView.swift b/Yattee/Views/Settings/PlaybackSettingsView.swift index 64937512..176c6626 100644 --- a/Yattee/Views/Settings/PlaybackSettingsView.swift +++ b/Yattee/Views/Settings/PlaybackSettingsView.swift @@ -38,7 +38,10 @@ private struct QualitySection: View { @Bindable var settings: SettingsManager var body: some View { - SettingsFormSection("settings.playback.video.header") { + SettingsFormSection( + "settings.playback.video.header", + footer: "settings.playback.quality.allowSoftwareDecoded.footer" + ) { PlatformMenuPicker( String(localized: "settings.playback.quality.preferred"), selection: $settings.preferredQuality @@ -58,6 +61,11 @@ private struct QualitySection: View { } } #endif + + Toggle( + String(localized: "settings.playback.quality.allowSoftwareDecoded"), + isOn: $settings.allowSoftwareDecodedFormats + ) } } }