From 56bd62db80809190388493e917fc75d80654bd3c Mon Sep 17 00:00:00 2001 From: Joe Date: Tue, 23 Jul 2024 05:18:28 -0600 Subject: [PATCH] App-Wide Bitrate Limit (#1147) * Creation of bitrate selections that mirror Jellyfin-Web. The goal is to eventually allow for these same selections to be available for usage in the Player itself to set the max bitrate per playback session. This App-Wide setting is for things like preserving data (Mobile) or for areas that have perpetually have low bandwidth (AppleTV). These settings currently default to 'Auto' which is the current limit of 360,000,000 bps / 360 mpbs. I have added a spot in BaseItemDTO+VideoPlayerViewModel to get the smaller amount between 360 Mpbs and the App Maximum Setting. This exists so I can go back and update this to get the Minumum between the Player Session max bitrate and the App Setting max bitrate. Test on iPhone 10S, AppleTV 3rd Gen, and the iPhone 15 Pro via enumulator. * Fix Bitrate naming (360p vs 480p) and remove the setting nested in a second section. * Creation of a Maximum setting with 360mbps and an auto that gets the bitrate at playback. * Remove comments for code where I want to eventually put it for better clarify * Linting fixes * Change the Playback Bitrate to an Int from a String since the Bitrate is valuable but the string isn't. Run the SwiftFormat on the maxBitrate function. * Migrate the settings to their own menu with both the bitrate and the optional test size when auto is used. * Creation of an enum filterValues function for Bitrate. This way, the selection on the Player Overlay (eventually) can be filtered to only include bitrates that are less than or equal to the App Setting for Maximum Bitrate. This should help prevent confusion / remove bandwidth conflicts. The eventual Player Overlay setting should never conflict with the App-Wide Setting and should only offer options that are less than the App-Wide Setting. * Change the videoPlayerViewModel to take parameters instead of defaults. Move the defaults up one level to be called there. Split the bitrate test from the getMaxBitrate to better guard against dividing against 0 and also split out the logic to be easier to read. Change the PlaybackBitrate filter to always include Auto and, when auto, include ALL bitrates. This filter is not currently used. * Remove the PlaybackBitrate FilterValues since this is not needed and will be created ad-hoc. * Update the bitrateTestDuration verbage to better reflect that you're changing the size of the bitrate test and not just increasing the duration. Re-use the existing largest to smallest labels since there isn't a ton of benefit using "Longest to Shortest" so this should re-use existing localization. Comment the Labels. No functional changes. Only an update to labels. * Delete the Bitrate.json file but retain the Resources folder. * Remove Resource Folder. --------- Co-authored-by: Joe Kribs --- Shared/Coordinators/SettingsCoordinator.swift | 15 ++++ .../BaseItemDto+VideoPlayerViewModel.swift | 44 +++++++++--- .../PlaybackBitrate/PlaybackBitrate.swift | 67 ++++++++++++++++++ .../PlaybackBitrateTestSize.swift | 34 +++++++++ Shared/Resources/bitrates.json | 58 --------------- Shared/Services/SwiftfinDefaults.swift | 2 + Shared/Strings/Strings.swift | 38 ++++++++++ .../MaximumBitrateSettingsView.swift | 47 ++++++++++++ .../Views/SettingsView/SettingsView.swift | 6 ++ Swiftfin.xcodeproj/project.pbxproj | 42 +++++++---- .../MaximumBitrateSettingsView.swift | 41 +++++++++++ .../SettingsView/SettingsView.swift | 5 ++ Translations/en.lproj/Localizable.strings | Bin 19866 -> 23678 bytes 13 files changed, 316 insertions(+), 83 deletions(-) create mode 100644 Shared/Objects/PlaybackBitrate/PlaybackBitrate.swift create mode 100644 Shared/Objects/PlaybackBitrate/PlaybackBitrateTestSize.swift delete mode 100644 Shared/Resources/bitrates.json create mode 100644 Swiftfin tvOS/Views/SettingsView/MaximumBitrateSettingsView.swift create mode 100644 Swiftfin/Views/SettingsView/MaximumBitrateSettingsView.swift 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 243dab8a87fe6af2201aa127c156324ae8ea6170..cb199bd63925d3675d6aa8f142d745fe0ab6e923 100644 GIT binary patch delta 3783 zcmb_f!EVz)5S<>0I3OV;jwa$%sn8G;A`lWq+^Pr(_yO0UB_eG?6AXQ z(2~w6!~ggA{hH?ZHlZ1v(E`7Bv8RJSv(sNqU^j!s0{fQuT+#=Pz&HziX91jV`ra`t z&+o$MsB)Y(Uj4asTiI=Ii%nfvKgZRjcO{@(dv;Ayrx5rR$fxufSDnYPVuo0l(-fu` zAevE5A92Tcpo?ujg!G#uI}%n^|8^-xTqUHPgab2hWY_Y69Ev=^mz_Svm1_$QmLn>C zd{Ce{itdg_Mv2HfGB&DGecgf9OY(5z4R^Hkj_(cVZ<1UR-lkxRWKQT4lxFY>C-jB( zBd2AL9??ENM+oWMOX;r6MO7M@4UV$`-wQzb{I{hK{bByYChSY{Y#pL5Q3Avj+-A}! z{8Wb>-@|bXJ%@L~^al;;D-#_ycl>>L4uAv;sAw-jtyPgWRAEH0bzFqRvLaOT0^2qe zYo7yEz60!o!sc-m63nV-F9P+SUnYLU!wJ=+$FQ>`V^ziw{773A#kIL5_cC>xQXkZ* ztElq_QSLs>w$xGoP4YPRg!4bGbB|I_qttyAO1^qSDm2E+7&i6g; z4wzHMhuPNcC3UrddygJ8Dm;fpo57`JUsO;jqKc>fo01JeOnxQkm_$ER&wp!RHn<`x zbyVTn$|&P+BhuGz`*`_Di@wg%jFlj}ZtR^5htD~cstTOc@k);%ISS3hn~z+VTz?+y z3^YblYEs+wR}(brW8maPdw)hJAfTqY{W`%xJcih5h)1wF?#$>5wv4fwU@qWMg}?EL zl9vbY9Z7_DAL;&|ckx)0Xbdyrh+*eGfc$Ak0u>|J>i3I#tINHcGf9xiEO5*ev=h+e RVI;%VoahFct6w+n`~#f#TIB!$ delta 9 QcmeyjgK^eu#tpN40T}iKg#Z8m