format merge

This commit is contained in:
Ethan Pippin 2022-01-10 13:34:03 -07:00
parent 071d07d5ff
commit e12da2cf07
8 changed files with 544 additions and 524 deletions

View File

@ -1,26 +1,23 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
#if os(tvOS) #if os(tvOS)
import TVVLCKit import TVVLCKit
#else #else
import MobileVLCKit import MobileVLCKit
#endif #endif
extension VLCMediaPlayer { extension VLCMediaPlayer {
/// Applies font size to the player /// Applies font size to the player
/// ///
/// This is pretty hacky until VLCKit 4 has a public API to support this /// This is pretty hacky until VLCKit 4 has a public API to support this
func setSubtitleSize(_ size: SubtitleSize) { func setSubtitleSize(_ size: SubtitleSize) {
perform( perform(Selector(("setTextRendererFontSize:")),
Selector(("setTextRendererFontSize:")), with: size.textRendererFontSize)
with: size.textRendererFontSize }
)
}
} }

View File

@ -1,59 +1,58 @@
// //
/* // Swiftfin is subject to the terms of the Mozilla Public
* SwiftFin is subject to the terms of the Mozilla Public // License, v2.0. If a copy of the MPL was not distributed with this
* 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/.
* file, you can obtain one at https://mozilla.org/MPL/2.0/. //
* // Copyright (c) 2022 Jellyfin & Jellyfin Contributors
* Copyright 2021 Aiden Vigue & Jellyfin Contributors //
*/
import Defaults import Defaults
enum SubtitleSize: Int32, CaseIterable, Defaults.Serializable { enum SubtitleSize: Int32, CaseIterable, Defaults.Serializable {
case smallest case smallest
case smaller case smaller
case regular case regular
case larger case larger
case largest case largest
} }
// MARK: - appearance // MARK: - appearance
extension SubtitleSize { extension SubtitleSize {
var label: String { var label: String {
switch self { switch self {
case .smallest: case .smallest:
return "Smallest" return "Smallest"
case .smaller: case .smaller:
return "Smaller" return "Smaller"
case .regular: case .regular:
return "Regular" return "Regular"
case .larger: case .larger:
return "Larger" return "Larger"
case .largest: case .largest:
return "Largest" return "Largest"
} }
} }
} }
// MARK: - sizing for VLC // MARK: - sizing for VLC
extension SubtitleSize { extension SubtitleSize {
/// Value to be passed to VLCKit (via hacky internal property, until VLCKit 4) /// 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 /// note that it doesn't correspond to actual font sizes; a smaller int creates bigger text
var textRendererFontSize: Int { var textRendererFontSize: Int {
switch self { switch self {
case .smallest: case .smallest:
return 24 return 24
case .smaller: case .smaller:
return 20 return 20
case .regular: case .regular:
return 16 return 16
case .larger: case .larger:
return 12 return 12
case .largest: case .largest:
return 8 return 8
} }
} }
} }

View File

