Add Allow Software-Decoded Formats playback setting

Lets the auto stream selector pick formats whose codec isn't hardware
decoded on the current device. Defaults off; when on, 4K VP9/AV1 can be
auto-selected on Apple TV models without those decoders. Software-decoded
streams also move into the Recommended section so the selection stays
visible without enabling advanced stream details.
This commit is contained in:
Arkadiusz Fal
2026-05-07 18:00:14 +02:00
parent 823faee012
commit 16477641ab
8 changed files with 70 additions and 7 deletions

View File

@@ -25,6 +25,7 @@ enum SettingsKey: String, CaseIterable {
case preferredSubtitlesLanguage case preferredSubtitlesLanguage
case resumeAction case resumeAction
case tvOSMenuButtonClosesVideo case tvOSMenuButtonClosesVideo
case allowSoftwareDecodedFormats
// SponsorBlock // SponsorBlock
case sponsorBlockEnabled case sponsorBlockEnabled
@@ -129,7 +130,7 @@ enum SettingsKey: String, CaseIterable {
/// in both UserDefaults and iCloud, so each platform family syncs independently. /// in both UserDefaults and iCloud, so each platform family syncs independently.
var isPlatformSpecific: Bool { var isPlatformSpecific: Bool {
switch self { switch self {
case .preferredQuality, .cellularQuality, .macPlayerMode, .listStyle, case .preferredQuality, .cellularQuality, .allowSoftwareDecodedFormats, .macPlayerMode, .listStyle,
// Home layout different UI paradigms per platform // Home layout different UI paradigms per platform
.homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout, .homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout,
.homeSectionOrder, .homeSectionVisibility, .homeSectionItemsLimit, .homeSectionLayout, .homeSectionOrder, .homeSectionVisibility, .homeSectionItemsLimit, .homeSectionLayout,

View File

@@ -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"). /// Preferred audio language code (e.g., "en", "de", "ja").
/// When set, audio streams in this language will be auto-selected and shown first. /// When set, audio streams in this language will be auto-selected and shown first.
/// nil means no preference (use original/default audio). /// nil means no preference (use original/default audio).

View File

@@ -40,6 +40,7 @@ final class SettingsManager {
var _playerVolume: Float? var _playerVolume: Float?
var _resumeAction: ResumeAction? var _resumeAction: ResumeAction?
var _tvOSMenuButtonClosesVideo: Bool? var _tvOSMenuButtonClosesVideo: Bool?
var _allowSoftwareDecodedFormats: Bool?
// SponsorBlock // SponsorBlock
var _sponsorBlockEnabled: Bool? var _sponsorBlockEnabled: Bool?
@@ -413,6 +414,7 @@ final class SettingsManager {
_playerVolume = nil _playerVolume = nil
_resumeAction = nil _resumeAction = nil
_tvOSMenuButtonClosesVideo = nil _tvOSMenuButtonClosesVideo = nil
_allowSoftwareDecodedFormats = nil
_sponsorBlockEnabled = nil _sponsorBlockEnabled = nil
_sponsorBlockCategories = nil _sponsorBlockCategories = nil
_sponsorBlockAPIURL = nil _sponsorBlockAPIURL = nil

View File

@@ -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" : { "settings.playback.quality.best" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -18047,4 +18067,4 @@
} }
}, },
"version" : "1.0" "version" : "1.0"
} }

View File

@@ -1906,9 +1906,17 @@ final class PlayerService {
filteredVideoStreams = videoOnlyStreams filteredVideoStreams = videoOnlyStreams
} }
// Filter out codecs with priority 0 (software decode) if hardware options exist // Filter out codecs with priority 0 (software decode) if hardware options exist,
let hardwareDecodableStreams = filteredVideoStreams.filter { videoCodecPriority($0.videoCodec) > 0 } // unless the user opted in to software-decoded formats (e.g. to unlock 4K VP9/AV1
let streamsToConsider = hardwareDecodableStreams.isEmpty ? filteredVideoStreams : hardwareDecodableStreams // 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 // Sort by resolution first, then by codec priority
let sortedVideo = streamsToConsider.sorted { s1, s2 in let sortedVideo = streamsToConsider.sorted { s1, s2 in

View File

@@ -92,17 +92,21 @@ extension QualitySelectorView {
} }
/// Recommended video streams (hardware-decodable codecs). /// Recommended video streams (hardware-decodable codecs).
/// When `allowSoftwareDecodedFormats` is ON, all video streams are considered recommended.
var recommendedVideoStreams: [Stream] { var recommendedVideoStreams: [Stream] {
videoStreams.filter { (stream: Stream) -> Bool in videoStreams.filter { (stream: Stream) -> Bool in
if stream.url.isFileURL { return true } if stream.url.isFileURL { return true }
if stream.isMuxed { return true } if stream.isMuxed { return true }
if allowSoftwareDecodedFormats { return true }
return !requiresSoftwareDecode(stream.videoCodec) return !requiresSoftwareDecode(stream.videoCodec)
} }
} }
/// Other video streams (software decode required). /// Other video streams (software decode required).
/// Empty when `allowSoftwareDecodedFormats` is ON those streams are now recommended.
var otherVideoStreams: [Stream] { 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.url.isFileURL { return false }
if stream.isMuxed { return false } if stream.isMuxed { return false }
return requiresSoftwareDecode(stream.videoCodec) return requiresSoftwareDecode(stream.videoCodec)

View File

@@ -71,6 +71,12 @@ struct QualitySelectorView: View {
appEnvironment?.settingsManager.showAdvancedStreamDetails ?? false 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 // MARK: - Computed Properties
/// Available tabs based on streams /// Available tabs based on streams

View File

@@ -38,7 +38,10 @@ private struct QualitySection: View {
@Bindable var settings: SettingsManager @Bindable var settings: SettingsManager
var body: some View { var body: some View {
SettingsFormSection("settings.playback.video.header") { SettingsFormSection(
"settings.playback.video.header",
footer: "settings.playback.quality.allowSoftwareDecoded.footer"
) {
PlatformMenuPicker( PlatformMenuPicker(
String(localized: "settings.playback.quality.preferred"), String(localized: "settings.playback.quality.preferred"),
selection: $settings.preferredQuality selection: $settings.preferredQuality
@@ -58,6 +61,11 @@ private struct QualitySection: View {
} }
} }
#endif #endif
Toggle(
String(localized: "settings.playback.quality.allowSoftwareDecoded"),
isOn: $settings.allowSoftwareDecodedFormats
)
} }
} }
} }