diff --git a/Shared/Extensions/VLCPlayer+subtitles.swift b/Shared/Extensions/VLCPlayer+subtitles.swift new file mode 100644 index 00000000..f1e9b6bd --- /dev/null +++ b/Shared/Extensions/VLCPlayer+subtitles.swift @@ -0,0 +1,26 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +#if os(tvOS) +import TVVLCKit +#else +import MobileVLCKit +#endif + +extension VLCMediaPlayer { + /// Applies font size to the player + /// + /// This is pretty hacky until VLCKit 4 has a public API to support this + func setSubtitleSize(_ size: SubtitleSize) { + perform( + Selector(("setTextRendererFontSize:")), + with: size.textRendererFontSize + ) + } +} diff --git a/Shared/Objects/SubtitleSize.swift b/Shared/Objects/SubtitleSize.swift new file mode 100644 index 00000000..7b944efc --- /dev/null +++ b/Shared/Objects/SubtitleSize.swift @@ -0,0 +1,59 @@ +// + /* + * 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 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Defaults + +enum SubtitleSize: Int32, CaseIterable, Defaults.Serializable { + case smallest + case smaller + case regular + case larger + case largest +} + +// MARK: - appearance + +extension SubtitleSize { + var label: String { + switch self { + case .smallest: + return "Smallest" + case .smaller: + return "Smaller" + case .regular: + return "Regular" + case .larger: + return "Larger" + case .largest: + return "Largest" + } + } +} + +// MARK: - sizing for VLC + +extension SubtitleSize { + /// Value to be passed to VLCKit (via hacky internal property, until VLCKit 4) + /// + /// note that it doesn't correspond to actual font sizes; a smaller int creates bigger text + var textRendererFontSize: Int { + switch self { + case .smallest: + return 24 + case .smaller: + return 20 + case .regular: + return 16 + case .larger: + return 12 + case .largest: + return 8 + } + } +} diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index d8ed248b..9ab7725f 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -25,51 +25,47 @@ extension SwiftfinStore { extension Defaults.Keys { - // Universal settings - static let defaultHTTPScheme = Key("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite) - static let appAppearance = Key("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite) + // Universal settings + static let defaultHTTPScheme = Key("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite) + static let appAppearance = Key("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite) - // General settings - static let lastServerUserID = Defaults.Key("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite) - static let inNetworkBandwidth = Key("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) - static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) - static let isAutoSelectSubtitles = Key("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite) - static let autoSelectSubtitlesLangCode = Key("AutoSelectSubtitlesLangCode", default: "Auto", - suite: SwiftfinStore.Defaults.generalSuite) - static let autoSelectAudioLangCode = Key("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) + // General settings + static let lastServerUserID = Defaults.Key("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite) + static let inNetworkBandwidth = Key("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) + static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) + static let isAutoSelectSubtitles = Key("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let autoSelectSubtitlesLangCode = Key("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) + static let autoSelectAudioLangCode = Key("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) - // Customize settings - static let showPosterLabels = Key("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite) - static let showCastAndCrew = Key("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite) + // Customize settings + static let showPosterLabels = Key("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let showCastAndCrew = Key("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite) - // Video player / overlay settings - static let overlayType = Key("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite) - static let jumpGesturesEnabled = Key("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite) - static let videoPlayerJumpForward = Key("videoPlayerJumpForward", default: .fifteen, - suite: SwiftfinStore.Defaults.generalSuite) - static let videoPlayerJumpBackward = Key("videoPlayerJumpBackward", default: .fifteen, - suite: SwiftfinStore.Defaults.generalSuite) - static let autoplayEnabled = Key("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) - static let resumeOffset = Key("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite) + // Video player / overlay settings + static let overlayType = Key("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite) + static let jumpGesturesEnabled = Key("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let videoPlayerJumpForward = Key("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) + static let videoPlayerJumpBackward = Key("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) + static let autoplayEnabled = Key("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let resumeOffset = Key("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let subtitleSize = Key("subtitleSize", default: .regular, suite: SwiftfinStore.Defaults.generalSuite) - // Should show video player items - static let shouldShowPlayPreviousItem = Key("shouldShowPreviousItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) - static let shouldShowPlayNextItem = Key("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) - static let shouldShowAutoPlay = Key("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) + // Should show video player items + static let shouldShowPlayPreviousItem = Key("shouldShowPreviousItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let shouldShowPlayNextItem = Key("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let shouldShowAutoPlay = Key("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) - // Should show video player items in overlay menu - static let shouldShowJumpButtonsInOverlayMenu = Key("shouldShowJumpButtonsInMenu", default: true, - suite: SwiftfinStore.Defaults.generalSuite) + // Should show video player items in overlay menu + static let shouldShowJumpButtonsInOverlayMenu = Key("shouldShowJumpButtonsInMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) - // Experimental settings - enum Experimental { - static let syncSubtitleStateWithAdjacent = Key("experimental.syncSubtitleState", default: false, - suite: SwiftfinStore.Defaults.generalSuite) - static let liveTVAlphaEnabled = Key("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite) - } + // Experimental settings + struct Experimental { + static let syncSubtitleStateWithAdjacent = Key("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let liveTVAlphaEnabled = Key("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite) + } - // tvos specific - static let downActionShowsMenu = Key("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) - static let confirmClose = Key("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite) - static let tvOSCinematicViews = Key("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite) + // tvos specific + static let downActionShowsMenu = Key("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) + static let confirmClose = Key("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let tvOSCinematicViews = Key("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite) } diff --git a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift index a71b97ab..c9e6db47 100644 --- a/Swiftfin tvOS/Views/SettingsView/SettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/SettingsView.swift @@ -13,126 +13,127 @@ import SwiftUI struct SettingsView: View { - @EnvironmentObject - var settingsRouter: SettingsCoordinator.Router - @ObservedObject - var viewModel: SettingsViewModel + @EnvironmentObject var settingsRouter: SettingsCoordinator.Router + @ObservedObject var viewModel: SettingsViewModel - @Default(.autoSelectAudioLangCode) - var autoSelectAudioLangcode - @Default(.videoPlayerJumpForward) - var jumpForwardLength - @Default(.videoPlayerJumpBackward) - var jumpBackwardLength - @Default(.downActionShowsMenu) - var downActionShowsMenu - @Default(.confirmClose) - var confirmClose - @Default(.tvOSCinematicViews) - var tvOSCinematicViews - @Default(.showPosterLabels) - var showPosterLabels - @Default(.resumeOffset) - var resumeOffset + @Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode + @Default(.videoPlayerJumpForward) var jumpForwardLength + @Default(.videoPlayerJumpBackward) var jumpBackwardLength + @Default(.downActionShowsMenu) var downActionShowsMenu + @Default(.confirmClose) var confirmClose + @Default(.tvOSCinematicViews) var tvOSCinematicViews + @Default(.showPosterLabels) var showPosterLabels + @Default(.resumeOffset) var resumeOffset + @Default(.subtitleSize) var subtitleSize - var body: some View { - GeometryReader { reader in - HStack { + var body: some View { + GeometryReader { reader in + HStack { - Image(uiImage: UIImage(named: "App Icon")!) - .cornerRadius(30) - .scaleEffect(2) - .frame(width: reader.size.width / 2) + Image(uiImage: UIImage(named: "App Icon")!) + .cornerRadius(30) + .scaleEffect(2) + .frame(width: reader.size.width / 2) - Form { - Section(header: EmptyView()) { + Form { + Section(header: EmptyView()) { - Button {} label: { - HStack { - Text("User") - Spacer() - Text(viewModel.user.username) - .foregroundColor(.jellyfinPurple) - } - } + Button { - Button { - settingsRouter.route(to: \.serverDetail) - } label: { - HStack { - Text("Server") - .foregroundColor(.primary) - Spacer() - Text(viewModel.server.name) - .foregroundColor(.jellyfinPurple) + } label: { + HStack { + Text("User") + Spacer() + Text(viewModel.user.username) + .foregroundColor(.jellyfinPurple) + } + } - Image(systemName: "chevron.right") - .foregroundColor(.jellyfinPurple) - } - } + Button { + settingsRouter.route(to: \.serverDetail) + } label: { + HStack { + Text("Server") + .foregroundColor(.primary) + Spacer() + Text(viewModel.server.name) + .foregroundColor(.jellyfinPurple) - Button { - SessionManager.main.logout() - } label: { - Text("Switch User") - .foregroundColor(Color.jellyfinPurple) - .font(.callout) - } - } + Image(systemName: "chevron.right") + .foregroundColor(.jellyfinPurple) + } + } - Section(header: Text("Video Player")) { - Picker("Jump Forward Length", selection: $jumpForwardLength) { - ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in - Text(length.label).tag(length.rawValue) - } - } + Button { + SessionManager.main.logout() + } label: { + Text("Switch User") + .foregroundColor(Color.jellyfinPurple) + .font(.callout) + } + } - Picker("Jump Backward Length", selection: $jumpBackwardLength) { - ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in - Text(length.label).tag(length.rawValue) - } - } + Section(header: Text("Video Player")) { + Picker("Jump Forward Length", selection: $jumpForwardLength) { + ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in + Text(length.label).tag(length.rawValue) + } + } - Toggle("Resume 5 Second Offset", isOn: $resumeOffset) + Picker("Jump Backward Length", selection: $jumpBackwardLength) { + ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in + Text(length.label).tag(length.rawValue) + } + } - Toggle("Press Down for Menu", isOn: $downActionShowsMenu) + Toggle("Resume 5 Second Offset", isOn: $resumeOffset) - Toggle("Confirm Close", isOn: $confirmClose) + Toggle("Press Down for Menu", isOn: $downActionShowsMenu) - Button { - settingsRouter.route(to: \.overlaySettings) - } label: { - HStack { - Text("Overlay") - .foregroundColor(.primary) - Spacer() - Image(systemName: "chevron.right") - } - } + Toggle("Confirm Close", isOn: $confirmClose) - Button { - settingsRouter.route(to: \.experimentalSettings) - } label: { - HStack { - Text("Experimental") - .foregroundColor(.primary) - Spacer() - Image(systemName: "chevron.right") - } - } - } + Button { + settingsRouter.route(to: \.overlaySettings) + } label: { + HStack { + Text("Overlay") + .foregroundColor(.primary) + Spacer() + Image(systemName: "chevron.right") + } + } - Section { - Toggle("Cinematic Views", isOn: $tvOSCinematicViews) - Toggle("Show Poster Labels", isOn: $showPosterLabels) + Button { + settingsRouter.route(to: \.experimentalSettings) + } label: { + HStack { + Text("Experimental") + .foregroundColor(.primary) + Spacer() + Image(systemName: "chevron.right") + } + } + } - } header: { - Text("Appearance") - } - } - } - } - } + Section { + Toggle("Cinematic Views", isOn: $tvOSCinematicViews) + } header: { + Text("Appearance") + } + + Section(header: L10n.accessibility.text) { + Toggle("Show Poster Labels", isOn: $showPosterLabels) + + Picker("Subtitle size", selection: $subtitleSize) { + ForEach(SubtitleSize.allCases, id: \.self) { size in + Text(size.label).tag(size.rawValue) + } + } + } + } + } + } + } } struct SettingsView_Previews: PreviewProvider { diff --git a/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift index 29672a2c..88e82a14 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift @@ -382,140 +382,138 @@ class VLCPlayerViewController: UIViewController { extension VLCPlayerViewController { - /// Main function that handles setting up the media player with the current VideoPlayerViewModel - /// and also takes the role of setting the 'viewModel' property with the given viewModel - /// - /// Use case for this is setting new media within the same VLCPlayerViewController - func setupMediaPlayer(newViewModel: VideoPlayerViewModel) { + /// Main function that handles setting up the media player with the current VideoPlayerViewModel + /// and also takes the role of setting the 'viewModel' property with the given viewModel + /// + /// Use case for this is setting new media within the same VLCPlayerViewController + func setupMediaPlayer(newViewModel: VideoPlayerViewModel) { - // remove old player + // remove old player - if vlcMediaPlayer.media != nil { - viewModelListeners.forEach { $0.cancel() } + if vlcMediaPlayer.media != nil { + viewModelListeners.forEach({ $0.cancel() }) - vlcMediaPlayer.stop() - viewModel.sendStopReport() - viewModel.playerOverlayDelegate = nil - } + vlcMediaPlayer.stop() + viewModel.sendStopReport() + viewModel.playerOverlayDelegate = nil + } - vlcMediaPlayer = VLCMediaPlayer() + vlcMediaPlayer = VLCMediaPlayer() - // setup with new player and view model + // setup with new player and view model - vlcMediaPlayer = VLCMediaPlayer() + vlcMediaPlayer = VLCMediaPlayer() - vlcMediaPlayer.delegate = self - vlcMediaPlayer.drawable = videoContentView + vlcMediaPlayer.delegate = self + vlcMediaPlayer.drawable = videoContentView - // TODO: Custom subtitle sizes - vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16) + vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize]) - stopOverlayDismissTimer() + stopOverlayDismissTimer() - // Stop current media if there is one - if vlcMediaPlayer.media != nil { - viewModelListeners.forEach { $0.cancel() } + // Stop current media if there is one + if vlcMediaPlayer.media != nil { + viewModelListeners.forEach({ $0.cancel() }) - vlcMediaPlayer.stop() - viewModel.sendStopReport() - viewModel.playerOverlayDelegate = nil - } + vlcMediaPlayer.stop() + viewModel.sendStopReport() + viewModel.playerOverlayDelegate = nil + } - lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 - lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 + lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 + lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 - // TODO: Custom buffer/cache amounts + // TODO: Custom buffer/cache amounts - let media = VLCMedia(url: newViewModel.streamURL) - media.addOption("--prefetch-buffer-size=1048576") - media.addOption("--network-caching=5000") + let media = VLCMedia(url: newViewModel.streamURL) + media.addOption("--prefetch-buffer-size=1048576") + media.addOption("--network-caching=5000") - vlcMediaPlayer.media = media + vlcMediaPlayer.media = media - setupOverlayHostingController(viewModel: newViewModel) - setupViewModelListeners(viewModel: newViewModel) + setupOverlayHostingController(viewModel: newViewModel) + setupViewModelListeners(viewModel: newViewModel) - newViewModel.getAdjacentEpisodes() - newViewModel.playerOverlayDelegate = self + newViewModel.getAdjacentEpisodes() + newViewModel.playerOverlayDelegate = self - let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0 + let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0 - if startPercentage > 0 { - if viewModel.resumeOffset { - let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000) - var startSeconds = round((startPercentage / 100) * videoDurationSeconds) - startSeconds = startSeconds.subtract(5, floor: 0) - let newStartPercentage = startSeconds / videoDurationSeconds - newViewModel.sliderPercentage = newStartPercentage - } else { - newViewModel.sliderPercentage = startPercentage / 100 - } - } + if startPercentage > 0 { + if viewModel.resumeOffset { + let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000) + var startSeconds = round((startPercentage / 100) * videoDurationSeconds) + startSeconds = startSeconds.subtract(5, floor: 0) + let newStartPercentage = startSeconds / videoDurationSeconds + newViewModel.sliderPercentage = newStartPercentage + } else { + newViewModel.sliderPercentage = startPercentage / 100 + } + } - viewModel = newViewModel - } + viewModel = newViewModel + } - // MARK: startPlayback + // MARK: startPlayback + func startPlayback() { + vlcMediaPlayer.play() - func startPlayback() { - vlcMediaPlayer.play() + // Setup external subtitles + for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) { + if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) { + vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false) + } + } - // Setup external subtitles - for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) { - if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) { - vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false) - } - } + setMediaPlayerTimeAtCurrentSlider() - setMediaPlayerTimeAtCurrentSlider() + viewModel.sendPlayReport() - viewModel.sendPlayReport() + restartOverlayDismissTimer(interval: 5) + } - restartOverlayDismissTimer(interval: 5) - } + // MARK: setupViewModelListeners - // MARK: setupViewModelListeners + private func setupViewModelListeners(viewModel: VideoPlayerViewModel) { + viewModel.$playbackSpeed.sink { newSpeed in + self.vlcMediaPlayer.rate = Float(newSpeed.rawValue) + }.store(in: &viewModelListeners) - private func setupViewModelListeners(viewModel: VideoPlayerViewModel) { - viewModel.$playbackSpeed.sink { newSpeed in - self.vlcMediaPlayer.rate = Float(newSpeed.rawValue) - }.store(in: &viewModelListeners) + viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in + if sliderIsScrubbing { + self.didBeginScrubbing() + } else { + self.didEndScrubbing() + } + }.store(in: &viewModelListeners) - viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in - if sliderIsScrubbing { - self.didBeginScrubbing() - } else { - self.didEndScrubbing() - } - }.store(in: &viewModelListeners) + viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in + self.didSelectAudioStream(index: newAudioStreamIndex) + }.store(in: &viewModelListeners) - viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in - self.didSelectAudioStream(index: newAudioStreamIndex) - }.store(in: &viewModelListeners) + viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in + self.didSelectSubtitleStream(index: newSubtitleStreamIndex) + }.store(in: &viewModelListeners) - viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in - self.didSelectSubtitleStream(index: newSubtitleStreamIndex) - }.store(in: &viewModelListeners) + viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in + self.didToggleSubtitles(newValue: newSubtitlesEnabled) + }.store(in: &viewModelListeners) + } - viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in - self.didToggleSubtitles(newValue: newSubtitlesEnabled) - }.store(in: &viewModelListeners) - } + func setMediaPlayerTimeAtCurrentSlider() { + // Necessary math as VLCMediaPlayer doesn't work well + // by just setting the position + let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000) + let videoDuration = Double(viewModel.item.runTimeTicks! / 10_000_000) + let secondsScrubbedTo = round(viewModel.sliderPercentage * videoDuration) + let newPositionOffset = secondsScrubbedTo - videoPosition - func setMediaPlayerTimeAtCurrentSlider() { - // Necessary math as VLCMediaPlayer doesn't work well - // by just setting the position - let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000) - let videoDuration = Double(viewModel.item.runTimeTicks! / 10_000_000) - let secondsScrubbedTo = round(viewModel.sliderPercentage * videoDuration) - let newPositionOffset = secondsScrubbedTo - videoPosition - - if newPositionOffset > 0 { - vlcMediaPlayer.jumpForward(Int32(newPositionOffset)) - } else { - vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset))) - } - } + if newPositionOffset > 0 { + vlcMediaPlayer.jumpForward(Int32(newPositionOffset)) + } else { + vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset))) + } + } } // MARK: Show/Hide Overlay diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 789ccdab..161b0935 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -138,6 +138,11 @@ 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53EE24E5265060780068F029 /* LibrarySearchView.swift */; }; 53F866442687A45F00DCD1D7 /* PortraitItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */; }; 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */; }; + 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; }; + 5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; }; + 5D1603FE278A40DC00D22B99 /* SubtitleSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */; }; + 5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */; }; + 5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */; }; 5D64683D277B1649009E09AE /* PreferenceUIHostingSwizzling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D64683C277B1649009E09AE /* PreferenceUIHostingSwizzling.swift */; }; 62133890265F83A900A81A2A /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6213388F265F83A900A81A2A /* LibraryListView.swift */; }; 621338932660107500A81A2A /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; @@ -572,6 +577,8 @@ 53EE24E5265060780068F029 /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; 53F866432687A45F00DCD1D7 /* PortraitItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortraitItemView.swift; sourceTree = ""; }; 53FF7F29263CF3F500585C35 /* LatestMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaView.swift; sourceTree = ""; }; + 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubtitleSize.swift; sourceTree = ""; }; + 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VLCPlayer+subtitles.swift"; sourceTree = ""; }; 5D64683C277B1649009E09AE /* PreferenceUIHostingSwizzling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingSwizzling.swift; sourceTree = ""; }; 6213388F265F83A900A81A2A /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; 621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; @@ -977,6 +984,7 @@ E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */, E193D4D727193CAC00900D82 /* PortraitImageStackable.swift */, E10D87DD278510E300BD264C /* PosterSize.swift */, + 5D1603FB278A3D5700D22B99 /* SubtitleSize.swift */, E1D4BF832719D25A00A11E64 /* TrackLanguage.swift */, 535870AC2669D8DD00D05A09 /* Typings.swift */, E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */, @@ -1035,6 +1043,7 @@ 62ECA01926FA6D6900E8EBB7 /* AppURLHandler */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */, 53F866422687A45400DCD1D7 /* Components */, + 5D160401278A41BA00D22B99 /* Extensions */, 5377CC02263B596B003A4E83 /* Info.plist */, E13D02842788B634000FCB04 /* Swiftfin.entitlements */, 5377CBFA263B596B003A4E83 /* Preview Content */, @@ -1215,6 +1224,13 @@ path = Components; sourceTree = ""; }; + 5D160401278A41BA00D22B99 /* Extensions */ = { + isa = PBXGroup; + children = ( + ); + path = Extensions; + sourceTree = ""; + }; 5D64683B277B15E4009E09AE /* PreferenceUIHosting */ = { isa = PBXGroup; children = ( @@ -1238,6 +1254,7 @@ E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */, E1C812C4277A90B200918266 /* URLComponentsExtensions.swift */, 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */, + 5D160402278A41FD00D22B99 /* VLCPlayer+subtitles.swift */, 6220D0AC26D5EABB00B8E046 /* ViewExtensions.swift */, ); path = Extensions; @@ -1665,6 +1682,7 @@ buildPhases = ( 3D0F2756C71CDF6B9EEBD4E0 /* [CP] Check Pods Manifest.lock */, 6286F0A3271C0ABA00C40ED5 /* R.swift */, + C6EE6AB295A273FF14E6EF56 /* [CP] Prepare Artifacts */, 5358705C2669D21600D05A09 /* Sources */, 5358705D2669D21600D05A09 /* Frameworks */, 5358705E2669D21600D05A09 /* Resources */, @@ -2023,6 +2041,23 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Swiftfin iOS/Pods-Swiftfin iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + C6EE6AB295A273FF14E6EF56 /* [CP] Prepare Artifacts */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Swiftfin tvOS/Pods-Swiftfin tvOS-artifacts-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Prepare Artifacts"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Swiftfin tvOS/Pods-Swiftfin tvOS-artifacts-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Swiftfin tvOS/Pods-Swiftfin tvOS-artifacts.sh\"\n"; + showEnvVarsInLog = 0; + }; D4D3981ADF75BCD341D590C0 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -2086,6 +2121,7 @@ E13DD3F027178F87009D4DAF /* SwiftfinNotificationCenter.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */, + 5D32EA12278C95E30020E292 /* VLCPlayer+subtitles.swift in Sources */, E1D4BF8B2719D3D000A11E64 /* BasicAppSettingsCoordinator.swift in Sources */, E1E5D53B2783A80900692DFE /* CinematicItemViewTopRow.swift in Sources */, E13DD3FA2717E961009D4DAF /* UserListViewModel.swift in Sources */, @@ -2163,6 +2199,7 @@ E1AD104E26D96CE3003E4A08 /* BaseItemDtoExtensions.swift in Sources */, 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */, 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, + 5D1603FD278A40DB00D22B99 /* SubtitleSize.swift in Sources */, E1AA33232782648000F6439C /* OverlaySliderColor.swift in Sources */, E103A6A7278AB6D700820EC7 /* CinematicResumeCardView.swift in Sources */, 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */, @@ -2272,6 +2309,7 @@ E13DD3EC27178A54009D4DAF /* UserSignInViewModel.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, C4BE07852728446F003F4AD1 /* LiveTVChannelsViewModel.swift in Sources */, + 5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, C4BE078B272844AF003F4AD1 /* LiveTVChannelsView.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, @@ -2312,6 +2350,7 @@ 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, E13DD3F5271793BB009D4DAF /* UserSignInView.swift in Sources */, + 5D1603FC278A3D5800D22B99 /* SubtitleSize.swift in Sources */, E1F0204E26CCCA74001C1C3B /* VideoPlayerJumpLength.swift in Sources */, 53649AB1269CFB1900A2D8B7 /* LogManager.swift in Sources */, E13DD3E127176BD3009D4DAF /* ServerListViewModel.swift in Sources */, @@ -2426,6 +2465,7 @@ 62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */, 62EC353226766849000E9F2D /* SessionManager.swift in Sources */, 536D3D79267BD5D00004248C /* ViewModel.swift in Sources */, + 5D1603FE278A40DC00D22B99 /* SubtitleSize.swift in Sources */, E1AA332427829B5200F6439C /* OverlayType.swift in Sources */, E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */, ); diff --git a/Swiftfin/Views/SettingsView/SettingsView.swift b/Swiftfin/Views/SettingsView/SettingsView.swift index aacf6bbb..7606bc32 100644 --- a/Swiftfin/Views/SettingsView/SettingsView.swift +++ b/Swiftfin/Views/SettingsView/SettingsView.swift @@ -13,148 +13,139 @@ import SwiftUI struct SettingsView: View { - @EnvironmentObject - var settingsRouter: SettingsCoordinator.Router - @ObservedObject - var viewModel: SettingsViewModel + @EnvironmentObject var settingsRouter: SettingsCoordinator.Router + @ObservedObject var viewModel: SettingsViewModel - @Default(.inNetworkBandwidth) - var inNetworkStreamBitrate - @Default(.outOfNetworkBandwidth) - var outOfNetworkStreamBitrate - @Default(.isAutoSelectSubtitles) - var isAutoSelectSubtitles - @Default(.autoSelectSubtitlesLangCode) - var autoSelectSubtitlesLangcode - @Default(.autoSelectAudioLangCode) - var autoSelectAudioLangcode - @Default(.appAppearance) - var appAppearance - @Default(.overlayType) - var overlayType - @Default(.videoPlayerJumpForward) - var jumpForwardLength - @Default(.videoPlayerJumpBackward) - var jumpBackwardLength - @Default(.jumpGesturesEnabled) - var jumpGesturesEnabled - @Default(.showPosterLabels) - var showPosterLabels - @Default(.showCastAndCrew) - var showCastAndCrew - @Default(.resumeOffset) - var resumeOffset + @Default(.inNetworkBandwidth) var inNetworkStreamBitrate + @Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate + @Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles + @Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode + @Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode + @Default(.appAppearance) var appAppearance + @Default(.overlayType) var overlayType + @Default(.videoPlayerJumpForward) var jumpForwardLength + @Default(.videoPlayerJumpBackward) var jumpBackwardLength + @Default(.jumpGesturesEnabled) var jumpGesturesEnabled + @Default(.showPosterLabels) var showPosterLabels + @Default(.showCastAndCrew) var showCastAndCrew + @Default(.resumeOffset) var resumeOffset + @Default(.subtitleSize) var subtitleSize - var body: some View { - Form { - Section(header: EmptyView()) { - HStack { - Text("User") - Spacer() - Text(viewModel.user.username) - .foregroundColor(.jellyfinPurple) - } + var body: some View { + Form { + Section(header: EmptyView()) { + HStack { + Text("User") + Spacer() + Text(viewModel.user.username) + .foregroundColor(.jellyfinPurple) + } - Button { - settingsRouter.route(to: \.serverDetail) - } label: { - HStack { - Text("Server") - .foregroundColor(.primary) - Spacer() - Text(viewModel.server.name) - .foregroundColor(.jellyfinPurple) + Button { + settingsRouter.route(to: \.serverDetail) + } label: { + HStack { + Text("Server") + .foregroundColor(.primary) + Spacer() + Text(viewModel.server.name) + .foregroundColor(.jellyfinPurple) - Image(systemName: "chevron.right") - } - } + Image(systemName: "chevron.right") + } + } - Button { - settingsRouter.dismissCoordinator { - SessionManager.main.logout() - } - } label: { - Text("Switch User") - .font(.callout) - } - } + Button { + settingsRouter.dismissCoordinator { + SessionManager.main.logout() + } + } label: { + Text("Switch User") + .font(.callout) + } + } - // TODO: Implement these for playback - // Section(header: Text("Networking")) { - // Picker("Default local quality", selection: $inNetworkStreamBitrate) { - // ForEach(self.viewModel.bitrates, id: \.self) { bitrate in - // Text(bitrate.name).tag(bitrate.value) - // } - // } + // TODO: Implement these for playback +// Section(header: Text("Networking")) { +// Picker("Default local quality", selection: $inNetworkStreamBitrate) { +// ForEach(self.viewModel.bitrates, id: \.self) { bitrate in +// Text(bitrate.name).tag(bitrate.value) +// } +// } // - // Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { - // ForEach(self.viewModel.bitrates, id: \.self) { bitrate in - // Text(bitrate.name).tag(bitrate.value) - // } - // } - // } +// Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { +// ForEach(self.viewModel.bitrates, id: \.self) { bitrate in +// Text(bitrate.name).tag(bitrate.value) +// } +// } +// } - Section(header: Text("Video Player")) { - Picker("Jump Forward Length", selection: $jumpForwardLength) { - ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in - Text(length.label).tag(length.rawValue) - } - } + Section(header: Text("Video Player")) { + Picker("Jump Forward Length", selection: $jumpForwardLength) { + ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in + Text(length.label).tag(length.rawValue) + } + } - Picker("Jump Backward Length", selection: $jumpBackwardLength) { - ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in - Text(length.label).tag(length.rawValue) - } - } + Picker("Jump Backward Length", selection: $jumpBackwardLength) { + ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in + Text(length.label).tag(length.rawValue) + } + } - Toggle("Jump Gestures Enabled", isOn: $jumpGesturesEnabled) + Toggle("Jump Gestures Enabled", isOn: $jumpGesturesEnabled) - Toggle("Resume 5 Second Offset", isOn: $resumeOffset) + Toggle("Resume 5 Second Offset", isOn: $resumeOffset) - Button { - settingsRouter.route(to: \.overlaySettings) - } label: { - HStack { - Text("Overlay") - .foregroundColor(.primary) - Spacer() - Text(overlayType.label) - Image(systemName: "chevron.right") - } - } + Button { + settingsRouter.route(to: \.overlaySettings) + } label: { + HStack { + Text("Overlay") + .foregroundColor(.primary) + Spacer() + Text(overlayType.label) + Image(systemName: "chevron.right") + } + } - Button { - settingsRouter.route(to: \.experimentalSettings) - } label: { - HStack { - Text("Experimental") - .foregroundColor(.primary) - Spacer() - Image(systemName: "chevron.right") - } - } - } + Button { + settingsRouter.route(to: \.experimentalSettings) + } label: { + HStack { + Text("Experimental") + .foregroundColor(.primary) + Spacer() + Image(systemName: "chevron.right") + } + } + } - Section(header: L10n.accessibility.text) { - Toggle("Show Poster Labels", isOn: $showPosterLabels) - Toggle("Show Cast and Crew", isOn: $showCastAndCrew) + Section(header: L10n.accessibility.text) { + Toggle("Show Poster Labels", isOn: $showPosterLabels) + Toggle("Show Cast and Crew", isOn: $showCastAndCrew) - Picker(L10n.appearance, selection: $appAppearance) { - ForEach(AppAppearance.allCases, id: \.self) { appearance in - Text(appearance.localizedName).tag(appearance.rawValue) - } - } - } - } - .navigationBarTitle("Settings", displayMode: .inline) - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - settingsRouter.dismissCoordinator() - } label: { - Image(systemName: "xmark.circle.fill") - } - } - } - } + Picker(L10n.appearance, selection: $appAppearance) { + ForEach(AppAppearance.allCases, id: \.self) { appearance in + Text(appearance.localizedName).tag(appearance.rawValue) + } + } + Picker("Subtitle size", selection: $subtitleSize) { + ForEach(SubtitleSize.allCases, id: \.self) { size in + Text(size.label).tag(size.rawValue) + } + } + } + } + .navigationBarTitle("Settings", displayMode: .inline) + .toolbar { + ToolbarItemGroup(placement: .navigationBarLeading) { + Button { + settingsRouter.dismissCoordinator() + } label: { + Image(systemName: "xmark.circle.fill") + } + } + } + } } diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift index 50a6d022..554a3807 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift @@ -284,138 +284,136 @@ class VLCPlayerViewController: UIViewController { extension VLCPlayerViewController { - /// Main function that handles setting up the media player with the current VideoPlayerViewModel - /// and also takes the role of setting the 'viewModel' property with the given viewModel - /// - /// Use case for this is setting new media within the same VLCPlayerViewController - func setupMediaPlayer(newViewModel: VideoPlayerViewModel) { + /// Main function that handles setting up the media player with the current VideoPlayerViewModel + /// and also takes the role of setting the 'viewModel' property with the given viewModel + /// + /// Use case for this is setting new media within the same VLCPlayerViewController + func setupMediaPlayer(newViewModel: VideoPlayerViewModel) { - // remove old player + // remove old player - if vlcMediaPlayer.media != nil { - viewModelListeners.forEach { $0.cancel() } + if vlcMediaPlayer.media != nil { + viewModelListeners.forEach({ $0.cancel() }) - vlcMediaPlayer.stop() - viewModel.sendStopReport() - viewModel.playerOverlayDelegate = nil - } + vlcMediaPlayer.stop() + viewModel.sendStopReport() + viewModel.playerOverlayDelegate = nil + } - vlcMediaPlayer = VLCMediaPlayer() + vlcMediaPlayer = VLCMediaPlayer() - // setup with new player and view model + // setup with new player and view model - vlcMediaPlayer = VLCMediaPlayer() + vlcMediaPlayer = VLCMediaPlayer() - vlcMediaPlayer.delegate = self - vlcMediaPlayer.drawable = videoContentView + vlcMediaPlayer.delegate = self + vlcMediaPlayer.drawable = videoContentView - // TODO: Custom subtitle sizes - vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14) + vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize]) - stopOverlayDismissTimer() + stopOverlayDismissTimer() - lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 - lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 + lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 + lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 - let media = VLCMedia(url: newViewModel.streamURL) - media.addOption("--prefetch-buffer-size=1048576") - media.addOption("--network-caching=5000") + let media = VLCMedia(url: newViewModel.streamURL) + media.addOption("--prefetch-buffer-size=1048576") + media.addOption("--network-caching=5000") - vlcMediaPlayer.media = media + vlcMediaPlayer.media = media - setupOverlayHostingController(viewModel: newViewModel) - setupViewModelListeners(viewModel: newViewModel) + setupOverlayHostingController(viewModel: newViewModel) + setupViewModelListeners(viewModel: newViewModel) - newViewModel.getAdjacentEpisodes() - newViewModel.playerOverlayDelegate = self + newViewModel.getAdjacentEpisodes() + newViewModel.playerOverlayDelegate = self - let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0 + let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0 - if startPercentage > 0 { - if viewModel.resumeOffset { - let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000) - var startSeconds = round((startPercentage / 100) * videoDurationSeconds) - startSeconds = startSeconds.subtract(5, floor: 0) - let newStartPercentage = startSeconds / videoDurationSeconds - newViewModel.sliderPercentage = newStartPercentage - } else { - newViewModel.sliderPercentage = startPercentage / 100 - } - } + if startPercentage > 0 { + if viewModel.resumeOffset { + let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000) + var startSeconds = round((startPercentage / 100) * videoDurationSeconds) + startSeconds = startSeconds.subtract(5, floor: 0) + let newStartPercentage = startSeconds / videoDurationSeconds + newViewModel.sliderPercentage = newStartPercentage + } else { + newViewModel.sliderPercentage = startPercentage / 100 + } + } - viewModel = newViewModel - } + viewModel = newViewModel + } - // MARK: startPlayback + // MARK: startPlayback + func startPlayback() { + vlcMediaPlayer.play() - func startPlayback() { - vlcMediaPlayer.play() + // Setup external subtitles + for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) { + if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) { + vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false) + } + } - // Setup external subtitles - for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) { - if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) { - vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false) - } - } + setMediaPlayerTimeAtCurrentSlider() - setMediaPlayerTimeAtCurrentSlider() + viewModel.sendPlayReport() - viewModel.sendPlayReport() + restartOverlayDismissTimer() + } - restartOverlayDismissTimer() - } + // MARK: setupViewModelListeners - // MARK: setupViewModelListeners + private func setupViewModelListeners(viewModel: VideoPlayerViewModel) { - private func setupViewModelListeners(viewModel: VideoPlayerViewModel) { + viewModel.$playbackSpeed.sink { newSpeed in + self.vlcMediaPlayer.rate = Float(newSpeed.rawValue) + }.store(in: &viewModelListeners) - viewModel.$playbackSpeed.sink { newSpeed in - self.vlcMediaPlayer.rate = Float(newSpeed.rawValue) - }.store(in: &viewModelListeners) + viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in + if sliderIsScrubbing { + self.didBeginScrubbing() + } else { + self.didEndScrubbing() + } + }.store(in: &viewModelListeners) - viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in - if sliderIsScrubbing { - self.didBeginScrubbing() - } else { - self.didEndScrubbing() - } - }.store(in: &viewModelListeners) + viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in + self.didSelectAudioStream(index: newAudioStreamIndex) + }.store(in: &viewModelListeners) - viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in - self.didSelectAudioStream(index: newAudioStreamIndex) - }.store(in: &viewModelListeners) + viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in + self.didSelectSubtitleStream(index: newSubtitleStreamIndex) + }.store(in: &viewModelListeners) - viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in - self.didSelectSubtitleStream(index: newSubtitleStreamIndex) - }.store(in: &viewModelListeners) + viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in + self.didToggleSubtitles(newValue: newSubtitlesEnabled) + }.store(in: &viewModelListeners) - viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in - self.didToggleSubtitles(newValue: newSubtitlesEnabled) - }.store(in: &viewModelListeners) + viewModel.$jumpBackwardLength.sink { newJumpBackwardLength in + self.refreshJumpBackwardOverlayView(with: newJumpBackwardLength) + }.store(in: &viewModelListeners) - viewModel.$jumpBackwardLength.sink { newJumpBackwardLength in - self.refreshJumpBackwardOverlayView(with: newJumpBackwardLength) - }.store(in: &viewModelListeners) + viewModel.$jumpForwardLength.sink { newJumpForwardLength in + self.refreshJumpForwardOverlayView(with: newJumpForwardLength) + }.store(in: &viewModelListeners) + } - viewModel.$jumpForwardLength.sink { newJumpForwardLength in - self.refreshJumpForwardOverlayView(with: newJumpForwardLength) - }.store(in: &viewModelListeners) - } + func setMediaPlayerTimeAtCurrentSlider() { + // Necessary math as VLCMediaPlayer doesn't work well + // by just setting the position + let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000) + let videoDuration = Double(viewModel.item.runTimeTicks! / 10_000_000) + let secondsScrubbedTo = round(viewModel.sliderPercentage * videoDuration) + let newPositionOffset = secondsScrubbedTo - videoPosition - func setMediaPlayerTimeAtCurrentSlider() { - // Necessary math as VLCMediaPlayer doesn't work well - // by just setting the position - let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000) - let videoDuration = Double(viewModel.item.runTimeTicks! / 10_000_000) - let secondsScrubbedTo = round(viewModel.sliderPercentage * videoDuration) - let newPositionOffset = secondsScrubbedTo - videoPosition - - if newPositionOffset > 0 { - vlcMediaPlayer.jumpForward(Int32(newPositionOffset)) - } else { - vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset))) - } - } + if newPositionOffset > 0 { + vlcMediaPlayer.jumpForward(Int32(newPositionOffset)) + } else { + vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset))) + } + } } // MARK: Show/Hide Overlay