@ -25,47 +25,52 @@ extension SwiftfinStore {
extension Defaults.Keys { extension Defaults.Keys {
// Universal settings // Universal settings
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite) static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite) static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
// General settings // General settings
static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite) static let lastServerUserID = Defaults.Key<String?>("lastServerUserID", suite: SwiftfinStore.Defaults.generalSuite)
static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) static let inNetworkBandwidth = Key<Int>("InNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite) static let outOfNetworkBandwidth = Key<Int>("OutOfNetworkBandwidth", default: 40_000_000, suite: SwiftfinStore.Defaults.generalSuite)
static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let isAutoSelectSubtitles = Key<Bool>("isAutoSelectSubtitles", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) static let autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto",
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
// Customize settings // Customize settings
static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let showCastAndCrew = Key<Bool>("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let showCastAndCrew = Key<Bool>("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Video player / overlay settings // Video player / overlay settings
static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite) static let overlayType = Key<OverlayType>("overlayType", default: .normal, suite: SwiftfinStore.Defaults.generalSuite)
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let subtitleSize = Key<SubtitleSize>("subtitleSize", default: .regular, suite: SwiftfinStore.Defaults.generalSuite) static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let subtitleSize = Key<SubtitleSize>("subtitleSize", default: .regular, suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items // Should show video player items
static let shouldShowPlayPreviousItem = Key<Bool>("shouldShowPreviousItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let shouldShowPlayPreviousItem = Key<Bool>("shouldShowPreviousItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let shouldShowPlayNextItem = Key<Bool>("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let shouldShowPlayNextItem = Key<Bool>("shouldShowNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items in overlay menu // Should show video player items in overlay menu
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true,
suite: SwiftfinStore.Defaults.generalSuite)
// Experimental settings // Experimental settings
struct Experimental { enum Experimental {
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false,
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
} static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
}
// tvos specific // tvos specific
static let downActionShowsMenu = Key<Bool>("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let downActionShowsMenu = Key<Bool>("downActionShowsMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let confirmClose = Key<Bool>("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let confirmClose = Key<Bool>("confirmClose", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let tvOSCinematicViews = Key<Bool>("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let tvOSCinematicViews = Key<Bool>("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite)
} }

View File

@ -13,127 +13,136 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router @EnvironmentObject
@ObservedObject var viewModel: SettingsViewModel var settingsRouter: SettingsCoordinator.Router
@ObservedObject
var viewModel: SettingsViewModel
@Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode @Default(.autoSelectAudioLangCode)
@Default(.videoPlayerJumpForward) var jumpForwardLength var autoSelectAudioLangcode
@Default(.videoPlayerJumpBackward) var jumpBackwardLength @Default(.videoPlayerJumpForward)
@Default(.downActionShowsMenu) var downActionShowsMenu var jumpForwardLength
@Default(.confirmClose) var confirmClose @Default(.videoPlayerJumpBackward)
@Default(.tvOSCinematicViews) var tvOSCinematicViews var jumpBackwardLength
@Default(.showPosterLabels) var showPosterLabels @Default(.downActionShowsMenu)
@Default(.resumeOffset) var resumeOffset var downActionShowsMenu
@Default(.subtitleSize) var subtitleSize @Default(.confirmClose)
var confirmClose
@Default(.tvOSCinematicViews)
var tvOSCinematicViews
@Default(.showPosterLabels)
var showPosterLabels
@Default(.resumeOffset)
var resumeOffset
@Default(.subtitleSize)
var subtitleSize
var body: some View { var body: some View {
GeometryReader { reader in GeometryReader { reader in
HStack { HStack {
Image(uiImage: UIImage(named: "App Icon")!) Image(uiImage: UIImage(named: "App Icon")!)
.cornerRadius(30) .cornerRadius(30)
.scaleEffect(2) .scaleEffect(2)
.frame(width: reader.size.width / 2) .frame(width: reader.size.width / 2)
Form { Form {
Section(header: EmptyView()) { Section(header: EmptyView()) {
Button { Button {} label: {
HStack {
Text("User")
Spacer()
Text(viewModel.user.username)
.foregroundColor(.jellyfinPurple)
}
}
} label: { Button {
HStack { settingsRouter.route(to: \.serverDetail)
Text("User") } label: {
Spacer() HStack {
Text(viewModel.user.username) Text("Server")
.foregroundColor(.jellyfinPurple) .foregroundColor(.primary)
} Spacer()
} Text(viewModel.server.name)
.foregroundColor(.jellyfinPurple)
Button { Image(systemName: "chevron.right")
settingsRouter.route(to: \.serverDetail) .foregroundColor(.jellyfinPurple)
} label: { }
HStack { }
Text("Server")
.foregroundColor(.primary)
Spacer()
Text(viewModel.server.name)
.foregroundColor(.jellyfinPurple)
Image(systemName: "chevron.right") Button {
.foregroundColor(.jellyfinPurple) SessionManager.main.logout()
} } label: {
} Text("Switch User")
.foregroundColor(Color.jellyfinPurple)
.font(.callout)
}
}
Button { Section(header: Text("Video Player")) {
SessionManager.main.logout() Picker("Jump Forward Length", selection: $jumpForwardLength) {
} label: { ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
Text("Switch User") Text(length.label).tag(length.rawValue)
.foregroundColor(Color.jellyfinPurple) }
.font(.callout) }
}
}
Section(header: Text("Video Player")) { Picker("Jump Backward Length", selection: $jumpBackwardLength) {
Picker("Jump Forward Length", selection: $jumpForwardLength) { ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in Text(length.label).tag(length.rawValue)
Text(length.label).tag(length.rawValue) }
} }
}
Picker("Jump Backward Length", selection: $jumpBackwardLength) { Toggle("Resume 5 Second Offset", isOn: $resumeOffset)
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
Text(length.label).tag(length.rawValue)
}
}
Toggle("Resume 5 Second Offset", isOn: $resumeOffset) Toggle("Press Down for Menu", isOn: $downActionShowsMenu)
Toggle("Press Down for Menu", isOn: $downActionShowsMenu) Toggle("Confirm Close", isOn: $confirmClose)
Toggle("Confirm Close", isOn: $confirmClose) Button {
settingsRouter.route(to: \.overlaySettings)
} label: {
HStack {
Text("Overlay")
.foregroundColor(.primary)
Spacer()
Image(systemName: "chevron.right")
}
}
Button { Button {
settingsRouter.route(to: \.overlaySettings) settingsRouter.route(to: \.experimentalSettings)
} label: { } label: {
HStack { HStack {
Text("Overlay") Text("Experimental")
.foregroundColor(.primary) .foregroundColor(.primary)
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
} }
} }
}
Button { Section {
settingsRouter.route(to: \.experimentalSettings) Toggle("Cinematic Views", isOn: $tvOSCinematicViews)
} label: { } header: {
HStack { Text("Appearance")
Text("Experimental") }
.foregroundColor(.primary)
Spacer()
Image(systemName: "chevron.right")
}
}
}
Section { Section(header: L10n.accessibility.text) {
Toggle("Cinematic Views", isOn: $tvOSCinematicViews) Toggle("Show Poster Labels", isOn: $showPosterLabels)
} header: {
Text("Appearance")
}
Section(header: L10n.accessibility.text) { Picker("Subtitle size", selection: $subtitleSize) {
Toggle("Show Poster Labels", isOn: $showPosterLabels) ForEach(SubtitleSize.allCases, id: \.self) { size in
Text(size.label).tag(size.rawValue)
Picker("Subtitle size", selection: $subtitleSize) { }
ForEach(SubtitleSize.allCases, id: \.self) { size in }
Text(size.label).tag(size.rawValue) }
} }
} }
} }
} }
}
}
}
} }
struct SettingsView_Previews: PreviewProvider { struct SettingsView_Previews: PreviewProvider {

View File

@ -382,138 +382,139 @@ class VLCPlayerViewController: UIViewController {
extension VLCPlayerViewController { extension VLCPlayerViewController {
/// Main function that handles setting up the media player with the current 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 /// 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 /// Use case for this is setting new media within the same VLCPlayerViewController
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) { func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
// remove old player // remove old player
if vlcMediaPlayer.media != nil { if vlcMediaPlayer.media != nil {
viewModelListeners.forEach({ $0.cancel() }) viewModelListeners.forEach { $0.cancel() }
vlcMediaPlayer.stop() vlcMediaPlayer.stop()
viewModel.sendStopReport() viewModel.sendStopReport()
viewModel.playerOverlayDelegate = nil 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.delegate = self
vlcMediaPlayer.drawable = videoContentView vlcMediaPlayer.drawable = videoContentView
vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize]) vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize])
stopOverlayDismissTimer() stopOverlayDismissTimer()
// Stop current media if there is one // Stop current media if there is one
if vlcMediaPlayer.media != nil { if vlcMediaPlayer.media != nil {
viewModelListeners.forEach({ $0.cancel() }) viewModelListeners.forEach { $0.cancel() }
vlcMediaPlayer.stop() vlcMediaPlayer.stop()
viewModel.sendStopReport() viewModel.sendStopReport()
viewModel.playerOverlayDelegate = nil viewModel.playerOverlayDelegate = nil
} }
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
lastProgressReportTicks = 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) let media = VLCMedia(url: newViewModel.streamURL)
media.addOption("--prefetch-buffer-size=1048576") media.addOption("--prefetch-buffer-size=1048576")
media.addOption("--network-caching=5000") media.addOption("--network-caching=5000")
vlcMediaPlayer.media = media vlcMediaPlayer.media = media
setupOverlayHostingController(viewModel: newViewModel) setupOverlayHostingController(viewModel: newViewModel)
setupViewModelListeners(viewModel: newViewModel) setupViewModelListeners(viewModel: newViewModel)
newViewModel.getAdjacentEpisodes() newViewModel.getAdjacentEpisodes()
newViewModel.playerOverlayDelegate = self newViewModel.playerOverlayDelegate = self
let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0 let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0
if startPercentage > 0 { if startPercentage > 0 {
if viewModel.resumeOffset { if viewModel.resumeOffset {
let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000) let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000)
var startSeconds = round((startPercentage / 100) * videoDurationSeconds) var startSeconds = round((startPercentage / 100) * videoDurationSeconds)
startSeconds = startSeconds.subtract(5, floor: 0) startSeconds = startSeconds.subtract(5, floor: 0)
let newStartPercentage = startSeconds / videoDurationSeconds let newStartPercentage = startSeconds / videoDurationSeconds
newViewModel.sliderPercentage = newStartPercentage newViewModel.sliderPercentage = newStartPercentage
} else { } else {
newViewModel.sliderPercentage = startPercentage / 100 newViewModel.sliderPercentage = startPercentage / 100
} }
} }
viewModel = newViewModel viewModel = newViewModel
} }
// MARK: startPlayback // MARK: startPlayback
func startPlayback() {
vlcMediaPlayer.play()
// Setup external subtitles func startPlayback() {
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) { vlcMediaPlayer.play()
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
}
}
setMediaPlayerTimeAtCurrentSlider() // 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)
}
}
viewModel.sendPlayReport() setMediaPlayerTimeAtCurrentSlider()
restartOverlayDismissTimer(interval: 5) viewModel.sendPlayReport()
}
// MARK: setupViewModelListeners restartOverlayDismissTimer(interval: 5)
}
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) { // MARK: setupViewModelListeners
viewModel.$playbackSpeed.sink { newSpeed in
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
}.store(in: &viewModelListeners)
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
if sliderIsScrubbing { viewModel.$playbackSpeed.sink { newSpeed in
self.didBeginScrubbing() self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
} else { }.store(in: &viewModelListeners)
self.didEndScrubbing()
}
}.store(in: &viewModelListeners)
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
self.didSelectAudioStream(index: newAudioStreamIndex) if sliderIsScrubbing {
}.store(in: &viewModelListeners) self.didBeginScrubbing()
} else {
self.didEndScrubbing()
}
}.store(in: &viewModelListeners)
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
self.didSelectSubtitleStream(index: newSubtitleStreamIndex) self.didSelectAudioStream(index: newAudioStreamIndex)
}.store(in: &viewModelListeners) }.store(in: &viewModelListeners)
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
self.didToggleSubtitles(newValue: newSubtitlesEnabled) self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
}.store(in: &viewModelListeners) }.store(in: &viewModelListeners)
}
func setMediaPlayerTimeAtCurrentSlider() { viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
// Necessary math as VLCMediaPlayer doesn't work well self.didToggleSubtitles(newValue: newSubtitlesEnabled)
// by just setting the position }.store(in: &viewModelListeners)
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 { func setMediaPlayerTimeAtCurrentSlider() {
vlcMediaPlayer.jumpForward(Int32(newPositionOffset)) // Necessary math as VLCMediaPlayer doesn't work well
} else { // by just setting the position
vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset))) 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)))
}
}
} }
// MARK: Show/Hide Overlay // MARK: Show/Hide Overlay

