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
* 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
*/
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
#if os(tvOS)
import TVVLCKit
import TVVLCKit
#else
import MobileVLCKit
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
)
}
/// 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)
}
}

View File

@ -1,59 +1,58 @@
//
/*
* 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
*/
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
//
import Defaults
enum SubtitleSize: Int32, CaseIterable, Defaults.Serializable {
case smallest
case smaller
case regular
case larger
case largest
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"
}
}
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
}
}
/// 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
}
}
}

View File

@ -25,47 +25,52 @@ extension SwiftfinStore {
extension Defaults.Keys {
// Universal settings
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
// Universal settings
static let defaultHTTPScheme = Key<HTTPScheme>("defaultHTTPScheme", default: .http, suite: SwiftfinStore.Defaults.universalSuite)
static let appAppearance = Key<AppAppearance>("appAppearance", default: .system, suite: SwiftfinStore.Defaults.universalSuite)
// General settings
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 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 autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
// General settings
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 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 autoSelectSubtitlesLangCode = Key<String>("AutoSelectSubtitlesLangCode", default: "Auto",
suite: SwiftfinStore.Defaults.generalSuite)
static let autoSelectAudioLangCode = Key<String>("AutoSelectAudioLangCode", default: "Auto", suite: SwiftfinStore.Defaults.generalSuite)
// Customize settings
static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let showCastAndCrew = Key<Bool>("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Customize settings
static let showPosterLabels = Key<Bool>("showPosterLabels", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let showCastAndCrew = Key<Bool>("showCastAndCrew", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Video player / overlay settings
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 videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, 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)
// Video player / overlay settings
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 videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,
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
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 shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items
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 shouldShowAutoPlay = Key<Bool>("shouldShowAutoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items in overlay menu
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true, suite: SwiftfinStore.Defaults.generalSuite)
// Should show video player items in overlay menu
static let shouldShowJumpButtonsInOverlayMenu = Key<Bool>("shouldShowJumpButtonsInMenu", default: true,
suite: SwiftfinStore.Defaults.generalSuite)
// Experimental settings
struct Experimental {
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
}
// Experimental settings
enum Experimental {
static let syncSubtitleStateWithAdjacent = Key<Bool>("experimental.syncSubtitleState", default: false,
suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
}
// tvos specific
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 tvOSCinematicViews = Key<Bool>("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite)
// tvos specific
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 tvOSCinematicViews = Key<Bool>("tvOSCinematicViews", default: false, suite: SwiftfinStore.Defaults.generalSuite)
}

View File

@ -13,127 +13,136 @@ 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(.subtitleSize) var subtitleSize
@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 {
Button {} label: {
HStack {
Text("User")
Spacer()
Text(viewModel.user.username)
.foregroundColor(.jellyfinPurple)
}
}
} label: {
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")
.foregroundColor(.jellyfinPurple)
}
}
Image(systemName: "chevron.right")
.foregroundColor(.jellyfinPurple)
}
}
Button {
SessionManager.main.logout()
} label: {
Text("Switch User")
.foregroundColor(Color.jellyfinPurple)
.font(.callout)
}
}
Button {
SessionManager.main.logout()
} label: {
Text("Switch User")
.foregroundColor(Color.jellyfinPurple)
.font(.callout)
}
}
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("Resume 5 Second Offset", isOn: $resumeOffset)
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 {
settingsRouter.route(to: \.overlaySettings)
} label: {
HStack {
Text("Overlay")
.foregroundColor(.primary)
Spacer()
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 {
Toggle("Cinematic Views", isOn: $tvOSCinematicViews)
} header: {
Text("Appearance")
}
Section {
Toggle("Cinematic Views", isOn: $tvOSCinematicViews)
} header: {
Text("Appearance")
}
Section(header: L10n.accessibility.text) {
Toggle("Show Poster Labels", isOn: $showPosterLabels)
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)
}
}
}
}
}
}
}
Picker("Subtitle size", selection: $subtitleSize) {
ForEach(SubtitleSize.allCases, id: \.self) { size in
Text(size.label).tag(size.rawValue)
}
}
}
}
}
}
}
}
struct SettingsView_Previews: PreviewProvider {

View File

@ -382,138 +382,139 @@ 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
vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize])
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
func startPlayback() {
vlcMediaPlayer.play()
// MARK: startPlayback
// 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)
}
}
func startPlayback() {
vlcMediaPlayer.play()
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) {
viewModel.$playbackSpeed.sink { newSpeed in
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
}.store(in: &viewModelListeners)
// MARK: setupViewModelListeners
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
if sliderIsScrubbing {
self.didBeginScrubbing()
} else {
self.didEndScrubbing()
}
}.store(in: &viewModelListeners)
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
viewModel.$playbackSpeed.sink { newSpeed in
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
}.store(in: &viewModelListeners)
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
self.didSelectAudioStream(index: newAudioStreamIndex)
}.store(in: &viewModelListeners)
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
if sliderIsScrubbing {
self.didBeginScrubbing()
} else {
self.didEndScrubbing()
}
}.store(in: &viewModelListeners)
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
}.store(in: &viewModelListeners)
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
self.didSelectAudioStream(index: newAudioStreamIndex)
}.store(in: &viewModelListeners)
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
self.didToggleSubtitles(newValue: newSubtitlesEnabled)
}.store(in: &viewModelListeners)
}
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
}.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
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
self.didToggleSubtitles(newValue: newSubtitlesEnabled)
}.store(in: &viewModelListeners)
}
if newPositionOffset > 0 {
vlcMediaPlayer.jumpForward(Int32(newPositionOffset))
} else {
vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset)))
}
}
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)))
}
}
}
// MARK: Show/Hide Overlay

