diff --git a/Demo/Pillarbox-demo.xcodeproj/project.pbxproj b/Demo/Pillarbox-demo.xcodeproj/project.pbxproj index ba9525c79..678329974 100644 --- a/Demo/Pillarbox-demo.xcodeproj/project.pbxproj +++ b/Demo/Pillarbox-demo.xcodeproj/project.pbxproj @@ -93,6 +93,9 @@ 6FDB51CB2A4042B2001F430F /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDB51CA2A4042B2001F430F /* Router.swift */; }; 6FE324B329E4657D007501CF /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE324B229E4657D007501CF /* View.swift */; }; 6FED426B2A96F4D3004D7724 /* TransitionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FED426A2A96F4D3004D7724 /* TransitionView.swift */; }; + 6FF7C9832C0084BC00FBDADB /* PlaybackHudFontSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF7C9822C0084BC00FBDADB /* PlaybackHudFontSize.swift */; }; + 6FF7C9852C0084CE00FBDADB /* PlaybackHudColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF7C9842C0084CE00FBDADB /* PlaybackHudColor.swift */; }; + 6FF7C9872C0084DD00FBDADB /* SeekBehaviorSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF7C9862C0084DD00FBDADB /* SeekBehaviorSetting.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -186,6 +189,9 @@ 6FDB51CA2A4042B2001F430F /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; 6FE324B229E4657D007501CF /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; 6FED426A2A96F4D3004D7724 /* TransitionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionView.swift; sourceTree = ""; }; + 6FF7C9822C0084BC00FBDADB /* PlaybackHudFontSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackHudFontSize.swift; sourceTree = ""; }; + 6FF7C9842C0084CE00FBDADB /* PlaybackHudColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackHudColor.swift; sourceTree = ""; }; + 6FF7C9862C0084DD00FBDADB /* SeekBehaviorSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeekBehaviorSetting.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -294,8 +300,11 @@ children = ( 6F59E84B29CF31E10093E6FB /* Media.swift */, 6F59E84C29CF31E10093E6FB /* MediaDescription.swift */, + 6FF7C9842C0084CE00FBDADB /* PlaybackHudColor.swift */, + 6FF7C9822C0084BC00FBDADB /* PlaybackHudFontSize.swift */, 6F59E84A29CF31E10093E6FB /* Playlist.swift */, 6F59E84929CF31E10093E6FB /* RadioChannel.swift */, + 6FF7C9862C0084DD00FBDADB /* SeekBehaviorSetting.swift */, 6F59E84729CF31E10093E6FB /* ServerSetting.swift */, 6F59E84829CF31E10093E6FB /* Template.swift */, 6F59E84629CF31E10093E6FB /* Vendor.swift */, @@ -642,7 +651,9 @@ 0EF2A5492B44500800F01804 /* SettingsBundle.swift in Sources */, 6FAD51142B331AAD0078FE08 /* SingleView.swift in Sources */, 6F7EAA552B17755C00194D03 /* TrackingProgressTutorial~ios.swift in Sources */, + 6FF7C9852C0084CE00FBDADB /* PlaybackHudColor.swift in Sources */, 6F59E88229CF31E10093E6FB /* Media.swift in Sources */, + 6FF7C9872C0084DD00FBDADB /* SeekBehaviorSetting.swift in Sources */, 6F59E87E29CF31E10093E6FB /* ServerSetting.swift in Sources */, 6F59E87929CF31E10093E6FB /* SearchView.swift in Sources */, 6F59E88029CF31E10093E6FB /* RadioChannel.swift in Sources */, @@ -659,6 +670,7 @@ 0E48F3FC2B2DBAD4001982BB /* CustomNavigationLink.swift in Sources */, 6FD407892B189C0600D34BD1 /* TrackingVisibilityTutorial~ios.swift in Sources */, 0ECC5AD52A517A4C0064E701 /* PlaybackSlider~ios.swift in Sources */, + 6FF7C9832C0084BC00FBDADB /* PlaybackHudFontSize.swift in Sources */, 6F12A9522BD2B8A300AD6DDB /* IntegratingWithControlCenter.swift in Sources */, 0EE2A3B02B29F82200BAAD65 /* CustomSection.swift in Sources */, 6F59E89929CF31E20093E6FB /* PlaybackView.swift in Sources */, diff --git a/Demo/Resources/Localizable.xcstrings b/Demo/Resources/Localizable.xcstrings index 7a2ba93ec..e68d711bb 100644 --- a/Demo/Resources/Localizable.xcstrings +++ b/Demo/Resources/Localizable.xcstrings @@ -66,6 +66,9 @@ }, "Automatic" : { + }, + "Blue" : { + }, "Both" : { @@ -78,6 +81,9 @@ }, "Clear URL cache" : { + }, + "Color" : { + }, "Content displayed" : { @@ -99,15 +105,24 @@ }, "Embeddings" : { + }, + "Enabled" : { + }, "Enter URL or URN" : { }, "Examples" : { + }, + "Font size" : { + }, "GitHub" : { + }, + "Green" : { + }, "Immediate" : { @@ -150,6 +165,9 @@ }, "Play" : { + }, + "Playback HUD" : { + }, "Player" : { @@ -162,6 +180,12 @@ }, "Project" : { + }, + "Red" : { + + }, + "Reset" : { + }, "Search" : { @@ -177,6 +201,9 @@ }, "Showcase" : { + }, + "Shows a video overlay displaying various playback-related statistics." : { + }, "Simulate memory warning" : { @@ -213,12 +240,27 @@ }, "Use a proxy tool to observe events." : { + }, + "Value" : { + }, "Version information" : { }, "Web" : { + }, + "White" : { + + }, + "X offset" : { + + }, + "Y offset" : { + + }, + "Yellow" : { + } }, "version" : "1.0" diff --git a/Demo/Sources/Application/AppDelegate.swift b/Demo/Sources/Application/AppDelegate.swift index bb30e0819..95d52317c 100644 --- a/Demo/Sources/Application/AppDelegate.swift +++ b/Demo/Sources/Application/AppDelegate.swift @@ -17,7 +17,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate { // swiftlint:disable:next discouraged_optional_collection func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { try? AVAudioSession.sharedInstance().setCategory(.playback) - UserDefaults.standard.registerDefaults() + UserDefaults.registerDefaults() configureShowTime() configureDataProvider() configureAnalytics() diff --git a/Demo/Sources/ContentLists/ContentListView.swift b/Demo/Sources/ContentLists/ContentListView.swift index 20341e605..5a9eb16ac 100644 --- a/Demo/Sources/ContentLists/ContentListView.swift +++ b/Demo/Sources/ContentLists/ContentListView.swift @@ -14,7 +14,7 @@ private struct LoadedView: View { let contents: [ContentListViewModel.Content] @EnvironmentObject private var router: Router - @AppStorage(UserDefaults.serverSettingKey) + @AppStorage(UserDefaults.DemoSettingKey.serverSetting.rawValue) private var serverSetting: ServerSetting = .ilProduction var body: some View { @@ -65,7 +65,7 @@ private struct LoadedView: View { private struct ContentCell: View { let content: ContentListViewModel.Content - @AppStorage(UserDefaults.serverSettingKey) + @AppStorage(UserDefaults.DemoSettingKey.serverSetting.rawValue) private var serverSetting: ServerSetting = .ilProduction @EnvironmentObject private var router: Router diff --git a/Demo/Sources/ContentLists/ContentListsView.swift b/Demo/Sources/ContentLists/ContentListsView.swift index fbf16e5a8..8d7f175df 100644 --- a/Demo/Sources/ContentLists/ContentListsView.swift +++ b/Demo/Sources/ContentLists/ContentListsView.swift @@ -9,7 +9,7 @@ import SwiftUI // Behavior: h-exp, v-exp struct ContentListsView: View { - @AppStorage(UserDefaults.serverSettingKey) + @AppStorage(UserDefaults.DemoSettingKey.serverSetting.rawValue) private var selectedServerSetting: ServerSetting = .ilProduction var body: some View { diff --git a/Demo/Sources/Model/PlaybackHudColor.swift b/Demo/Sources/Model/PlaybackHudColor.swift new file mode 100644 index 000000000..4de207b2a --- /dev/null +++ b/Demo/Sources/Model/PlaybackHudColor.swift @@ -0,0 +1,15 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Foundation + +enum PlaybackHudColor: Int { + case yellow + case green + case red + case blue + case white +} diff --git a/Demo/Sources/Model/PlaybackHudFontSize.swift b/Demo/Sources/Model/PlaybackHudFontSize.swift new file mode 100644 index 000000000..9dff9c3c7 --- /dev/null +++ b/Demo/Sources/Model/PlaybackHudFontSize.swift @@ -0,0 +1,77 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Foundation +import UIKit + +enum PlaybackHudFontSize: Int, CaseIterable { + case small + case `default` + case large + + private static let smallValue = constant(iOS: 9, tvOS: 30) + private static let defaultValue = constant(iOS: 18, tvOS: 40) + private static let largeValue = constant(iOS: 27, tvOS: 50) + + var name: String { + switch self { + case .small: + return "Small" + case .default: + return "Default" + case .large: + return "Large" + } + } + + var rawValue: Int { + let value = Self.value(from: self) + return Self.scaledValue(fromValue: value) + } + + init?(rawValue: Int) { + let value = Self.value(fromScaledValue: rawValue) + self = Self.size(fromValue: value) + } + + private static func value(from size: Self) -> Int { + switch size { + case .small: + return smallValue + case .default: + return defaultValue + case .large: + return largeValue + } + } + + private static func size(fromValue value: Int) -> Self { + switch value { + case smallValue: + return .small + case largeValue: + return .large + default: + return .default + } + } + + private static func scaledValue(fromValue value: Int) -> Int { +#if targetEnvironment(simulator) + value +#else + value * Int(UIScreen.main.scale) +#endif + } + + private static func value(fromScaledValue value: Int) -> Int { +#if targetEnvironment(simulator) + value +#else + value / Int(UIScreen.main.scale) +#endif + } +} diff --git a/Demo/Sources/Model/SeekBehaviorSetting.swift b/Demo/Sources/Model/SeekBehaviorSetting.swift new file mode 100644 index 000000000..c65c69aaf --- /dev/null +++ b/Demo/Sources/Model/SeekBehaviorSetting.swift @@ -0,0 +1,13 @@ +// +// Copyright (c) SRG SSR. All rights reserved. +// +// License information is available from the LICENSE file. +// + +import Foundation + +@objc +enum SeekBehaviorSetting: Int { + case immediate + case deferred +} diff --git a/Demo/Sources/Players/SimplePlayerView.swift b/Demo/Sources/Players/SimplePlayerView.swift index bacbbe128..df49164b9 100644 --- a/Demo/Sources/Players/SimplePlayerView.swift +++ b/Demo/Sources/Players/SimplePlayerView.swift @@ -16,7 +16,7 @@ struct SimplePlayerView: View { var body: some View { ZStack { - VideoView(player: player) + videoView() progressView() playbackButton() } @@ -30,6 +30,13 @@ struct SimplePlayerView: View { .tracked(name: "simple-player") } + @ViewBuilder + private func videoView() -> some View { + VideoView(player: player) + .background(.black) + .ignoresSafeArea() + } + @ViewBuilder private func progressView() -> some View { ProgressView() diff --git a/Demo/Sources/Settings/SettingsView.swift b/Demo/Sources/Settings/SettingsView.swift index 86cca2536..ab396cc22 100644 --- a/Demo/Sources/Settings/SettingsView.swift +++ b/Demo/Sources/Settings/SettingsView.swift @@ -56,15 +56,30 @@ private struct InfoCell: View { } struct SettingsView: View { - @AppStorage(UserDefaults.presenterModeEnabledKey) + @AppStorage(UserDefaults.DemoSettingKey.presenterModeEnabled.rawValue) private var isPresenterModeEnabled = false - @AppStorage(UserDefaults.smartNavigationEnabledKey) + @AppStorage(UserDefaults.DemoSettingKey.smartNavigationEnabled.rawValue) private var isSmartNavigationEnabled = true - @AppStorage(UserDefaults.seekBehaviorSettingKey) + @AppStorage(UserDefaults.DemoSettingKey.seekBehaviorSetting.rawValue) private var seekBehaviorSetting: SeekBehaviorSetting = .immediate + @AppStorage(UserDefaults.PlaybackHudSettingKey.enabled.rawValue, store: .playbackHud) + private var playbackHudEnabled = false + + @AppStorage(UserDefaults.PlaybackHudSettingKey.fontSize.rawValue, store: .playbackHud) + private var playbackHudFontSize: PlaybackHudFontSize = .default + + @AppStorage(UserDefaults.PlaybackHudSettingKey.color.rawValue, store: .playbackHud) + private var playbackHudColor: PlaybackHudColor = .yellow + + @AppStorage(UserDefaults.PlaybackHudSettingKey.xOffset.rawValue, store: .playbackHud) + private var playbackHudXOffset = UserDefaults.playbackHudDefaultHudXOffset + + @AppStorage(UserDefaults.PlaybackHudSettingKey.yOffset.rawValue, store: .playbackHud) + private var playbackHudYOffset = UserDefaults.playbackHudDefaultHudYOffset + private var version: String { Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String } @@ -83,6 +98,7 @@ struct SettingsView: View { content() .padding(.horizontal, constant(iOS: 0, tvOS: 20)) } + .animation(.defaultLinear, value: playbackHudEnabled) .tracked(name: "settings") #if os(iOS) .navigationTitle("Settings") @@ -109,6 +125,7 @@ struct SettingsView: View { applicationSection() playerSection() debuggingSection() + playbackHudSection() #if os(iOS) gitHubSection() #endif @@ -148,6 +165,9 @@ struct SettingsView: View { Text("Immediate").tag(SeekBehaviorSetting.immediate) Text("Deferred").tag(SeekBehaviorSetting.deferred) } +#if os(tvOS) + .pickerStyle(.navigationLink) +#endif } @ViewBuilder @@ -164,6 +184,74 @@ struct SettingsView: View { } } + @ViewBuilder + private func playbackHudSection() -> some View { + Section { + Toggle("Enabled", isOn: $playbackHudEnabled) + if playbackHudEnabled { + Picker("Font size", selection: $playbackHudFontSize) { + ForEach(PlaybackHudFontSize.allCases, id: \.self) { size in + Text(size.name).tag(size) + } + } +#if os(tvOS) + .pickerStyle(.navigationLink) +#endif + + Picker("Color", selection: $playbackHudColor) { + Text("Yellow").tag(PlaybackHudColor.yellow) + Text("Green").tag(PlaybackHudColor.green) + Text("Red").tag(PlaybackHudColor.red) + Text("Blue").tag(PlaybackHudColor.blue) + Text("White").tag(PlaybackHudColor.white) + } +#if os(tvOS) + .pickerStyle(.navigationLink) +#endif + + numberEditor("X offset", value: $playbackHudXOffset) + numberEditor("Y offset", value: $playbackHudYOffset) + + Button(action: UserDefaults.resetPlaybackHudSettings) { + Text("Reset") + .frame(maxWidth: .infinity, alignment: .leading) + } + .foregroundStyle(.red) + } + } header: { + Text("Playback HUD") + .headerStyle() + } footer: { + Text("Shows a video overlay displaying various playback-related statistics.") + } + } + + @ViewBuilder + private func numberEditor(_ key: LocalizedStringKey, value: Binding) -> some View { +#if os(iOS) + Stepper(value: value) { + HStack { + Text(key) + numberTextField(value: value) + } + } +#else + HStack { + Text(key) + Spacer() + numberTextField(value: value) + } +#endif + } + + @ViewBuilder + private func numberTextField(value: Binding) -> some View { + TextField("Value", value: value, format: .number) + .multilineTextAlignment(.trailing) + .foregroundColor(.secondary) + .keyboardType(.numberPad) + } + @ViewBuilder private func versionSection() -> some View { Section { @@ -192,9 +280,6 @@ struct SettingsView: View { Text(" in Switzerland") } .frame(maxWidth: .infinity) -#if os(tvOS) - .focusable() -#endif } #if os(iOS) diff --git a/Demo/Sources/Settings/UserDefaults.swift b/Demo/Sources/Settings/UserDefaults.swift index d71ed2070..d8cf3aa14 100644 --- a/Demo/Sources/Settings/UserDefaults.swift +++ b/Demo/Sources/Settings/UserDefaults.swift @@ -7,28 +7,24 @@ import Foundation import PillarboxPlayer -@objc -enum SeekBehaviorSetting: Int { - case immediate - case deferred -} - // Extensions allowing the use of KVO to detect user default changes by key. // Keys and dynamic property names must match. -// +// // For more information see https://stackoverflow.com/a/47856467/760435 extension UserDefaults { - static let presenterModeEnabledKey = "presenterModeEnabled" - static let smartNavigationEnabledKey = "smartNavigationEnabled" - static let seekBehaviorSettingKey = "seekBehaviorSetting" - static let serverSettingKey = "serverSetting" + enum DemoSettingKey: String, CaseIterable { + case presenterModeEnabled + case smartNavigationEnabled + case seekBehaviorSetting + case serverSetting + } @objc dynamic var presenterModeEnabled: Bool { - bool(forKey: Self.presenterModeEnabledKey) + bool(forKey: DemoSettingKey.presenterModeEnabled.rawValue) } @objc dynamic var smartNavigationEnabled: Bool { - bool(forKey: Self.smartNavigationEnabledKey) + bool(forKey: DemoSettingKey.smartNavigationEnabled.rawValue) } var seekBehavior: SeekBehavior { @@ -41,19 +37,56 @@ extension UserDefaults { } @objc dynamic var seekBehaviorSetting: SeekBehaviorSetting { - .init(rawValue: integer(forKey: Self.seekBehaviorSettingKey)) ?? .immediate + .init(rawValue: integer(forKey: DemoSettingKey.seekBehaviorSetting.rawValue)) ?? .immediate } @objc dynamic var serverSetting: ServerSetting { - .init(rawValue: integer(forKey: Self.serverSettingKey)) ?? .ilProduction + .init(rawValue: integer(forKey: DemoSettingKey.serverSetting.rawValue)) ?? .ilProduction + } + + private static func registerDefaultDemoSettings() { + UserDefaults.standard.register(defaults: [ + DemoSettingKey.presenterModeEnabled.rawValue: false, + DemoSettingKey.seekBehaviorSetting.rawValue: SeekBehaviorSetting.immediate.rawValue, + DemoSettingKey.smartNavigationEnabled.rawValue: true, + DemoSettingKey.serverSetting.rawValue: ServerSetting.ilProduction.rawValue + ]) + } +} + +extension UserDefaults { + enum PlaybackHudSettingKey: String, CaseIterable { + case enabled = "enable" // Bool + case color // Int, see `PlaybackHudColor`. + case fontSize = "fontsize" // Int >= 8 + case xOffset = "xoffset" // Int >= 1 + case yOffset = "yoffset" // Int >= 1 + } + + static let playbackHud = UserDefaults(suiteName: "com.apple.avfoundation.videoperformancehud") + + static let playbackHudDefaultHudXOffset = 20 + static let playbackHudDefaultHudYOffset = 20 + + static func resetPlaybackHudSettings() { + guard let playbackHud else { return } + PlaybackHudSettingKey.allCases.forEach { key in + playbackHud.removeObject(forKey: key.rawValue) + } } - func registerDefaults() { - register(defaults: [ - Self.presenterModeEnabledKey: false, - Self.seekBehaviorSettingKey: SeekBehaviorSetting.immediate.rawValue, - Self.smartNavigationEnabledKey: true, - Self.serverSettingKey: ServerSetting.ilProduction.rawValue + private static func registerDefaultPlaybackHudSettings() { + playbackHud?.register(defaults: [ + PlaybackHudSettingKey.fontSize.rawValue: PlaybackHudFontSize.default.rawValue, + PlaybackHudSettingKey.xOffset.rawValue: Self.playbackHudDefaultHudXOffset, + PlaybackHudSettingKey.yOffset.rawValue: Self.playbackHudDefaultHudYOffset ]) } } + +extension UserDefaults { + static func registerDefaults() { + registerDefaultDemoSettings() + registerDefaultPlaybackHudSettings() + } +}