View File

@ -1043,7 +1043,6 @@
62ECA01926FA6D6900E8EBB7 /* AppURLHandler */, 62ECA01926FA6D6900E8EBB7 /* AppURLHandler */,
5377CBF8263B596B003A4E83 /* Assets.xcassets */, 5377CBF8263B596B003A4E83 /* Assets.xcassets */,
53F866422687A45400DCD1D7 /* Components */, 53F866422687A45400DCD1D7 /* Components */,
5D160401278A41BA00D22B99 /* Extensions */,
5377CC02263B596B003A4E83 /* Info.plist */, 5377CC02263B596B003A4E83 /* Info.plist */,
E13D02842788B634000FCB04 /* Swiftfin.entitlements */, E13D02842788B634000FCB04 /* Swiftfin.entitlements */,
5377CBFA263B596B003A4E83 /* Preview Content */, 5377CBFA263B596B003A4E83 /* Preview Content */,
@ -1224,13 +1223,6 @@
path = Components; path = Components;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5D160401278A41BA00D22B99 /* Extensions */ = {
isa = PBXGroup;
children = (
);
path = Extensions;
sourceTree = "<group>";
};
5D64683B277B15E4009E09AE /* PreferenceUIHosting */ = { 5D64683B277B15E4009E09AE /* PreferenceUIHosting */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (

View File

@ -13,139 +13,155 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var settingsRouter: SettingsCoordinator.Router @EnvironmentObject
@ObservedObject var viewModel: SettingsViewModel var settingsRouter: SettingsCoordinator.Router
@ObservedObject
var viewModel: SettingsViewModel
@Default(.inNetworkBandwidth) var inNetworkStreamBitrate @Default(.inNetworkBandwidth)
@Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate var inNetworkStreamBitrate
@Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles @Default(.outOfNetworkBandwidth)
@Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode var outOfNetworkStreamBitrate
@Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode @Default(.isAutoSelectSubtitles)
@Default(.appAppearance) var appAppearance var isAutoSelectSubtitles
@Default(.overlayType) var overlayType @Default(.autoSelectSubtitlesLangCode)
@Default(.videoPlayerJumpForward) var jumpForwardLength var autoSelectSubtitlesLangcode
@Default(.videoPlayerJumpBackward) var jumpBackwardLength @Default(.autoSelectAudioLangCode)
@Default(.jumpGesturesEnabled) var jumpGesturesEnabled var autoSelectAudioLangcode
@Default(.showPosterLabels) var showPosterLabels @Default(.appAppearance)
@Default(.showCastAndCrew) var showCastAndCrew var appAppearance
@Default(.resumeOffset) var resumeOffset @Default(.overlayType)
@Default(.subtitleSize) var subtitleSize 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 { var body: some View {
Form { Form {
Section(header: EmptyView()) { Section(header: EmptyView()) {
HStack { HStack {
Text("User") Text("User")
Spacer() Spacer()
Text(viewModel.user.username) Text(viewModel.user.username)
.foregroundColor(.jellyfinPurple) .foregroundColor(.jellyfinPurple)
} }
Button { Button {
settingsRouter.route(to: \.serverDetail) settingsRouter.route(to: \.serverDetail)
} label: { } label: {
HStack { HStack {
Text("Server") Text("Server")
.foregroundColor(.primary) .foregroundColor(.primary)
Spacer() Spacer()
Text(viewModel.server.name) Text(viewModel.server.name)
.foregroundColor(.jellyfinPurple) .foregroundColor(.jellyfinPurple)
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
} }
} }
Button { Button {
settingsRouter.dismissCoordinator { settingsRouter.dismissCoordinator {
SessionManager.main.logout() SessionManager.main.logout()
} }
} label: { } label: {
Text("Switch User") Text("Switch User")
.font(.callout) .font(.callout)
} }
} }
// TODO: Implement these for playback // TODO: Implement these for playback
// Section(header: Text("Networking")) { // Section(header: Text("Networking")) {
// Picker("Default local quality", selection: $inNetworkStreamBitrate) { // Picker("Default local quality", selection: $inNetworkStreamBitrate) {
// ForEach(self.viewModel.bitrates, id: \.self) { bitrate in // ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
// Text(bitrate.name).tag(bitrate.value) // Text(bitrate.name).tag(bitrate.value)
// } // }
// } // }
// //
// Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { // Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
// ForEach(self.viewModel.bitrates, id: \.self) { bitrate in // ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
// Text(bitrate.name).tag(bitrate.value) // Text(bitrate.name).tag(bitrate.value)
// } // }
// } // }
// } // }
Section(header: Text("Video Player")) { Section(header: Text("Video Player")) {
Picker("Jump Forward Length", selection: $jumpForwardLength) { Picker("Jump Forward Length", selection: $jumpForwardLength) {
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
Text(length.label).tag(length.rawValue) Text(length.label).tag(length.rawValue)
} }
} }
Picker("Jump Backward Length", selection: $jumpBackwardLength) { Picker("Jump Backward Length", selection: $jumpBackwardLength) {
ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in ForEach(VideoPlayerJumpLength.allCases, id: \.self) { length in
Text(length.label).tag(length.rawValue) 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 { Button {
settingsRouter.route(to: \.overlaySettings) settingsRouter.route(to: \.overlaySettings)
} label: { } label: {
HStack { HStack {
Text("Overlay") Text("Overlay")
.foregroundColor(.primary) .foregroundColor(.primary)
Spacer() Spacer()
Text(overlayType.label) Text(overlayType.label)
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
} }
} }
Button { Button {
settingsRouter.route(to: \.experimentalSettings) settingsRouter.route(to: \.experimentalSettings)
} label: { } label: {
HStack { HStack {
Text("Experimental") Text("Experimental")
.foregroundColor(.primary) .foregroundColor(.primary)
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
} }
} }
} }
Section(header: L10n.accessibility.text) { Section(header: L10n.accessibility.text) {
Toggle("Show Poster Labels", isOn: $showPosterLabels) Toggle("Show Poster Labels", isOn: $showPosterLabels)
Toggle("Show Cast and Crew", isOn: $showCastAndCrew) Toggle("Show Cast and Crew", isOn: $showCastAndCrew)
Picker(L10n.appearance, selection: $appAppearance) { Picker(L10n.appearance, selection: $appAppearance) {
ForEach(AppAppearance.allCases, id: \.self) { appearance in ForEach(AppAppearance.allCases, id: \.self) { appearance in
Text(appearance.localizedName).tag(appearance.rawValue) Text(appearance.localizedName).tag(appearance.rawValue)
} }
} }
Picker("Subtitle size", selection: $subtitleSize) { Picker("Subtitle size", selection: $subtitleSize) {
ForEach(SubtitleSize.allCases, id: \.self) { size in ForEach(SubtitleSize.allCases, id: \.self) { size in
Text(size.label).tag(size.rawValue) Text(size.label).tag(size.rawValue)
} }
} }
} }
} }
.navigationBarTitle("Settings", displayMode: .inline) .navigationBarTitle("Settings", displayMode: .inline)
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) { ToolbarItemGroup(placement: .navigationBarLeading) {
Button { Button {
settingsRouter.dismissCoordinator() settingsRouter.dismissCoordinator()
} label: { } label: {
Image(systemName: "xmark.circle.fill") Image(systemName: "xmark.circle.fill")
} }
} }
} }
} }
} }