View File

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

View File

@ -13,139 +13,155 @@ 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(.subtitleSize) var subtitleSize
@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)
}
}
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")
}
}
}
}
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")
}
}
}
}
}

View File

@ -284,136 +284,137 @@ 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
vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize])
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
func startPlayback() {
vlcMediaPlayer.play()
// MARK: startPlayback
// 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)
}
}
func startPlayback() {
vlcMediaPlayer.play()
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
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
}.store(in: &viewModelListeners)
private func setupViewModelListeners(viewModel: VideoPlayerViewModel) {
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
if sliderIsScrubbing {
self.didBeginScrubbing()
} else {
self.didEndScrubbing()
}
}.store(in: &viewModelListeners)
viewModel.$playbackSpeed.sink { newSpeed in
self.vlcMediaPlayer.rate = Float(newSpeed.rawValue)
}.store(in: &viewModelListeners)
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
self.didSelectAudioStream(index: newAudioStreamIndex)
}.store(in: &viewModelListeners)
viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in
if sliderIsScrubbing {
self.didBeginScrubbing()
} else {
self.didEndScrubbing()
}
}.store(in: &viewModelListeners)
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
}.store(in: &viewModelListeners)
viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in
self.didSelectAudioStream(index: newAudioStreamIndex)
}.store(in: &viewModelListeners)
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
self.didToggleSubtitles(newValue: newSubtitlesEnabled)
}.store(in: &viewModelListeners)
viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in
self.didSelectSubtitleStream(index: newSubtitleStreamIndex)
}.store(in: &viewModelListeners)
viewModel.$jumpBackwardLength.sink { newJumpBackwardLength in
self.refreshJumpBackwardOverlayView(with: newJumpBackwardLength)
}.store(in: &viewModelListeners)
viewModel.$subtitlesEnabled.sink { newSubtitlesEnabled in
self.didToggleSubtitles(newValue: newSubtitlesEnabled)
}.store(in: &viewModelListeners)
viewModel.$jumpForwardLength.sink { newJumpForwardLength in
self.refreshJumpForwardOverlayView(with: newJumpForwardLength)
}.store(in: &viewModelListeners)
}
viewModel.$jumpBackwardLength.sink { newJumpBackwardLength in
self.refreshJumpBackwardOverlayView(with: newJumpBackwardLength)
}.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
viewModel.$jumpForwardLength.sink { newJumpForwardLength in
self.refreshJumpForwardOverlayView(with: newJumpForwardLength)
}.store(in: &viewModelListeners)
}
if newPositionOffset > 0 {
vlcMediaPlayer.jumpForward(Int32(newPositionOffset))
} else {
vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset)))
}
}
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)))
}
}
}
// MARK: Show/Hide Overlay