diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/NativePlayerViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/NativePlayerViewController.swift index f5fcbbc3..695f548f 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/NativePlayerViewController.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/NativePlayerViewController.swift @@ -145,15 +145,18 @@ class NativePlayerViewController: AVPlayerViewController { private func play() { player?.play() - viewModel.sendPlayReport(startTimeTicks: viewModel.item.userData?.playbackPositionTicks ?? 0) +// viewModel.sendPlayReport(startTimeTicks: viewModel.item.userData?.playbackPositionTicks ?? 0) + viewModel.sendPlayReport() } private func sendProgressReport(seconds: Double) { - viewModel.sendProgressReport(ticks: Int64(seconds) * 10_000_000) +// viewModel.sendProgressReport(ticks: Int64(seconds) * 10_000_000) + viewModel.sendProgressReport() } private func stop() { self.player?.pause() - viewModel.sendStopReport(ticks: 10_000_000) + viewModel.sendStopReport() +// viewModel.sendStopReport(ticks: 10_000_000) } } diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift new file mode 100644 index 00000000..0cbc102a --- /dev/null +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VLCPlayerViewController.swift @@ -0,0 +1,612 @@ +// +// PlayerViewController.swift +// JellyfinVideoPlayerDev +// +// Created by Ethan Pippin on 11/12/21. +// + +import AVKit +import AVFoundation +import Combine +import Defaults +import JellyfinAPI +import MediaPlayer +import TVVLCKit +import SwiftUI +import UIKit + +// TODO: Make the VLC player layer a view +// This will allow changing media and putting the view somewhere else +// in a compact state, like a small viewer while navigating the app + +// TODO: Look at making overlays handle timer and all gesture events + +class VLCPlayerViewController: UIViewController { + + // MARK: variables + + private var viewModel: VideoPlayerViewModel + private var vlcMediaPlayer = VLCMediaPlayer() + private var lastPlayerTicks: Int64 = 0 + private var lastProgressReportTicks: Int64 = 0 + private var viewModelReactCancellables = Set() + private var overlayDismissTimer: Timer? + + private var currentPlayerTicks: Int64 { + return Int64(vlcMediaPlayer.time.intValue) * 100_000 + } + + private var displayingOverlay: Bool { +// return currentOverlayHostingController?.view.alpha ?? 0 > 0 + return false + } + + private var jumpForwardLength: VideoPlayerJumpLength { + return Defaults[.videoPlayerJumpForward] + } + + private var jumpBackwardLength: VideoPlayerJumpLength { + return Defaults[.videoPlayerJumpBackward] + } + + private lazy var videoContentView = makeVideoContentView() + private lazy var tapGestureView = makeTapGestureView() +// private var currentOverlayHostingController: UIHostingController? + + // MARK: init + + init(viewModel: VideoPlayerViewModel) { + + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + + viewModel.playerOverlayDelegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupSubviews() { + view.addSubview(videoContentView) + view.addSubview(tapGestureView) + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + videoContentView.topAnchor.constraint(equalTo: view.topAnchor), + videoContentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + videoContentView.leftAnchor.constraint(equalTo: view.leftAnchor), + videoContentView.rightAnchor.constraint(equalTo: view.rightAnchor) + ]) + NSLayoutConstraint.activate([ + tapGestureView.topAnchor.constraint(equalTo: videoContentView.topAnchor), + tapGestureView.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor), + tapGestureView.leftAnchor.constraint(equalTo: videoContentView.leftAnchor), + tapGestureView.rightAnchor.constraint(equalTo: videoContentView.rightAnchor) + ]) + } + + // MARK: viewWillDisappear + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + didSelectClose() + + let defaultNotificationCenter = NotificationCenter.default + defaultNotificationCenter.removeObserver(self, name: UIApplication.willTerminateNotification, object: nil) + defaultNotificationCenter.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) + defaultNotificationCenter.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + // MARK: viewDidLoad + + override func viewDidLoad() { + super.viewDidLoad() + + setupSubviews() + setupConstraints() + + 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 + vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14) + + setupMediaPlayer(newViewModel: viewModel) + + setupRightSwipedGestureRecognizer() + setupLeftSwipedGestureRecognizer() + + let defaultNotificationCenter = NotificationCenter.default + defaultNotificationCenter.addObserver(self, selector: #selector(appWillTerminate), name: UIApplication.willTerminateNotification, object: nil) + defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil) + defaultNotificationCenter.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + @objc private func appWillTerminate() { + viewModel.sendStopReport() + } + + @objc private func appWillResignActive() { + showOverlay() + + stopOverlayDismissTimer() + + vlcMediaPlayer.pause() + + viewModel.sendPauseReport(paused: true) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + startPlayback() + } + + // MARK: subviews + + private func makeVideoContentView() -> UIView { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .black + + return view + } + + private func makeTapGestureView() -> UIView { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap)) + let rightSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didRightSwipe)) + rightSwipeGesture.direction = .right + let leftSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(didLeftSwipe)) + leftSwipeGesture.direction = .left + + view.addGestureRecognizer(singleTapGesture) + view.addGestureRecognizer(rightSwipeGesture) + view.addGestureRecognizer(leftSwipeGesture) + + return view + } + + @objc private func didTap() { + self.didGenerallyTap() + } + + @objc private func didRightSwipe() { + self.didSelectForward() + } + + @objc private func didLeftSwipe() { + self.didSelectBackward() + } + + // MARK: pressesBegan + override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { + guard let buttonPress = presses.first?.type else { return } + + switch(buttonPress) { + case .menu: + print("Menu") + case .playPause: + didSelectMain() + print("Play/Pause") + case .select: + print("select") + case .upArrow: + print("Up arrow") + case .downArrow: + print("Down arrow") + case .leftArrow: + print("Left arrow") + case .rightArrow: + print("right arrow") + case .pageUp: + print("page up") + case .pageDown: + print("page down") + @unknown default: () + } + } + + func setupRightSwipedGestureRecognizer() { + let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipedRight)) + swipeRecognizer.direction = .right + view.addGestureRecognizer(swipeRecognizer) + } + + @objc func swipedRight() { + didSelectForward() + } + + func setupLeftSwipedGestureRecognizer() { + let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(swipedLeft)) + swipeRecognizer.direction = .left + view.addGestureRecognizer(swipeRecognizer) + } + + @objc func swipedLeft() { + didSelectBackward() + } + + // MARK: setupOverlayHostingController + private func setupOverlayHostingController(viewModel: VideoPlayerViewModel) { + +// // TODO: Look at injecting viewModel into the environment so it updates the current overlay +// if let currentOverlayHostingController = currentOverlayHostingController { +// // UX fade-out +// UIView.animate(withDuration: 0.5) { +// currentOverlayHostingController.view.alpha = 0 +// } completion: { _ in +// currentOverlayHostingController.view.isHidden = true +// +// currentOverlayHostingController.view.removeFromSuperview() +// currentOverlayHostingController.removeFromParent() +//// self.currentOverlayHostingController = nil +// } +// } +// +// let newOverlayView = VLCPlayerCompactOverlayView(viewModel: viewModel) +// let newOverlayHostingController = UIHostingController(rootView: newOverlayView) +// +// newOverlayHostingController.view.translatesAutoresizingMaskIntoConstraints = false +// newOverlayHostingController.view.backgroundColor = UIColor.clear +// +// // UX fade-in +// newOverlayHostingController.view.alpha = 0 +// +// addChild(newOverlayHostingController) +// view.addSubview(newOverlayHostingController.view) +// newOverlayHostingController.didMove(toParent: self) +// +// NSLayoutConstraint.activate([ +// newOverlayHostingController.view.topAnchor.constraint(equalTo: videoContentView.topAnchor), +// newOverlayHostingController.view.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor), +// newOverlayHostingController.view.leftAnchor.constraint(equalTo: videoContentView.leftAnchor), +// newOverlayHostingController.view.rightAnchor.constraint(equalTo: videoContentView.rightAnchor) +// ]) +// +// // UX fade-in +// UIView.animate(withDuration: 0.5) { +// newOverlayHostingController.view.alpha = 1 +// } +// +// self.currentOverlayHostingController = newOverlayHostingController +// +// // There is a behavior when setting this that the navigation bar +// // on the current navigation controller pops up, re-hide it +// self.navigationController?.isNavigationBarHidden = true + } +} + +// MARK: setupMediaPlayer +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) { + + stopOverlayDismissTimer() + + // Stop current media if there is one + if vlcMediaPlayer.media != nil { + viewModelReactCancellables.forEach({ $0.cancel() }) + + vlcMediaPlayer.stop() + viewModel.sendStopReport() + viewModel.playerOverlayDelegate = nil + } + + 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") + + vlcMediaPlayer.media = media + + setupOverlayHostingController(viewModel: newViewModel) + setupViewModelListeners(viewModel: newViewModel) + + newViewModel.getAdjacentEpisodes() + newViewModel.playerOverlayDelegate = self + + let startPercentage = viewModel.item.userData?.playedPercentage ?? 0 + + if startPercentage > 0 { + newViewModel.sliderPercentage = startPercentage / 100 + } + + viewModel = newViewModel + } + + // MARK: startPlayback + func startPlayback() { + vlcMediaPlayer.play() + + setMediaPlayerTimeAtCurrentSlider() + + viewModel.sendPlayReport() + + restartOverlayDismissTimer() + } + + // MARK: setupViewModelListeners + + private func setupViewModelListeners(viewModel: VideoPlayerViewModel) { + viewModel.$playbackSpeed.sink { newSpeed in + self.vlcMediaPlayer.rate = Float(newSpeed.rawValue) + }.store(in: &viewModelReactCancellables) + + viewModel.$sliderIsScrubbing.sink { sliderIsScrubbing in + if sliderIsScrubbing { + self.didBeginScrubbing() + } else { + self.didEndScrubbing() + } + }.store(in: &viewModelReactCancellables) + + viewModel.$selectedAudioStreamIndex.sink { newAudioStreamIndex in + self.didSelectAudioStream(index: newAudioStreamIndex) + }.store(in: &viewModelReactCancellables) + + viewModel.$selectedSubtitleStreamIndex.sink { newSubtitleStreamIndex in + self.didSelectSubtitleStream(index: newSubtitleStreamIndex) + }.store(in: &viewModelReactCancellables) + } + + 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 +extension VLCPlayerViewController { + + private func showOverlay() { +// guard let overlayHostingController = currentOverlayHostingController else { return } +// +// guard overlayHostingController.view.alpha != 1 else { return } +// +// UIView.animate(withDuration: 0.2) { +// overlayHostingController.view.alpha = 1 +// } + } + + private func hideOverlay() { +// guard let overlayHostingController = currentOverlayHostingController else { return } +// +// guard overlayHostingController.view.alpha != 0 else { return } +// +// UIView.animate(withDuration: 0.2) { +// overlayHostingController.view.alpha = 0 +// } + } + + private func toggleOverlay() { +// guard let overlayHostingController = currentOverlayHostingController else { return } +// +// if overlayHostingController.view.alpha < 1 { +// showOverlay() +// } else { +// hideOverlay() +// } + } +} + +// MARK: OverlayTimer +extension VLCPlayerViewController { + + private func restartOverlayDismissTimer(interval: Double = 3) { + self.overlayDismissTimer?.invalidate() + self.overlayDismissTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(dismissTimerFired), userInfo: nil, repeats: false) + } + + @objc private func dismissTimerFired() { + self.hideOverlay() + } + + private func stopOverlayDismissTimer() { + self.overlayDismissTimer?.invalidate() + } +} + +// MARK: VLCMediaPlayerDelegate +extension VLCPlayerViewController: VLCMediaPlayerDelegate { + + + // MARK: mediaPlayerStateChanged + func mediaPlayerStateChanged(_ aNotification: Notification!) { + + viewModel.playerState = vlcMediaPlayer.state + + if vlcMediaPlayer.state == VLCMediaPlayerState.ended { + if viewModel.autoPlayNextItem && viewModel.shouldShowAutoPlayNextItem && viewModel.nextItemVideoPlayerViewModel != nil { + didSelectNextItem() + } else { + didSelectClose() + } + } + } + + // MARK: mediaPlayerTimeChanged + func mediaPlayerTimeChanged(_ aNotification: Notification!) { + + guard !viewModel.sliderIsScrubbing else { + lastPlayerTicks = currentPlayerTicks + return + } + + viewModel.sliderPercentage = Double(vlcMediaPlayer.position) + + // Have to manually set playing because VLCMediaPlayer doesn't + // properly set it itself + if abs(currentPlayerTicks - lastPlayerTicks) >= 10_000 { + viewModel.playerState = VLCMediaPlayerState.playing + } + + // If needing to fix subtitle streams during playback + if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled { + didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex) + didSelectAudioStream(index: viewModel.selectedAudioStreamIndex) + } + + lastPlayerTicks = currentPlayerTicks + + // Send progress report every 5 seconds + if abs(lastProgressReportTicks - currentPlayerTicks) >= 500_000_000 { + viewModel.sendProgressReport() + + lastProgressReportTicks = currentPlayerTicks + } + } +} + +// MARK: PlayerOverlayDelegate +extension VLCPlayerViewController: PlayerOverlayDelegate { + + func didSelectAudioStream(index: Int) { + vlcMediaPlayer.currentAudioTrackIndex = Int32(index) + + viewModel.sendProgressReport() + + lastProgressReportTicks = currentPlayerTicks + } + + func didSelectSubtitleStream(index: Int) { + if viewModel.subtitlesEnabled { + vlcMediaPlayer.currentVideoSubTitleIndex = Int32(index) + } else { + vlcMediaPlayer.currentVideoSubTitleIndex = -1 + } + + viewModel.sendProgressReport() + + lastProgressReportTicks = currentPlayerTicks + } + + func didSelectClose() { + vlcMediaPlayer.stop() + + viewModel.sendStopReport() + + dismiss(animated: true, completion: nil) + } + + func didSelectGoogleCast() { + print("didSelectCast") + } + + func didSelectAirplay() { + print("didSelectAirplay") + } + + func didSelectCaptions() { + + viewModel.subtitlesEnabled = !viewModel.subtitlesEnabled + + if viewModel.subtitlesEnabled { + vlcMediaPlayer.currentVideoSubTitleIndex = Int32(viewModel.selectedSubtitleStreamIndex) + } else { + vlcMediaPlayer.currentVideoSubTitleIndex = -1 + } + } + + // TODO: Implement properly in overlays + func didSelectMenu() { + stopOverlayDismissTimer() + } + + // TODO: Implement properly in overlays + func didDeselectMenu() { + restartOverlayDismissTimer() + } + + func didSelectBackward() { + vlcMediaPlayer.jumpBackward(jumpBackwardLength.rawValue) + + restartOverlayDismissTimer() + + viewModel.sendProgressReport() + + self.lastProgressReportTicks = currentPlayerTicks + } + + func didSelectForward() { + vlcMediaPlayer.jumpForward(jumpForwardLength.rawValue) + + restartOverlayDismissTimer() + + viewModel.sendProgressReport() + + self.lastProgressReportTicks = currentPlayerTicks + } + + func didSelectMain() { + + switch viewModel.playerState { + case .buffering: + vlcMediaPlayer.play() + restartOverlayDismissTimer() + case .playing: + viewModel.sendPauseReport(paused: true) + vlcMediaPlayer.pause() + restartOverlayDismissTimer(interval: 5) + case .paused: + viewModel.sendPauseReport(paused: false) + vlcMediaPlayer.play() + restartOverlayDismissTimer() + default: () + } + } + + func didGenerallyTap() { + toggleOverlay() + + restartOverlayDismissTimer(interval: 5) + } + + func didBeginScrubbing() { + stopOverlayDismissTimer() + } + + func didEndScrubbing() { + setMediaPlayerTimeAtCurrentSlider() + + restartOverlayDismissTimer() + + viewModel.sendProgressReport() + + self.lastProgressReportTicks = currentPlayerTicks + } + + func didSelectPreviousItem() { + setupMediaPlayer(newViewModel: viewModel.previousItemVideoPlayerViewModel!) + startPlayback() + } + + func didSelectNextItem() { + setupMediaPlayer(newViewModel: viewModel.nextItemVideoPlayerViewModel!) + startPlayback() + } +} diff --git a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerView.swift b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerView.swift index 0b2aee90..8f9bf3e9 100644 --- a/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerView.swift +++ b/JellyfinPlayer tvOS/Views/VideoPlayer/VideoPlayerView.swift @@ -23,3 +23,19 @@ struct NativePlayerView: UIViewControllerRepresentable { } } + +struct VLCPlayerView: UIViewControllerRepresentable { + + let viewModel: VideoPlayerViewModel + + typealias UIViewControllerType = VLCPlayerViewController + + func makeUIViewController(context: Context) -> VLCPlayerViewController { + + return VLCPlayerViewController(viewModel: viewModel) + } + + func updateUIViewController(_ uiViewController: VLCPlayerViewController, context: Context) { + + } +} diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index ecab7ffb..6b6adc17 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -253,6 +253,8 @@ E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; E131691926C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; }; + E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; }; + E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */; }; E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; @@ -288,6 +290,7 @@ E173DA5026D048D600CC4EB7 /* ServerDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA4F26D048D600CC4EB7 /* ServerDetailView.swift */; }; E173DA5226D04AAF00CC4EB7 /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5126D04AAF00CC4EB7 /* ColorExtension.swift */; }; E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E173DA5326D050F500CC4EB7 /* ServerDetailViewModel.swift */; }; + E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */ = {isa = PBXBuildFile; productRef = E178857C278037FD0094FBCF /* JellyfinAPI */; }; E18845F526DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; }; E18845F626DD631E00B0C5B7 /* BaseItemDto+Stackable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */; }; E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18845F726DEA9C900B0C5B7 /* ItemViewBody.swift */; }; @@ -570,6 +573,7 @@ E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.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 = ""; }; E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; E13DD3C127164941009D4DAF /* SwiftfinStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = ""; }; E13DD3C727164B1E009D4DAF /* UIDeviceExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDeviceExtensions.swift; sourceTree = ""; }; @@ -650,6 +654,7 @@ 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */, E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */, E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */, + E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */, E12186DE2718F1C50010884C /* Defaults in Frameworks */, 53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */, 363CADF08820D3B2055CF1D8 /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */, @@ -710,6 +715,7 @@ E1C812C7277AE40900918266 /* NativePlayerViewController.swift */, E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */, E1C812C8277AE40900918266 /* VideoPlayerView.swift */, + E1384943278036C70024FB48 /* VLCPlayerViewController.swift */, ); path = VideoPlayer; sourceTree = ""; @@ -1419,6 +1425,7 @@ E1218C9D271A2CD600EA0737 /* CombineExt */, E1218C9F271A2CF200EA0737 /* Nuke */, E1A9999A271A343C008E78C0 /* SwiftUICollection */, + E178857C278037FD0094FBCF /* JellyfinAPI */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */; @@ -1826,6 +1833,7 @@ 53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */, 53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */, 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, + E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, E1FCD08926C35A0D007C8DCF /* NetworkError.swift in Sources */, @@ -1883,6 +1891,7 @@ 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, E19169CF272514760085832A /* HTTPScheme.swift in Sources */, + E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */, E1C812CD277AE40A00918266 /* VideoPlayerViewModel.swift in Sources */, C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */, E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */, @@ -2810,6 +2819,11 @@ package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */; productName = Defaults; }; + E178857C278037FD0094FBCF /* JellyfinAPI */ = { + isa = XCSwiftPackageProductDependency; + package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; + productName = JellyfinAPI; + }; E1A99998271A3429008E78C0 /* SwiftUICollection */ = { isa = XCSwiftPackageProductDependency; package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */; diff --git a/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift b/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift index 35d872f5..191a836d 100644 --- a/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/JellyfinPlayer/Views/VideoPlayer/VLCPlayerViewController.swift @@ -19,6 +19,8 @@ import UIKit // This will allow changing media and putting the view somewhere else // in a compact state, like a small viewer while navigating the app +// TODO: Look at making overlays handle timer and all gesture events + class VLCPlayerViewController: UIViewController { // MARK: variables diff --git a/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift index a8d70173..56c03fae 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift @@ -27,7 +27,10 @@ final class VideoPlayerCoordinator: NavigationCoordinatable { } @ViewBuilder func makeStart() -> some View { - NativePlayerView(viewModel: viewModel) +// NativePlayerView(viewModel: viewModel) +// .navigationBarHidden(true) +// .ignoresSafeArea() + VLCPlayerView(viewModel: viewModel) .navigationBarHidden(true) .ignoresSafeArea() }