View File

@ -284,136 +284,137 @@ class VLCPlayerViewController: UIViewController {
extension VLCPlayerViewController { extension VLCPlayerViewController {
/// Main function that handles setting up the media player with the current 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 /// 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 /// Use case for this is setting new media within the same VLCPlayerViewController
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) { func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
// remove old player // remove old player
if vlcMediaPlayer.media != nil { if vlcMediaPlayer.media != nil {
viewModelListeners.forEach({ $0.cancel() }) viewModelListeners.forEach { $0.cancel() }
vlcMediaPlayer.stop() vlcMediaPlayer.stop()
viewModel.sendStopReport() viewModel.sendStopReport()
viewModel.playerOverlayDelegate = nil 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.delegate = self
vlcMediaPlayer.drawable = videoContentView vlcMediaPlayer.drawable = videoContentView
vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize]) vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize])
stopOverlayDismissTimer() stopOverlayDismissTimer()
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
let media = VLCMedia(url: newViewModel.streamURL) let media = VLCMedia(url: newViewModel.streamURL)
media.addOption("--prefetch-buffer-size=1048576") media.addOption("--prefetch-buffer-size=1048576")
media.addOption("--network-caching=5000") media.addOption("--network-caching=5000")
vlcMediaPlayer.media = media vlcMediaPlayer.media = media
setupOverlayHostingController(viewModel: newViewModel) setupOverlayHostingController(viewModel: newViewModel)
setupViewModelListeners(viewModel: newViewModel) setupViewModelListeners(viewModel: newViewModel)
newViewModel.getAdjacentEpisodes() newViewModel.getAdjacentEpisodes()
newViewModel.playerOverlayDelegate = self newViewModel.playerOverlayDelegate = self
let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0 let startPercentage = newViewModel.item.userData?.playedPercentage ?? 0
if startPercentage > 0 { if startPercentage > 0 {
if viewModel.resumeOffset { if viewModel.resumeOffset {
let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000) let videoDurationSeconds = Double(viewModel.item.runTimeTicks! / 10_000_000)
var startSeconds = round((startPercentage / 100) * videoDurationSeconds) var startSeconds = round((startPercentage / 100) * videoDurationSeconds)
startSeconds = startSeconds.subtract(5, floor: 0) startSeconds = startSeconds.subtract(5, floor: 0)
let newStartPercentage = startSeconds / videoDurationSeconds let newStartPercentage = startSeconds / videoDurationSeconds
newViewModel.sliderPercentage = newStartPercentage newViewModel.sliderPercentage = newStartPercentage
} else { } else {
newViewModel.sliderPercentage = startPercentage / 100 newViewModel.sliderPercentage = startPercentage / 100
} }
} }
viewModel = newViewModel viewModel = newViewModel
} }
// MARK: startPlayback // MARK: startPlayback
func startPlayback() {
vlcMediaPlayer.play()
// Setup external subtitles func startPlayback() {
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) { vlcMediaPlayer.play()
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
}
}
setMediaPlayerTimeAtCurrentSlider() // 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)
}
}
viewModel.sendPlayReport() setMediaPlayerTimeAtCurrentSlider()
restartOverlayDismissTimer() viewModel.sendPlayReport()
}
// MARK: setupViewModelListeners restartOverlayDismissTimer()
}
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) { // MARK: setupViewModelListeners
viewModel.$playbackSpeed.sink { newSpeed in private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
}.store(in: &viewModelListeners)
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in viewModel.$playbackSpeed.sink { newSpeed in
if sliderIsScrubbing { self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
self.didBeginScrubbing() }.store(in: &viewModelListeners)
} else {
self.didEndScrubbing()
}
}.store(in: &viewModelListeners)
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
self.didSelectAudioStream(index: newAudioStreamIndex) if sliderIsScrubbing {
}.store(in: &viewModelListeners) self.didBeginScrubbing()
} else {
self.didEndScrubbing()
}
}.store(in: &viewModelListeners)
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
self.didSelectSubtitleStream(index: newSubtitleStreamIndex) self.didSelectAudioStream(index: newAudioStreamIndex)
}.store(in: &viewModelListeners) }.store(in: &viewModelListeners)
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
self.didToggleSubtitles(newValue: newSubtitlesEnabled) self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
}.store(in: &viewModelListeners) }.store(in: &viewModelListeners)
viewModel.$jumpBackwardLength.sink { newJumpBackwardLength in viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
self.refreshJumpBackwardOverlayView(with: newJumpBackwardLength) self.didToggleSubtitles(newValue: newSubtitlesEnabled)
}.store(in: &viewModelListeners) }.store(in: &viewModelListeners)
viewModel.$jumpForwardLength.sink { newJumpForwardLength in viewModel.$jumpBackwardLength.sink { newJumpBackwardLength in
self.refreshJumpForwardOverlayView(with: newJumpForwardLength) self.refreshJumpBackwardOverlayView(with: newJumpBackwardLength)
}.store(in: &viewModelListeners) }.store(in: &viewModelListeners)
}
func setMediaPlayerTimeAtCurrentSlider() { viewModel.$jumpForwardLength.sink { newJumpForwardLength in
// Necessary math as VLCMediaPlayer doesn't work well self.refreshJumpForwardOverlayView(with: newJumpForwardLength)
// by just setting the position }.store(in: &viewModelListeners)
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 { func setMediaPlayerTimeAtCurrentSlider() {
vlcMediaPlayer.jumpForward(Int32(newPositionOffset)) // Necessary math as VLCMediaPlayer doesn't work well
} else { // by just setting the position
vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset))) 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)))
}
}
} }
// MARK: Show/Hide Overlay // MARK: Show/Hide Overlay