diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index fd0d85f0..868d900d 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -23,6 +23,8 @@ final class SettingsCoordinator: NavigationCoordinatable { @Route(.push) var nativePlayerSettings = makeNativePlayerSettings @Route(.push) + var maximumBitrateSettings = makeMaximumBitrateSettings + @Route(.push) var quickConnect = makeQuickConnectAuthorize @Route(.push) var resetUserPassword = makeResetUserPassword @@ -65,6 +67,8 @@ final class SettingsCoordinator: NavigationCoordinatable { var serverDetail = makeServerDetail @Route(.modal) var videoPlayerSettings = makeVideoPlayerSettings + @Route(.modal) + var maximumBitrateSettings = makeMaximumBitrateSettings #endif #if os(iOS) @@ -73,6 +77,11 @@ final class SettingsCoordinator: NavigationCoordinatable { NativeVideoPlayerSettingsView() } + @ViewBuilder + func makeMaximumBitrateSettings() -> some View { + MaximumBitrateSettingsView() + } + @ViewBuilder func makeQuickConnectAuthorize() -> some View { QuickConnectAuthorizeView() @@ -166,6 +175,12 @@ final class SettingsCoordinator: NavigationCoordinatable { func makeVideoPlayerSettings() -> NavigationViewCoordinator { NavigationViewCoordinator(VideoPlayerSettingsCoordinator()) } + + func makeMaximumBitrateSettings() -> NavigationViewCoordinator { + NavigationViewCoordinator { + MaximumBitrateSettingsView() + } + } #endif @ViewBuilder diff --git a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift index 05d5b6b6..d2eb60c5 100644 --- a/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPI/BaseItemDto/BaseItemDto+VideoPlayerViewModel.swift @@ -13,20 +13,19 @@ import JellyfinAPI import Logging extension BaseItemDto { - func videoPlayerViewModel(with mediaSource: MediaSourceInfo) async throws -> VideoPlayerViewModel { - let currentVideoPlayerType = Defaults[.VideoPlayer.videoPlayerType] - // TODO: fix bitrate settings - let tempOverkillBitrate = 360_000_000 - let profile = DeviceProfile.build(for: currentVideoPlayerType, maxBitrate: tempOverkillBitrate) + let currentVideoBitrate = Defaults[.VideoPlayer.appMaximumBitrate] + + let maxBitrate = try await getMaxBitrate(for: currentVideoBitrate) + let profile = DeviceProfile.build(for: currentVideoPlayerType, maxBitrate: maxBitrate) let userSession = Container.shared.currentUserSession()! let playbackInfo = PlaybackInfoDto(deviceProfile: profile) let playbackInfoParameters = Paths.GetPostedPlaybackInfoParameters( userID: userSession.user.id, - maxStreamingBitrate: tempOverkillBitrate + maxStreamingBitrate: maxBitrate ) let request = Paths.getPostedPlaybackInfo( @@ -47,11 +46,11 @@ extension BaseItemDto { } func liveVideoPlayerViewModel(with mediaSource: MediaSourceInfo, logger: Logger) async throws -> VideoPlayerViewModel { - let currentVideoPlayerType = Defaults[.VideoPlayer.videoPlayerType] - // TODO: fix bitrate settings - let tempOverkillBitrate = 360_000_000 - var profile = DeviceProfile.build(for: currentVideoPlayerType, maxBitrate: tempOverkillBitrate) + let currentVideoBitrate = Defaults[.VideoPlayer.appMaximumBitrate] + + let maxBitrate = try await getMaxBitrate(for: currentVideoBitrate) + var profile = DeviceProfile.build(for: currentVideoPlayerType, maxBitrate: maxBitrate) if Defaults[.Experimental.liveTVForceDirectPlay] { profile.directPlayProfiles = [DirectPlayProfile(type: .video)] } @@ -61,7 +60,7 @@ extension BaseItemDto { let playbackInfo = PlaybackInfoDto(deviceProfile: profile) let playbackInfoParameters = Paths.GetPostedPlaybackInfoParameters( userID: userSession.user.id, - maxStreamingBitrate: tempOverkillBitrate + maxStreamingBitrate: maxBitrate ) let request = Paths.getPostedPlaybackInfo( @@ -100,4 +99,27 @@ extension BaseItemDto { playSessionID: response.value.playSessionID! ) } + + private func getMaxBitrate(for bitrate: PlaybackBitrate) async throws -> Int { + let settingBitrate = Defaults[.VideoPlayer.appMaximumBitrateTest] + + guard bitrate != .auto else { + return try await testBitrate(with: settingBitrate.rawValue) + } + return bitrate.rawValue + } + + private func testBitrate(with testSize: Int) async throws -> Int { + precondition(testSize > 0, "testSize must be greater than zero") + + let userSession = Container.shared.currentUserSession()! + + let testStartTime = Date() + try await userSession.client.send(Paths.getBitrateTestBytes(size: testSize)) + let testDuration = Date().timeIntervalSince(testStartTime) + let testSizeBits = Double(testSize * 8) + let testBitrate = testSizeBits / testDuration + + return Int(testBitrate) + } } diff --git a/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift b/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift new file mode 100644 index 00000000..a40a84ce --- /dev/null +++ b/Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift @@ -0,0 +1,67 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Foundation +import JellyfinAPI + +enum PlaybackBitrate: Int, CaseIterable, Defaults.Serializable, Displayable { + case auto = 0 + case max = 360_000_000 + case mbps120 = 120_000_000 + case mbps80 = 80_000_000 + case mbps60 = 60_000_000 + case mbps40 = 40_000_000 + case mbps20 = 20_000_000 + case mbps15 = 15_000_000 + case mbps10 = 10_000_000 + case mbps8 = 8_000_000 + case mbps6 = 6_000_000 + case mbps4 = 4_000_000 + case mbps3 = 3_000_000 + case kbps1500 = 1_500_000 + case kbps720 = 720_000 + case kbps420 = 420_000 + + var displayTitle: String { + switch self { + case .auto: + return L10n.bitrateAuto + case .max: + return L10n.bitrateMax + case .mbps120: + return L10n.bitrateMbps120 + case .mbps80: + return L10n.bitrateMbps80 + case .mbps60: + return L10n.bitrateMbps60 + case .mbps40: + return L10n.bitrateMbps40 + case .mbps20: + return L10n.bitrateMbps20 + case .mbps15: + return L10n.bitrateMbps15 + case .mbps10: + return L10n.bitrateMbps10 + case .mbps8: + return L10n.bitrateMbps8 + case .mbps6: + return L10n.bitrateMbps6 + case .mbps4: + return L10n.bitrateMbps4 + case .mbps3: + return L10n.bitrateMbps3 + case .kbps1500: + return L10n.bitrateKbps1500 + case .kbps720: + return L10n.bitrateKbps720 + case .kbps420: + return L10n.bitrateKbps420 + } + } +} diff --git a/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift b/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift new file mode 100644 index 00000000..800e4c4f --- /dev/null +++ b/Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift @@ -0,0 +1,34 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Defaults +import Foundation +import JellyfinAPI + +enum PlaybackBitrateTestSize: Int, CaseIterable, Defaults.Serializable, Displayable { + case largest = 10_000_000 + case larger = 7_500_000 + case regular = 5_000_000 + case smaller = 2_500_000 + case smallest = 1_000_000 + + var displayTitle: String { + switch self { + case .largest: + return L10n.largest + case .larger: + return L10n.larger + case .regular: + return L10n.regular + case .smaller: + return L10n.smaller + case .smallest: + return L10n.smallest + } + } +} diff --git a/Shared/Resources/bitrates.json b/Shared/Resources/bitrates.json deleted file mode 100644 index 2b5863ce..00000000 --- a/Shared/Resources/bitrates.json +++ /dev/null @@ -1,58 +0,0 @@ -[ - { - "name": "4K - 120 Mbps", - "value": 120000000 - }, - { - "name": "4K - 100 Mbps", - "value": 100000000 - }, - { - "name": "4K - 80 Mbps", - "value": 80000000 - }, - { - "name": "1080p - 60 Mbps", - "value": 60000000 - }, - { - "name": "1080p - 40 Mbps", - "value": 40000000 - }, - { - "name": "1080p - 20 Mbps", - "value": 20000000 - }, - { - "name": "1080p - 15 Mbps", - "value": 15000000 - }, - { - "name": "1080p - 10 Mbps", - "value": 10000000 - }, - { - "name": "720p - 8 Mbps", - "value": 8000000 - }, - { - "name": "720p - 6 Mbps", - "value": 6000000 - }, - { - "name": "720p - 4 Mbps", - "value": 4000000 - }, - { - "name": "480p - 3 Mbps", - "value": 3000000 - }, - { - "name": "480p - 1.5 Mbps", - "value": 1500000 - }, - { - "name": "480p - 740 Kbps", - "value": 740000 - } -] diff --git a/Shared/Services/SwiftfinDefaults.swift b/Shared/Services/SwiftfinDefaults.swift index b1c1a214..afb8bd14 100644 --- a/Shared/Services/SwiftfinDefaults.swift +++ b/Shared/Services/SwiftfinDefaults.swift @@ -171,6 +171,8 @@ extension Defaults.Keys { enum VideoPlayer { + static let appMaximumBitrate: Key = UserKey("appMaximumBitrate", default: .auto) + static let appMaximumBitrateTest: Key = UserKey("appMaximumBitrateTest", default: .regular) static let autoPlayEnabled: Key = UserKey("autoPlayEnabled", default: true) static let barActionButtons: Key<[VideoPlayerActionButton]> = UserKey( "barActionButtons", diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index b4474573..8004ccc6 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -54,6 +54,40 @@ internal enum L10n { internal static let back = L10n.tr("Localizable", "back", fallback: "Back") /// Bar Buttons internal static let barButtons = L10n.tr("Localizable", "barButtons", fallback: "Bar Buttons") + /// Auto + internal static let bitrateAuto = L10n.tr("Localizable", "bitrateAuto", fallback: "Auto") + /// 480p - 1.5 Mbps + internal static let bitrateKbps1500 = L10n.tr("Localizable", "bitrateKbps1500", fallback: "480p - 1.5 Mbps") + /// 360p - 420 Kbps + internal static let bitrateKbps420 = L10n.tr("Localizable", "bitrateKbps420", fallback: "360p - 420 Kbps") + /// 480p - 720 Kbps + internal static let bitrateKbps720 = L10n.tr("Localizable", "bitrateKbps720", fallback: "480p - 720 Kbps") + /// Maximum + internal static let bitrateMax = L10n.tr("Localizable", "bitrateMax", fallback: "Maximum") + /// 1080p - 10 Mbps + internal static let bitrateMbps10 = L10n.tr("Localizable", "bitrateMbps10", fallback: "1080p - 10 Mbps") + /// 4K - 120 Mbps + internal static let bitrateMbps120 = L10n.tr("Localizable", "bitrateMbps120", fallback: "4K - 120 Mbps") + /// 1080p - 15 Mbps + internal static let bitrateMbps15 = L10n.tr("Localizable", "bitrateMbps15", fallback: "1080p - 15 Mbps") + /// 1080p - 20 Mbps + internal static let bitrateMbps20 = L10n.tr("Localizable", "bitrateMbps20", fallback: "1080p - 20 Mbps") + /// 480p - 3 Mbps + internal static let bitrateMbps3 = L10n.tr("Localizable", "bitrateMbps3", fallback: "480p - 3 Mbps") + /// 720p - 4 Mbps + internal static let bitrateMbps4 = L10n.tr("Localizable", "bitrateMbps4", fallback: "720p - 4 Mbps") + /// 1080p - 40 Mbps + internal static let bitrateMbps40 = L10n.tr("Localizable", "bitrateMbps40", fallback: "1080p - 40 Mbps") + /// 720p - 6 Mbps + internal static let bitrateMbps6 = L10n.tr("Localizable", "bitrateMbps6", fallback: "720p - 6 Mbps") + /// 1080p - 60 Mbps + internal static let bitrateMbps60 = L10n.tr("Localizable", "bitrateMbps60", fallback: "1080p - 60 Mbps") + /// 720p - 8 Mbps + internal static let bitrateMbps8 = L10n.tr("Localizable", "bitrateMbps8", fallback: "720p - 8 Mbps") + /// 4K - 80 Mbps + internal static let bitrateMbps80 = L10n.tr("Localizable", "bitrateMbps80", fallback: "4K - 80 Mbps") + /// Larger tests result in a more accurate bitrate but may delay playback + internal static let bitrateTestDescription = L10n.tr("Localizable", "bitrateTestDescription", fallback: "Larger tests result in a more accurate bitrate but may delay playback") /// Blue internal static let blue = L10n.tr("Localizable", "blue", fallback: "Blue") /// Bugs and Features @@ -244,6 +278,8 @@ internal enum L10n { } /// Logs internal static let logs = L10n.tr("Localizable", "logs", fallback: "Logs") + /// Maximum Bitrate + internal static let maximumBitrate = L10n.tr("Localizable", "maximumBitrate", fallback: "Maximum Bitrate") /// Media internal static let media = L10n.tr("Localizable", "media", fallback: "Media") /// Menu Buttons @@ -570,6 +606,8 @@ internal enum L10n { internal static let systemControlGesturesEnabled = L10n.tr("Localizable", "systemControlGesturesEnabled", fallback: "System Control Gestures Enabled") /// Tags internal static let tags = L10n.tr("Localizable", "tags", fallback: "Tags") + /// Test Size + internal static let testSize = L10n.tr("Localizable", "testSize", fallback: "Test Size") /// Timestamp internal static let timestamp = L10n.tr("Localizable", "timestamp", fallback: "Timestamp") /// Timestamp Type diff --git a/Swiftfin tvOS/Views/SettingsView/MaximumBitrateSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/MaximumBitrateSettingsView.swift new file mode 100644 index 00000000..cda5ef1f --- /dev/null +++ b/Swiftfin tvOS/Views/SettingsView/MaximumBitrateSettingsView.swift @@ -0,0 +1,47 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +struct MaximumBitrateSettingsView: View { + @Default(.VideoPlayer.appMaximumBitrate) + private var appMaximumBitrate + @Default(.VideoPlayer.appMaximumBitrateTest) + private var appMaximumBitrateTest + + var body: some View { + SplitFormWindowView() + .descriptionView { + Image(systemName: "network") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 400) + } + .contentView { + Section { + CaseIterablePicker( + L10n.maximumBitrate, + selection: $appMaximumBitrate + ) + + if appMaximumBitrate == PlaybackBitrate.auto { + CaseIterablePicker( + L10n.testSize, + selection: $appMaximumBitrateTest + ) + } + } footer: { + if appMaximumBitrate == PlaybackBitrate.auto { + Text(L10n.bitrateTestDescription) + } + } + } + .navigationTitle(L10n.maximumBitrate) + } +} diff --git a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift index 086eea3d..3a7d9e16 100644 --- a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift @@ -64,6 +64,12 @@ struct SettingsView: View { .onSelect { router.route(to: \.videoPlayerSettings) } + + ChevronButton(L10n.maximumBitrate) + .onSelect { + router.route(to: \.maximumBitrateSettings) + } + } header: { L10n.videoPlayer.text } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index f2c1b325..4ff202be 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -14,6 +14,12 @@ 4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; }; 4E16FD582C01A32700110147 /* LetterPickerOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */; }; 4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; }; + 4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; }; + 4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; }; + 4E73E2AE2C420207002D2A78 /* MaximumBitrateSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2AD2C420207002D2A78 /* MaximumBitrateSettingsView.swift */; }; + 4E73E2B02C4211CA002D2A78 /* MaximumBitrateSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2AF2C4211CA002D2A78 /* MaximumBitrateSettingsView.swift */; }; + 4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; }; + 4E762AAF2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */; }; 4E8B34EA2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; }; 4E8B34EB2AB91B6E0018F305 /* ItemFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; @@ -28,7 +34,6 @@ 534D4FF726A7D7CC000A7A48 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534D4FEB26A7D7CC000A7A48 /* Localizable.strings */; }; 535870632669D21600D05A09 /* SwiftfinApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870622669D21600D05A09 /* SwiftfinApp.swift */; }; 535870672669D21700D05A09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 535870662669D21700D05A09 /* Assets.xcassets */; }; - 5358707E2669D64F00D05A09 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; 535870AD2669D8DD00D05A09 /* ItemFilterCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535870AC2669D8DD00D05A09 /* ItemFilterCollection.swift */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; }; 5364F455266CA0DC0026ECBA /* BaseItemPerson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5364F454266CA0DC0026ECBA /* BaseItemPerson.swift */; }; @@ -145,7 +150,6 @@ 62ECA01826FA685A00E8EBB7 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62ECA01726FA685A00E8EBB7 /* DeepLink.swift */; }; 6334175B287DDFB9000603CE /* QuickConnectAuthorizeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175A287DDFB9000603CE /* QuickConnectAuthorizeView.swift */; }; 6334175D287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6334175C287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift */; }; - AE8C3159265D6F90008AA076 /* bitrates.json in Resources */ = {isa = PBXBuildFile; fileRef = AE8C3158265D6F90008AA076 /* bitrates.json */; }; BD0BA22B2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */; }; BD0BA22C2AD6503B00306A8D /* OnlineVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */; }; BD0BA22E2AD6508C00306A8D /* DownloadVideoPlayerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */; }; @@ -932,6 +936,10 @@ 4E16FD522C01840C00110147 /* LetterPickerBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerBar.swift; sourceTree = ""; }; 4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LetterPickerOrientation.swift; sourceTree = ""; }; 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = ""; }; + 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = ""; }; + 4E73E2AD2C420207002D2A78 /* MaximumBitrateSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaximumBitrateSettingsView.swift; sourceTree = ""; }; + 4E73E2AF2C4211CA002D2A78 /* MaximumBitrateSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaximumBitrateSettingsView.swift; sourceTree = ""; }; + 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaybackBitrate.swift; sourceTree = ""; }; 4E8B34E92AB91B6E0018F305 /* ItemFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemFilter.swift; sourceTree = ""; }; 531690E6267ABD79005D8AB9 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 531AC8BE26750DE20091C7EB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; @@ -1048,7 +1056,6 @@ 6334175A287DDFB9000603CE /* QuickConnectAuthorizeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectAuthorizeView.swift; sourceTree = ""; }; 6334175C287DE0D0000603CE /* QuickConnectAuthorizeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickConnectAuthorizeViewModel.swift; sourceTree = ""; }; 637FCAF3287B5B2600C0A353 /* UDPBroadcast.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = UDPBroadcast.xcframework; path = Carthage/Build/UDPBroadcast.xcframework; sourceTree = ""; }; - AE8C3158265D6F90008AA076 /* bitrates.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = bitrates.json; sourceTree = ""; }; BD0BA22A2AD6503B00306A8D /* OnlineVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineVideoPlayerManager.swift; sourceTree = ""; }; BD0BA22D2AD6508C00306A8D /* DownloadVideoPlayerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadVideoPlayerManager.swift; sourceTree = ""; }; BD3957742C112A330078CEF8 /* ButtonSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonSection.swift; sourceTree = ""; }; @@ -1672,6 +1679,15 @@ path = Components; sourceTree = ""; }; + 4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */ = { + isa = PBXGroup; + children = ( + 4E762AAD2C3A1A95004D1579 /* PlaybackBitrate.swift */, + 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */, + ); + path = PlaybackBitrate; + sourceTree = ""; + }; 5310694F2684E7EE00CFFDBA /* VideoPlayer */ = { isa = PBXGroup; children = ( @@ -1784,7 +1800,6 @@ E1FCD08E26C466F3007C8DCF /* Errors */, 621338912660106C00A81A2A /* Extensions */, 535870AB2669D8D300D05A09 /* Objects */, - AE8C3157265D6F5E008AA076 /* Resources */, 091B5A852683142E00D78B61 /* ServerDiscovery */, E1549654296CA2EF00C4EF88 /* Services */, 6286F09F271C0AA500C40ED5 /* Strings */, @@ -1810,6 +1825,7 @@ E1DE2B4E2B983F3200F6715F /* LibraryParent */, E1AA331E2782639D00F6439C /* OverlayType.swift */, E1C925F62887504B002A7A66 /* PanDirectionGestureRecognizer.swift */, + 4E3A785D2C3B87A400D33C11 /* PlaybackBitrate */, E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */, E1937A60288F32DB00CB80AA /* Poster.swift */, E1CCF12D28ABF989006CAC9E /* PosterDisplayType.swift */, @@ -2185,14 +2201,6 @@ path = Coordinators; sourceTree = ""; }; - AE8C3157265D6F5E008AA076 /* Resources */ = { - isa = PBXGroup; - children = ( - AE8C3158265D6F90008AA076 /* bitrates.json */, - ); - path = Resources; - sourceTree = ""; - }; BD0BA2292AD6501300306A8D /* VideoPlayerManager */ = { isa = PBXGroup; children = ( @@ -3468,6 +3476,7 @@ E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */, E104C86F296E087200C1C3F9 /* IndicatorSettingsView.swift */, E16AF11B292C98A7001422A8 /* GestureSettingsView.swift */, + 4E73E2AD2C420207002D2A78 /* MaximumBitrateSettingsView.swift */, E15756332936851D00976E1F /* NativeVideoPlayerSettingsView.swift */, E1545BD62BDC559500D9578F /* UserProfileSettingsView */, E1BE1CEB2BDB68BC008176A9 /* SettingsView */, @@ -3482,6 +3491,7 @@ E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */, E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */, E104C872296E0D0A00C1C3F9 /* IndicatorSettingsView.swift */, + 4E73E2AF2C4211CA002D2A78 /* MaximumBitrateSettingsView.swift */, 5398514426B64DA100101B49 /* SettingsView.swift */, E1549679296CB4B000C4EF88 /* VideoPlayerSettingsView.swift */, ); @@ -3772,7 +3782,6 @@ 53913C0826D323FE00EB3286 /* Localizable.strings in Resources */, 53913C1126D323FE00EB3286 /* Localizable.strings in Resources */, 535870672669D21700D05A09 /* Assets.xcassets in Resources */, - 5358707E2669D64F00D05A09 /* bitrates.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3796,7 +3805,6 @@ 53913BEF26D323FE00EB3286 /* Localizable.strings in Resources */, 53913C0726D323FE00EB3286 /* Localizable.strings in Resources */, 53913C1026D323FE00EB3286 /* Localizable.strings in Resources */, - AE8C3159265D6F90008AA076 /* bitrates.json in Resources */, 5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3919,6 +3927,7 @@ E187A60529AD2E25008387E6 /* StepperView.swift in Sources */, E1575E71293E77B5001665B1 /* RepeatingTimer.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* AppSettingsCoordinator.swift in Sources */, + 4E73E2B02C4211CA002D2A78 /* MaximumBitrateSettingsView.swift in Sources */, E10231452BCF8A51009D71FC /* ChannelProgram.swift in Sources */, E146A9D92BE6E9830034DA1E /* StoredValue.swift in Sources */, E13DD3FA2717E961009D4DAF /* SelectUserViewModel.swift in Sources */, @@ -3982,6 +3991,7 @@ E14EA16A2BF7333B00DE757A /* UserProfileImageViewModel.swift in Sources */, E1575E98293E7B1E001665B1 /* UIApplication.swift in Sources */, E12376B12A33DB33001F5B44 /* MediaSourceInfoCoordinator.swift in Sources */, + 4E73E2A72C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */, E17885A4278105170094FBCF /* SFSymbolButton.swift in Sources */, E13DD3ED27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, E1763A272BF303C9004DF6AB /* ServerSelectionMenu.swift in Sources */, @@ -4163,6 +4173,7 @@ E102315F2BCF8B75009D71FC /* VideoPlayerWrapperCoordinator.swift in Sources */, E1C9260D2887565C002A7A66 /* CinematicScrollView.swift in Sources */, E1575E90293E7B1E001665B1 /* EdgeInsets.swift in Sources */, + 4E762AAF2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */, E1E6C43D29AECC310064123F /* BarActionButtons.swift in Sources */, E1E6C44529AECCF20064123F /* PlayNextItemActionButton.swift in Sources */, 6264E88D273850380081A12A /* Strings.swift in Sources */, @@ -4329,6 +4340,7 @@ E11895B32893844A0042947B /* BackgroundParallaxHeaderModifier.swift in Sources */, E19D41B02BF2B7540082B8B2 /* URLSessionConfiguration.swift in Sources */, E172D3AD2BAC9DF8007B4647 /* SeasonItemViewModel.swift in Sources */, + 4E762AAE2C3A1A95004D1579 /* PlaybackBitrate.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, E175AFF3299AC117004DCF52 /* DebugSettingsView.swift in Sources */, @@ -4542,6 +4554,7 @@ E18E01DD288747230022598C /* iPadOSSeriesItemContentView.swift in Sources */, E14EA1692BF7330A00DE757A /* UserProfileImageViewModel.swift in Sources */, E18ACA952A15A3E100BB4F35 /* (null) in Sources */, + 4E73E2AE2C420207002D2A78 /* MaximumBitrateSettingsView.swift in Sources */, E1D5C39B28DF993400CDBEFB /* ThumbSlider.swift in Sources */, E1DC983D296DEB9B00982F06 /* UnwatchedIndicator.swift in Sources */, E1FE69AA28C29CC20021BC93 /* LandscapePosterProgressBar.swift in Sources */, @@ -4642,6 +4655,7 @@ E1A3E4C72BB74E50005C59F8 /* EpisodeCard.swift in Sources */, E1153DB42BBA80FB00424D36 /* EmptyCard.swift in Sources */, E1D3043528D1763100587289 /* SeeAllButton.swift in Sources */, + 4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */, E172D3B22BACA569007B4647 /* EpisodeContent.swift in Sources */, E13F05EC28BC9000003499D2 /* LibraryDisplayType.swift in Sources */, 4E16FD572C01A32700110147 /* LetterPickerOrientation.swift in Sources */, diff --git a/Swiftfin/Views/SettingsView/MaximumBitrateSettingsView.swift b/Swiftfin/Views/SettingsView/MaximumBitrateSettingsView.swift new file mode 100644 index 00000000..2ade4dd7 --- /dev/null +++ b/Swiftfin/Views/SettingsView/MaximumBitrateSettingsView.swift @@ -0,0 +1,41 @@ +// +// Swiftfin is subject to the terms of the Mozilla Public +// License, v2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. +// +// Copyright (c) 2024 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI + +struct MaximumBitrateSettingsView: View { + + @Default(.VideoPlayer.appMaximumBitrate) + private var appMaximumBitrate + @Default(.VideoPlayer.appMaximumBitrateTest) + private var appMaximumBitrateTest + + var body: some View { + Form { + Section { + CaseIterablePicker( + L10n.maximumBitrate, + selection: $appMaximumBitrate + ) + + if appMaximumBitrate == PlaybackBitrate.auto { + CaseIterablePicker( + L10n.testSize, + selection: $appMaximumBitrateTest + ) + } + } footer: { + if appMaximumBitrate == PlaybackBitrate.auto { + Text(L10n.bitrateTestDescription) + } + } + } + .navigationTitle(L10n.maximumBitrate) + } +} diff --git a/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift b/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift index a137b82c..b70eec14 100644 --- a/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift +++ b/Swiftfin/Views/SettingsView/SettingsView/SettingsView.swift @@ -71,6 +71,11 @@ struct SettingsView: View { .onSelect { router.route(to: \.videoPlayerSettings) } + + ChevronButton(L10n.maximumBitrate) + .onSelect { + router.route(to: \.maximumBitrateSettings) + } } Section(L10n.accessibility) { diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index 243dab8a..cb199bd6 100644 Binary files a/Translations/en.lproj/Localizable.strings and b/Translations/en.lproj/Localizable.strings differ