diff --git a/Shared/Extensions/JellyfinAPIExtensions/MediaStreamExtension.swift b/Shared/Extensions/JellyfinAPIExtensions/MediaStreamExtension.swift new file mode 100644 index 00000000..cb0084bf --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/MediaStreamExtension.swift @@ -0,0 +1,22 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import JellyfinAPI + +extension MediaStream { + + func externalURL(base: String) -> URL? { + guard let deliveryURL = deliveryUrl else { return nil } + var baseComponents = URLComponents(string: base) + baseComponents?.path += deliveryURL + + return baseComponents?.url + } +} diff --git a/Shared/ViewModels/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel.swift index ee3d4cba..aaa3311e 100644 --- a/Shared/ViewModels/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel.swift @@ -67,6 +67,8 @@ final class VideoPlayerViewModel: ViewModel { } @Published var autoplayEnabled: Bool { willSet { + previousItemVideoPlayerViewModel?.autoplayEnabled = newValue + nextItemVideoPlayerViewModel?.autoplayEnabled = newValue Defaults[.autoplayEnabled] = newValue } } @@ -115,6 +117,16 @@ final class VideoPlayerViewModel: ViewModel { return Int64(currentSeconds) * 10_000_000 } + // MARK: Helpers + + var currentAudioStream: MediaStream? { + return audioStreams.first(where: { $0.index == selectedAudioStreamIndex }) + } + + var currentSubtitleStream: MediaStream? { + return subtitleStreams.first(where: { $0.index == selectedSubtitleStreamIndex }) + } + // Necessary PassthroughSubject to capture manual scrubbing from sliders let sliderScrubbingSubject = PassthroughSubject() diff --git a/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift index e335d271..ea106ca7 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift @@ -24,7 +24,7 @@ class VLCPlayerViewController: UIViewController { // MARK: variables private var viewModel: VideoPlayerViewModel - private var vlcMediaPlayer = VLCMediaPlayer() + private var vlcMediaPlayer: VLCMediaPlayer private var lastPlayerTicks: Int64 = 0 private var lastProgressReportTicks: Int64 = 0 private var viewModelListeners = Set() @@ -59,6 +59,7 @@ class VLCPlayerViewController: UIViewController { init(viewModel: VideoPlayerViewModel) { self.viewModel = viewModel + self.vlcMediaPlayer = VLCMediaPlayer() super.init(nibName: nil, bundle: nil) @@ -118,14 +119,6 @@ class VLCPlayerViewController: UIViewController { view.backgroundColor = .black - // Outside of 'setupMediaPlayer' such that they - // aren't unnecessarily set more than once - vlcMediaPlayer.delegate = self - vlcMediaPlayer.drawable = videoContentView - - // TODO: custom font sizes - vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16) - setupMediaPlayer(newViewModel: viewModel) setupPanGestureRecognizer() @@ -387,6 +380,28 @@ extension VLCPlayerViewController { /// Use case for this is setting new media within the same VLCPlayerViewController func setupMediaPlayer(newViewModel: VideoPlayerViewModel) { + // remove old player + + if vlcMediaPlayer.media != nil { + viewModelListeners.forEach({ $0.cancel() }) + + vlcMediaPlayer.stop() + viewModel.sendStopReport() + viewModel.playerOverlayDelegate = nil + } + + vlcMediaPlayer = VLCMediaPlayer() + + // setup with new player and view model + + vlcMediaPlayer = VLCMediaPlayer() + + vlcMediaPlayer.delegate = self + vlcMediaPlayer.drawable = videoContentView + + // TODO: Custom subtitle sizes + vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16) + stopOverlayDismissTimer() // Stop current media if there is one @@ -436,6 +451,13 @@ extension VLCPlayerViewController { func startPlayback() { vlcMediaPlayer.play() + // Setup external subtitles + for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) { + if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) { + vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false) + } + } + setMediaPlayerTimeAtCurrentSlider() viewModel.sendPlayReport() @@ -672,7 +694,8 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate { } // If needing to fix subtitle streams during playback - if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled { + if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && + viewModel.subtitlesEnabled { didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex) } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 2413e1ea..70f53719 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -274,6 +274,8 @@ E1218C9C271A26C400EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9B271A26C400EA0737 /* Nuke */; }; E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9D271A2CD600EA0737 /* CombineExt */; }; E1218CA0271A2CF200EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9F271A2CF200EA0737 /* Nuke */; }; + E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */; }; + E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */; }; E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */; }; E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; @@ -645,6 +647,7 @@ E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = ""; }; E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = ""; }; E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = ""; }; + E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaStreamExtension.swift; sourceTree = ""; }; E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = ""; }; E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = ""; }; E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = ""; }; @@ -1502,10 +1505,11 @@ isa = PBXGroup; children = ( E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */, - E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */, E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */, + E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */, 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */, E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, + E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */, E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */, ); path = JellyfinAPIExtensions; @@ -1759,7 +1763,7 @@ 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */, 53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */, 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */, - E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore.git" */, + E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */, E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */, E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */, E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, @@ -2051,6 +2055,7 @@ E1C812CC277AE40A00918266 /* VideoPlayerView.swift in Sources */, 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, + E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */, E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, @@ -2191,6 +2196,7 @@ E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, + E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */, E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */, 62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */, 625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */, @@ -2701,7 +2707,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2738,7 +2744,7 @@ CURRENT_PROJECT_VERSION = 66; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2769,7 +2775,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2796,7 +2802,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2936,7 +2942,7 @@ minimumVersion = 1.0.0; }; }; - E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore.git" */ = { + E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/JohnEstropia/CoreStore.git"; requirement = { @@ -3060,17 +3066,17 @@ }; E13DD3C52716499E009D4DAF /* CoreStore */ = { isa = XCSwiftPackageProductDependency; - package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore.git" */; + package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; productName = CoreStore; }; E13DD3CC27164CA7009D4DAF /* CoreStore */ = { isa = XCSwiftPackageProductDependency; - package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore.git" */; + package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; productName = CoreStore; }; E13DD3CE27164E1F009D4DAF /* CoreStore */ = { isa = XCSwiftPackageProductDependency; - package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore.git" */; + package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */; productName = CoreStore; }; E13DD3D227168E65009D4DAF /* Defaults */ = { diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift index 7f1c6216..ce1ba728 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift @@ -24,7 +24,7 @@ class VLCPlayerViewController: UIViewController { // MARK: variables private var viewModel: VideoPlayerViewModel - private var vlcMediaPlayer = VLCMediaPlayer() + private var vlcMediaPlayer: VLCMediaPlayer private var lastPlayerTicks: Int64 = 0 private var lastProgressReportTicks: Int64 = 0 private var viewModelListeners = Set() @@ -49,6 +49,7 @@ class VLCPlayerViewController: UIViewController { init(viewModel: VideoPlayerViewModel) { self.viewModel = viewModel + self.vlcMediaPlayer = VLCMediaPlayer() super.init(nibName: nil, bundle: nil) @@ -97,14 +98,6 @@ class VLCPlayerViewController: UIViewController { view.backgroundColor = .black - // These are kept outside of 'setupMediaPlayer' such that - // they aren't unnecessarily set more than once - vlcMediaPlayer.delegate = self - vlcMediaPlayer.drawable = videoContentView - - // TODO: Custom subtitle sizes - vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14) - setupMediaPlayer(newViewModel: viewModel) refreshJumpBackwardOverlayView(with: viewModel.jumpBackwardLength) @@ -287,9 +280,8 @@ extension VLCPlayerViewController { /// Use case for this is setting new media within the same VLCPlayerViewController func setupMediaPlayer(newViewModel: VideoPlayerViewModel) { - stopOverlayDismissTimer() + // remove old player - // Stop current media if there is one if vlcMediaPlayer.media != nil { viewModelListeners.forEach({ $0.cancel() }) @@ -298,6 +290,20 @@ extension VLCPlayerViewController { viewModel.playerOverlayDelegate = nil } + vlcMediaPlayer = VLCMediaPlayer() + + // setup with new player and view model + + vlcMediaPlayer = VLCMediaPlayer() + + vlcMediaPlayer.delegate = self + vlcMediaPlayer.drawable = videoContentView + + // TODO: Custom subtitle sizes + vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14) + + stopOverlayDismissTimer() + lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0 @@ -334,6 +340,13 @@ extension VLCPlayerViewController { func startPlayback() { vlcMediaPlayer.play() + // Setup external subtitles + for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) { + if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) { + vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false) + } + } + setMediaPlayerTimeAtCurrentSlider() viewModel.sendPlayReport() @@ -526,7 +539,8 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate { } // If needing to fix subtitle streams during playback - if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled { + if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && + viewModel.subtitlesEnabled { didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex) }