format merge
This commit is contained in:
parent
071d07d5ff
commit
e12da2cf07
|
@ -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
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue