From 49eaaef6ffc91a136562d7081330f07a1db4808b Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 21 Jul 2021 23:12:29 -0600 Subject: [PATCH 01/12] Set server keyboard to url --- JellyfinPlayer/ConnectToServerView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 589dcdb0..706fb7a4 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -109,6 +109,7 @@ struct ConnectToServerView: View { TextField(NSLocalizedString("Server URL", comment: ""), text: $uri) .disableAutocorrection(true) .autocapitalization(.none) + .keyboardType(.URL) Button { viewModel.connectToServer() } label: { From 1e8b1f330133461425f6f941be209f32bc8f07bb Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 21 Jul 2021 23:32:44 -0600 Subject: [PATCH 02/12] Show server name in settings above logged in user --- JellyfinPlayer/SettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index dbd8a425..b23134ca 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -63,7 +63,7 @@ struct SettingsView: View { ) } - Section { + Section(header: Text(ServerEnvironment.current.server.name ?? "")) { HStack { Text("Signed in as \(username)").foregroundColor(.primary) Spacer() From 840414f801ee6ac66041573fface2252dc9ba5e3 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 21 Jul 2021 23:48:57 -0600 Subject: [PATCH 03/12] URL input for server on tvOS --- JellyfinPlayer tvOS/ConnectToServerView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/JellyfinPlayer tvOS/ConnectToServerView.swift b/JellyfinPlayer tvOS/ConnectToServerView.swift index 8701e2da..e408c871 100644 --- a/JellyfinPlayer tvOS/ConnectToServerView.swift +++ b/JellyfinPlayer tvOS/ConnectToServerView.swift @@ -111,6 +111,7 @@ struct ConnectToServerView: View { TextField(NSLocalizedString("Server URL", comment: ""), text: $uri) .disableAutocorrection(true) .autocapitalization(.none) + .keyboardType(.URL) Button { viewModel.connectToServer() } label: { From b644d0b743d8b3ed560c88f1afa99b7f575287d1 Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Thu, 22 Jul 2021 13:45:59 -0400 Subject: [PATCH 04/12] fix ATS --- JellyfinPlayer tvOS/Info.plist | 5 +++++ JellyfinPlayer/Info.plist | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/JellyfinPlayer tvOS/Info.plist b/JellyfinPlayer tvOS/Info.plist index 34b9158f..d2b80c8d 100644 --- a/JellyfinPlayer tvOS/Info.plist +++ b/JellyfinPlayer tvOS/Info.plist @@ -22,6 +22,11 @@ $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UILaunchScreen UIRequiredDeviceCapabilities diff --git a/JellyfinPlayer/Info.plist b/JellyfinPlayer/Info.plist index 02fe7bda..5121b455 100644 --- a/JellyfinPlayer/Info.plist +++ b/JellyfinPlayer/Info.plist @@ -28,8 +28,6 @@ NSAllowsArbitraryLoads - NSAllowsLocalNetworking - NSBluetoothAlwaysUsageDescription ${PRODUCT_NAME} uses Bluetooth to discover nearby Cast devices. From ba6a62d546917f5a43f1b48d6bd4d93f73dc4060 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 23 Jul 2021 11:54:37 +0900 Subject: [PATCH 05/12] update README.md --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 23c1584a..a507fd71 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@

SwiftFin

SwiftFin

+ + + @@ -19,6 +22,27 @@ Join the Beta on TestFlight +**Don't see Jellyfin in your language?** + +Check out our [Weblate instance](https://translate.jellyfin.org/projects/swiftfin/) to help translate Swiftfin and other projects. + + + + + ## ⚙️ Development Xcode 13.0 with command line tools. + +### Build Process + +```bash +# install Cocoapods (if not installed) +$ sudo gem install cocoapods + +# install dependencies +$ pod install + +# open workspace +$ open JellyfinPlayer.xcworkspace +``` \ No newline at end of file From dfa41359a68ae388f7614b1e33f2ad8d40740d37 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 23 Jul 2021 12:04:37 +0900 Subject: [PATCH 06/12] update Build Process --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a507fd71..ddf2d85d 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,9 @@ $ sudo gem install cocoapods # install dependencies $ pod install -# open workspace +# open workspace and build it $ open JellyfinPlayer.xcworkspace + +# or build using xcodebuild +$ xcodebuild build -workspace "JellyfinPlayer.xcworkspace" -scheme "JellyfinPlayer" ``` \ No newline at end of file From fe443465566988816e8f0767d354e5e712a2f21f Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 23 Jul 2021 12:06:59 +0900 Subject: [PATCH 07/12] change some word --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ddf2d85d..53b00424 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Join the Beta on TestFlight -**Don't see Jellyfin in your language?** +**Don't see Swiftfin in your language?** Check out our [Weblate instance](https://translate.jellyfin.org/projects/swiftfin/) to help translate Swiftfin and other projects. From f9deb65e99a13f1cf234b46deb8481ccf822ab2a Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 23 Jul 2021 12:10:44 +0900 Subject: [PATCH 08/12] update Build Process --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 53b00424..26bb7a53 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,4 @@ $ pod install # open workspace and build it $ open JellyfinPlayer.xcworkspace - -# or build using xcodebuild -$ xcodebuild build -workspace "JellyfinPlayer.xcworkspace" -scheme "JellyfinPlayer" ``` \ No newline at end of file From 99e8bf0d43d347ed93a3ee6696e859b7bc2a0176 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 23 Jul 2021 14:34:02 +0900 Subject: [PATCH 09/12] Swiftfin -> SwiftFin --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 26bb7a53..a553f6cc 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ Join the Beta on TestFlight -**Don't see Swiftfin in your language?** +**Don't see SwiftFin in your language?** -Check out our [Weblate instance](https://translate.jellyfin.org/projects/swiftfin/) to help translate Swiftfin and other projects. +Check out our [Weblate instance](https://translate.jellyfin.org/projects/swiftfin/) to help translate SwiftFin and other projects. From cceab035b617ee50d122c4e6991f7224862ecd5a Mon Sep 17 00:00:00 2001 From: Peng Lei Date: Sat, 24 Jul 2021 00:57:21 +0800 Subject: [PATCH 10/12] Update Localizable.strings --- .../zh-Hans.lproj/Localizable.strings | Bin 2750 -> 2168 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Translations/zh-Hans.lproj/Localizable.strings b/Translations/zh-Hans.lproj/Localizable.strings index 561b2bd694bc9167ce61b495f40c75ecc2beb5fd..08f3386f8268b0a8066d8d96bcfa1e5d1fa35550 100644 GIT binary patch literal 2168 zcmZ`)TTdHD6n^Jdj4a&trATStsH&O*X{jJYV1z2=Ww8f)h1s=s*ATxM6v8cpmJkTw za0>*QU>lN9jEzC+Pf0zqyHEKGJ#%Ijn}@!D_I%&D%{k|DOG{|D=V6HaNbvs;EurqT zZkusxqs`xX8WOhgw38(d=wpWrr4iuI59{Sc{`yeBpL6>p93kJr9|ZCHUMpI@YmGE0 z4HS*Pn;DDT*CtF$cT~c9ZP(p=!t*(I?o9X%C(W;IGO0PSq;4dB!T{ds6w4pG`Nc0+ zJAyN0Q_IlCDT40mF`v6&e=G?knoZMoQJP^oL?2J5=r}bT8YfxmkoIrT0Z{j>%C;Bz z=5)Qh#8*rF*^~fqJL?v%g=uh7hZ;!+YC$sJ`ox#^ zWi^Or5(#QMx@kyHR;_r&A|g&EuA6qdD*v3-&e+TeTP}aO+C)4$)Ya1;p#IKBi6UfL z4jIYHgwI!4Wtp#Cva4mp^n?MO>ITv5U@-MszFw|`Bko4k-@wTZQfq=*WN4sQ2FWk6 zg*~=$%<`w~bq?c5MvpOSTH~5%;4ZU%v&W}Ol9{40hjePTL%Kw-MuYC>=K11ZeBmWu zz4nUN>|j=UP|}e~$$&eDFj79B1t*r1S(jJEOQ#(9BZR(&S%+amk(?3jU*%2)^Mh4TA#t~#&+fbsP+hLWrBJeE* zyrCmCcgccAngS7d2C}x50z^>;6$S6735ENqmvi;{ICbF z?p9y$?fk#FX%P}AILJ7i&|}mlqh`j42QQCwwp^-L4`HP2Y#Az&Q9pz*?$#^fZh%&jJ{s3G)w|G2_HLzK!(A0<)1pZ!6<0O)9uvpl zj%|HJPw9?)JQ%%Kyz$qIJa;4*;mBhQEdJwXv!sEfM&!5pNe5Z_=Yli~@1qcszzX6k UgsL61H~f8TGZ@?Vv7y6}*c{8`A&FqsC z_Z-bB9+&9e@KldKB*$AM%}4A3-BrwXj>WAlkjZixF-_2V;Fqe=w70ImB35k#qESTV z#gx>m`+%&Jy|N0_C84i-7$BSRQTIwLR^apfm1s_)#u~()ry+ekWK#OBRQ*u4mt^Hx z&q1*Shbp5!1hQ6l{5cq)Sw2wf9pm+9oQs>-@szEHWg5FRj(FP;MZblYMFy2}%h!5}|04G8z3Jn{P+gkV$<*g8V%2=rZBIcpq6?RNkL?x9vU>VHZ8#A~xvikaADUzRO)W9Xpzf)8Tei8B(nd$$ASEY0&1?$B+%!%qDQ;mZ?zb`HDCRSTh#Y?UQm;UM1 aJ&JYiGw+`}BNC_o2FoDwd;Yxhc Date: Sat, 24 Jul 2021 21:49:27 +0900 Subject: [PATCH 11/12] fixed #96, #102 --- JellyfinPlayer/ItemView.swift | 3 +- JellyfinPlayer/JellyfinPlayerApp.swift | 11 + JellyfinPlayer/VideoPlayer.swift | 399 +++++++++++++------------ 3 files changed, 217 insertions(+), 196 deletions(-) diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index c0767486..a60937cc 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -34,7 +34,7 @@ struct ItemView: View { .statusBar(hidden: true) .edgesIgnoringSafeArea(.all) .prefersHomeIndicatorAutoHidden(true) - }.supportedOrientations(.landscape), isActive: $videoPlayerItem.shouldShowPlayer) { + }, isActive: $videoPlayerItem.shouldShowPlayer) { EmptyView() } VStack { @@ -56,7 +56,6 @@ struct ItemView: View { .navigationBarHidden(false) .navigationBarBackButtonHidden(false) .environmentObject(videoPlayerItem) - .supportedOrientations(.all) } } } diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift index f543fa21..22c2114a 100644 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/JellyfinPlayerApp.swift @@ -209,6 +209,8 @@ class EmailHelper: NSObject, MFMailComposeViewControllerDelegate { @main struct JellyfinPlayerApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + let persistenceController = PersistenceController.shared var body: some Scene { @@ -224,3 +226,12 @@ struct JellyfinPlayerApp: App { } } } + +class AppDelegate: NSObject, UIApplicationDelegate { + + static var orientationLock = UIInterfaceOrientationMask.all + + func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + AppDelegate.orientationLock + } +} diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 7434223c..e022b8b9 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -26,12 +26,12 @@ protocol PlayerViewControllerDelegate: AnyObject { } class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener { - + weak var delegate: PlayerViewControllerDelegate? - + var cancellables = Set() var mediaPlayer = VLCMediaPlayer() - + @IBOutlet weak var upNextView: UIView! @IBOutlet weak var timeText: UILabel! @IBOutlet weak var videoContentView: UIView! @@ -42,19 +42,19 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBOutlet weak var jumpForwardButton: UIButton! @IBOutlet weak var playerSettingsButton: UIButton! @IBOutlet weak var castButton: UIButton! - + var shouldShowLoadingScreen: Bool = false var ssTargetValueOffset: Int = 0 var ssStartValue: Int = 0 var optionsVC: VideoPlayerSettingsView? var castDeviceVC: VideoPlayerCastDeviceSelectorView? - + var paused: Bool = true var lastTime: Float = 0.0 var startTime: Int = 0 var controlsAppearTime: Double = 0 var isSeeking: Bool = false - + var playerDestination: PlayerDestination = .local var discoveredCastDevices: [GCKDevice] = [] var selectedCastDevice: GCKDevice? @@ -67,7 +67,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe return GCKCastContext.sharedInstance().sessionManager } var hasSentRemoteSeek: Bool = false - + var selectedPlaybackSpeedIndex: Int = 3 var selectedAudioTrack: Int32 = -1 var selectedCaptionTrack: Int32 = -1 @@ -76,13 +76,13 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe var subtitleTrackArray: [Subtitle] = [] var audioTrackArray: [AudioTrack] = [] let playbackSpeeds: [Float] = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] - + var manifest: BaseItemDto = BaseItemDto() var playbackItem = PlaybackItem() var remoteTimeUpdateTimer: Timer? var upNextViewModel: UpNextViewModel = UpNextViewModel() - var lastOri: UIDeviceOrientation! - + var lastOri: UIInterfaceOrientation! + // MARK: IBActions @IBAction func seekSliderStart(_ sender: Any) { if playerDestination == .local { @@ -92,7 +92,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe isSeeking = true } } - + @IBAction func seekSliderValueChanged(_ sender: Any) { let videoDuration: Double = Double(manifest.runTimeTicks! / Int64(10_000_000)) let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) @@ -107,7 +107,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe timeText.text = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))" } } - + @IBAction func seekSliderEnd(_ sender: Any) { isSeeking = false let videoPosition = playerDestination == .local ? Double(mediaPlayer.time.intValue / 1000) : Double(remotePositionTicks / Int(10_000_000)) @@ -115,7 +115,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Scrub is value from 0..1 - find position in video and add / or remove. let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) let offset = secondsScrubbedTo - videoPosition - + if playerDestination == .local { if offset > 0 { mediaPlayer.jumpForward(Int32(offset)) @@ -130,18 +130,18 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe ]) } } - + @IBAction func exitButtonPressed(_ sender: Any) { sendStopReport() mediaPlayer.stop() - + if castSessionManager.hasConnectedCastSession() { castSessionManager.endSessionAndStopCasting(true) } - + delegate?.exitPlayer(self) } - + @IBAction func controlViewTapped(_ sender: Any) { if playerDestination == .local { videoControlsView.isHidden = true @@ -150,14 +150,14 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + @IBAction func contentViewTapped(_ sender: Any) { if playerDestination == .local { videoControlsView.isHidden = false controlsAppearTime = CACurrentMediaTime() } } - + @IBAction func jumpBackTapped(_ sender: Any) { if paused == false { if playerDestination == .local { @@ -167,7 +167,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + @IBAction func jumpForwardTapped(_ sender: Any) { if paused == false { if playerDestination == .local { @@ -177,7 +177,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + @IBOutlet weak var mainActionButton: UIButton! @IBAction func mainActionButtonPressed(_ sender: Any) { if paused { @@ -202,14 +202,14 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + @IBAction func settingsButtonTapped(_ sender: UIButton) { optionsVC = VideoPlayerSettingsView() optionsVC?.delegate = self - + optionsVC?.modalPresentationStyle = .popover optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton - + // Present the view controller (in a popover). self.present(optionsVC!, animated: true) { print("popover visible, pause playback") @@ -217,17 +217,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) } } - + // MARK: Cast methods @IBAction func castButtonPressed(_ sender: Any) { if selectedCastDevice == nil { LogManager.shared.log.debug("Presenting Cast modal") castDeviceVC = VideoPlayerCastDeviceSelectorView() castDeviceVC?.delegate = self - + castDeviceVC?.modalPresentationStyle = .popover castDeviceVC?.popoverPresentationController?.sourceView = castButton - + // Present the view controller (in a popover). self.present(castDeviceVC!, animated: true) { self.mediaPlayer.pause() @@ -242,7 +242,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe playerDestination = .local } } - + func castPopoverDismissed() { LogManager.shared.log.debug("Cast modal dismissed") castDeviceVC?.dismiss(animated: true, completion: nil) @@ -251,7 +251,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } - + func castDeviceChanged() { LogManager.shared.log.debug("Cast device changed") if selectedCastDevice != nil { @@ -261,7 +261,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castSessionManager.startSession(with: selectedCastDevice!) } } - + // MARK: Cast End func settingsPopoverDismissed() { optionsVC?.dismiss(animated: true, completion: nil) @@ -270,7 +270,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } } - + func setupNowPlayingCC() { let commandCenter = MPRemoteCommandCenter.shared() commandCenter.playCommand.isEnabled = true @@ -278,7 +278,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe commandCenter.seekForwardCommand.isEnabled = true commandCenter.seekBackwardCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.isEnabled = true - + // Add handler for Pause Command commandCenter.pauseCommand.addTarget { _ in if self.playerDestination == .local { @@ -290,7 +290,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) return .success } - + // Add handler for Play command commandCenter.playCommand.addTarget { _ in if self.playerDestination == .local { @@ -302,7 +302,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) return .success } - + // Add handler for FF command commandCenter.seekForwardCommand.addTarget { _ in if self.playerDestination == .local { @@ -313,7 +313,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } return .success } - + // Add handler for RW command commandCenter.seekBackwardCommand.addTarget { _ in if self.playerDestination == .local { @@ -324,17 +324,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } return .success } - + // Scrubber commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in guard let self = self else {return .commandFailed} - + if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent { let targetSeconds = event.positionTime - + let videoPosition = Double(self.mediaPlayer.time.intValue) let offset = targetSeconds - videoPosition - + if self.playerDestination == .local { if offset > 0 { self.mediaPlayer.jumpForward(Int32(offset)/1000) @@ -343,34 +343,34 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } self.sendProgressReport(eventName: "unpause") } else { - + } - + return .success } else { return .commandFailed } } - + var nowPlayingInfo = [String: Any]() - + var runTicks = 0 var playbackTicks = 0 - + if let ticks = manifest.runTimeTicks { runTicks = Int(ticks / 10_000_000) } - + if let ticks = manifest.userData?.playbackPositionTicks { playbackTicks = Int(ticks / 10_000_000) } - + nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video" nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks - + if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) { if let artworkImage = UIImage(data: imageData as Data) { let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in @@ -379,33 +379,41 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork } } - + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - + UIApplication.shared.beginReceivingRemoteControlEvents() } - + // MARK: viewDidLoad override func viewDidLoad() { + super.viewDidLoad() if manifest.type == "Movie" { titleLabel.text = manifest.name ?? "" } else { titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0)) “\(manifest.name ?? "")”" - + setupNextUpView() upNextViewModel.delegate = self } - - lastOri = UIDevice.current.orientation - - if !UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat { - let value = UIInterfaceOrientation.landscapeRight.rawValue - UIDevice.current.setValue(value, forKey: "orientation") - UIViewController.attemptRotationToDeviceOrientation() + + DispatchQueue.main.async { + self.lastOri = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation + AppDelegate.orientationLock = .landscape + + if !self.lastOri.isLandscape { + UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation") + UIViewController.attemptRotationToDeviceOrientation() + } } - super.viewDidLoad() + + NotificationCenter.default.addObserver(self, selector: #selector(didChangedOrientation), name: UIDevice.orientationDidChangeNotification, object: nil) } - + + @objc func didChangedOrientation() { + lastOri = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation + } + func mediaHasStartedPlaying() { castButton.isHidden = true let discoveryCriteria = GCKDiscoveryCriteria(applicationID: "F007D354") @@ -415,7 +423,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castDiscoveryManager.add(self) castDiscoveryManager.startDiscovery() } - + func didUpdateDeviceList() { let totalDevices = castDiscoveryManager.deviceCount discoveredCastDevices = [] @@ -425,7 +433,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe discoveredCastDevices.append(device) } } - + if !discoveredCastDevices.isEmpty { castButton.isHidden = false castButton.isEnabled = true @@ -436,34 +444,37 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castButton.setImage(nil, for: .normal) } } - + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) self.tabBarController?.tabBar.isHidden = false self.navigationController?.isNavigationBarHidden = false overrideUserInterfaceStyle = .unspecified - UIDevice.current.setValue(lastOri.rawValue, forKey: "orientation") - UIViewController.attemptRotationToDeviceOrientation() - super.viewWillDisappear(animated) + DispatchQueue.main.async { + AppDelegate.orientationLock = .all + UIDevice.current.setValue(self.lastOri.rawValue, forKey: "orientation") + UIViewController.attemptRotationToDeviceOrientation() + } } - + // MARK: viewDidAppear override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) overrideUserInterfaceStyle = .dark self.tabBarController?.tabBar.isHidden = true self.navigationController?.isNavigationBarHidden = true - + mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14) // mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate") - + mediaPlayer.delegate = self mediaPlayer.drawable = videoContentView - + setupMediaPlayer() } - + func setupMediaPlayer() { - + // Fetch max bitrate from UserDefaults depending on current connection mode let maxBitrate = Defaults[.inNetworkBandwidth] print(maxBitrate) @@ -473,25 +484,25 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let profile = builder.buildProfile() dump(profile) let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) - + DispatchQueue.global(qos: .userInitiated).async { [self] in delegate?.showLoadingView(self) MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) .sink(receiveCompletion: { completion in switch completion { - case .finished: - break - case .failure(let error): - if let err = error as? ErrorResponse { - switch err { - case .error(401, _, _, _): - self.delegate?.exitPlayer(self) - SessionManager.current.logout() - case .error: - self.delegate?.exitPlayer(self) - } + case .finished: + break + case .failure(let error): + if let err = error as? ErrorResponse { + switch err { + case .error(401, _, _, _): + self.delegate?.exitPlayer(self) + SessionManager.current.logout() + case .error: + self.delegate?.exitPlayer(self) } - break + } + break } }, receiveValue: { [self] response in playSessionId = response.playSessionId ?? "" @@ -503,10 +514,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let item = PlaybackItem() item.videoType = .transcode item.videoUrl = streamURL! - + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) - + // Loop through media streams and add to array for stream in mediaSource.mediaStreams ?? [] { if stream.type == .subtitle { @@ -517,12 +528,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe deliveryUrl = nil } let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") - + if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) } } - + if stream.type == .audio { let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { @@ -531,26 +542,26 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe audioTrackArray.append(subtitle) } } - + if selectedAudioTrack == -1 { if audioTrackArray.count > 0 { selectedAudioTrack = audioTrackArray[0].id } } - + self.sendPlayReport() playbackItem = item } else { // Item will be directly played by the client. let streamURL: URL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")! - + let item = PlaybackItem() item.videoUrl = streamURL item.videoType = .directPlay - + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) - + // Loop through media streams and add to array for stream in mediaSource.mediaStreams ?? [] { if stream.type == .subtitle { @@ -561,12 +572,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe deliveryUrl = nil } let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!, languageCode: stream.language ?? "") - + if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) } } - + if stream.type == .audio { let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { @@ -575,25 +586,25 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe audioTrackArray.append(subtitle) } } - + if selectedAudioTrack == -1 { if audioTrackArray.count > 0 { selectedAudioTrack = audioTrackArray[0].id } } - + self.sendPlayReport() playbackItem = item - + // self.setupNowPlayingCC() } - + startLocalPlaybackEngine(true) }) .store(in: &cancellables) } } - + func setupTracksForPreferredDefaults() { subtitleTrackArray.forEach { subtitle in if Defaults[.isAutoSelectSubtitles] { @@ -607,7 +618,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + audioTrackArray.forEach { audio in if audio.languageCode.contains(Defaults[.autoSelectAudioLangCode]) { selectedAudioTrack = audio.id @@ -615,12 +626,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + func startLocalPlaybackEngine(_ fetchCaptions: Bool) { mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) mediaPlayer.play() sendPlayReport() - + // 1 second = 10,000,000 ticks var startTicks: Int64 = 0 if remotePositionTicks == 0 { @@ -628,7 +639,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { startTicks = Int64(remotePositionTicks) } - + if startTicks != 0 { let videoPosition = Double(mediaPlayer.time.intValue / 1000) let secondsScrubbedTo = startTicks / 10_000_000 @@ -639,7 +650,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe mediaPlayer.jumpBackward(Int32(abs(offset))) } } - + if fetchCaptions { mediaPlayer.pause() subtitleTrackArray.forEach { sub in @@ -648,47 +659,47 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + self.mediaHasStartedPlaying() delegate?.hideLoadingView(self) - + videoContentView.setNeedsLayout() videoContentView.setNeedsDisplay() self.view.setNeedsLayout() self.view.setNeedsDisplay() self.videoControlsView.setNeedsLayout() self.videoControlsView.setNeedsDisplay() - + mediaPlayer.pause() mediaPlayer.play() setupTracksForPreferredDefaults() } - + // MARK: VideoPlayerSettings Delegate func subtitleTrackChanged(newTrackID: Int32) { selectedCaptionTrack = newTrackID mediaPlayer.currentVideoSubTitleIndex = newTrackID } - + func audioTrackChanged(newTrackID: Int32) { selectedAudioTrack = newTrackID mediaPlayer.currentAudioTrackIndex = newTrackID } - + func playbackSpeedChanged(index: Int) { selectedPlaybackSpeedIndex = index mediaPlayer.rate = playbackSpeeds[index] } - + func smallNextUpView() { UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn) { [self] in upNextViewModel.largeView = false } } - + func setupNextUpView() { getNextEpisode() - + // Create the swiftUI view let contentView = UIHostingController(rootView: VideoUpNextView(viewModel: upNextViewModel)) self.upNextView.addSubview(contentView.view) @@ -699,7 +710,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe contentView.view.leftAnchor.constraint(equalTo: upNextView.leftAnchor).isActive = true contentView.view.rightAnchor.constraint(equalTo: upNextView.rightAnchor).isActive = true } - + func getNextEpisode() { TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id, limit: 2) .sink(receiveCompletion: { completion in @@ -713,21 +724,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe }) .store(in: &cancellables) } - + func setPlayerToNextUp() { mediaPlayer.stop() - + ssTargetValueOffset = 0 ssStartValue = 0 - + paused = true lastTime = 0.0 startTime = 0 controlsAppearTime = 0 isSeeking = false - + remotePositionTicks = 0 - + selectedPlaybackSpeedIndex = 3 selectedAudioTrack = -1 selectedCaptionTrack = -1 @@ -735,22 +746,22 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe lastProgressReportTime = 0 subtitleTrackArray = [] audioTrackArray = [] - + manifest = upNextViewModel.item! playbackItem = PlaybackItem() - + upNextViewModel.item = nil - + upNextView.isHidden = true shouldShowLoadingScreen = true videoControlsView.isHidden = true - + titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0)) “\(manifest.name ?? "")”" - + setupMediaPlayer() getNextEpisode() } - + } // MARK: - GCKGenericChannelDelegate @@ -760,7 +771,7 @@ extension PlayerViewController: GCKGenericChannelDelegate { if !paused { remotePositionTicks = remotePositionTicks + 2_000_000; // add 0.2 secs every timer evt. } - + if isSeeking == false { let remainingTime = (manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000 let hours = remainingTime / 3600 @@ -773,12 +784,12 @@ extension PlayerViewController: GCKGenericChannelDelegate { timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))" } timeText.text = timeTextStr - + let playbackProgress = Float(remotePositionTicks) / Float(manifest.runTimeTicks!) seekSlider.setValue(playbackProgress, animated: true) } } - + func cast(_ channel: GCKGenericChannel, didReceiveTextMessage message: String, withNamespace protocolNamespace: String) { if let data = message.data(using: .utf8) { if let json = try? JSON(data: data) { @@ -802,7 +813,7 @@ extension PlayerViewController: GCKGenericChannelDelegate { } } } - + func sendJellyfinCommand(command: String, options: [String: Any]) { let payload: [String: Any] = [ "options": options, @@ -817,14 +828,14 @@ extension PlayerViewController: GCKGenericChannelDelegate { "subtitleBurnIn": false ] let jsonData = JSON(payload) - + jellyfinCastChannel?.sendTextMessage(jsonData.rawString()!, error: nil) - + if command == "Seek" { remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000) // Send playback report as Jellyfin Chromecast isn't smarter than a rock. let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false, positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - + PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) .sink(receiveCompletion: { result in print(result) @@ -841,20 +852,20 @@ extension PlayerViewController: GCKSessionManagerListener { func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) { self.sendStopReport() mediaPlayer.stop() - + playerDestination = .remote videoContentView.isHidden = true videoControlsView.isHidden = false castButton.setImage(UIImage(named: "CastConnected"), for: .normal) manager.currentCastSession?.start() - + jellyfinCastChannel!.delegate = self session.add(jellyfinCastChannel!) - + if let client = session.remoteMediaClient { client.add(self) } - + let playNowOptions: [String: Any] = [ "items": [[ "Id": self.manifest.id!, @@ -867,33 +878,33 @@ extension PlayerViewController: GCKSessionManagerListener { ] sendJellyfinCommand(command: "PlayNow", options: playNowOptions) } - + func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) { self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") self.sessionDidStart(manager: sessionManager, didStart: session) } - + func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) { self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") self.sessionDidStart(manager: sessionManager, didStart: session) } - + func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) { LogManager.shared.log.error((error as NSError).debugDescription) } - + func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKCastSession, withError error: Error?) { if error != nil { LogManager.shared.log.error((error! as NSError).debugDescription) } - + playerDestination = .local videoContentView.isHidden = false remoteTimeUpdateTimer?.invalidate() castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) startLocalPlaybackEngine(false) } - + func sessionManager(_ sessionManager: GCKSessionManager, didSuspend session: GCKCastSession, with reason: GCKConnectionSuspendReason) { playerDestination = .local videoContentView.isHidden = false @@ -906,37 +917,37 @@ extension PlayerViewController: GCKSessionManagerListener { // MARK: - VLCMediaPlayer Delegates extension PlayerViewController: VLCMediaPlayerDelegate { func mediaPlayerStateChanged(_ aNotification: Notification!) { - let currentState: VLCMediaPlayerState = mediaPlayer.state - switch currentState { - case .stopped : - LogManager.shared.log.debug("Player state changed: STOPPED") - break - case .ended : - LogManager.shared.log.debug("Player state changed: ENDED") - break - case .playing : - LogManager.shared.log.debug("Player state changed: PLAYING") - sendProgressReport(eventName: "unpause") - delegate?.hideLoadingView(self) - paused = false - case .paused : - LogManager.shared.log.debug("Player state changed: PAUSED") - paused = true - case .opening : - LogManager.shared.log.debug("Player state changed: OPENING") - case .buffering : - LogManager.shared.log.debug("Player state changed: BUFFERING") - delegate?.showLoadingView(self) - case .error : - LogManager.shared.log.error("Video had error.") - sendStopReport() - case .esAdded: - mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) - @unknown default: - break - } + let currentState: VLCMediaPlayerState = mediaPlayer.state + switch currentState { + case .stopped : + LogManager.shared.log.debug("Player state changed: STOPPED") + break + case .ended : + LogManager.shared.log.debug("Player state changed: ENDED") + break + case .playing : + LogManager.shared.log.debug("Player state changed: PLAYING") + sendProgressReport(eventName: "unpause") + delegate?.hideLoadingView(self) + paused = false + case .paused : + LogManager.shared.log.debug("Player state changed: PAUSED") + paused = true + case .opening : + LogManager.shared.log.debug("Player state changed: OPENING") + case .buffering : + LogManager.shared.log.debug("Player state changed: BUFFERING") + delegate?.showLoadingView(self) + case .error : + LogManager.shared.log.error("Video had error.") + sendStopReport() + case .esAdded: + mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) + @unknown default: + break + } } - + func mediaPlayerTimeChanged(_ aNotification: Notification!) { let time = mediaPlayer.position if abs(time-lastTime) > 0.00005 { @@ -944,7 +955,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate { mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) seekSlider.setValue(mediaPlayer.position, animated: true) delegate?.hideLoadingView(self) - + if manifest.type == "Episode" && upNextViewModel.item != nil { if time > 0.96 { upNextView.isHidden = false @@ -954,9 +965,9 @@ extension PlayerViewController: VLCMediaPlayerDelegate { self.jumpForwardButton.isHidden = false } } - + timeText.text = String(mediaPlayer.remainingTime.stringValue.dropFirst()) - + if CACurrentMediaTime() - controlsAppearTime > 5 { self.smallNextUpView() UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: { @@ -969,7 +980,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate { } lastTime = time } - + if CACurrentMediaTime() - lastProgressReportTime > 5 { mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack sendProgressReport(eventName: "timeupdate") @@ -982,36 +993,36 @@ extension PlayerViewController: VLCMediaPlayerDelegate { struct VLCPlayerWithControls: UIViewControllerRepresentable { var item: BaseItemDto @Environment(\.presentationMode) var presentationMode - + var loadBinding: Binding var pBinding: Binding - + class Coordinator: NSObject, PlayerViewControllerDelegate { let loadBinding: Binding let pBinding: Binding - + init(loadBinding: Binding, pBinding: Binding) { self.loadBinding = loadBinding self.pBinding = pBinding } - + func hideLoadingView(_ viewController: PlayerViewController) { self.loadBinding.wrappedValue = false } - + func showLoadingView(_ viewController: PlayerViewController) { self.loadBinding.wrappedValue = true } - + func exitPlayer(_ viewController: PlayerViewController) { self.pBinding.wrappedValue = false } } - + func makeCoordinator() -> Coordinator { Coordinator(loadBinding: self.loadBinding, pBinding: self.pBinding) } - + typealias UIViewControllerType = PlayerViewController func makeUIViewController(context: UIViewControllerRepresentableContext) -> VLCPlayerWithControls.UIViewControllerType { let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil) @@ -1020,7 +1031,7 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable { customViewController.delegate = context.coordinator return customViewController } - + func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType, context: UIViewControllerRepresentableContext) { } } @@ -1030,7 +1041,7 @@ extension PlayerViewController { func sendProgressReport(eventName: String) { if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - + PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) .sink(receiveCompletion: { result in print(result) @@ -1040,10 +1051,10 @@ extension PlayerViewController { .store(in: &cancellables) } } - + func sendStopReport() { let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: []) - + PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo) .sink(receiveCompletion: { result in print(result) @@ -1052,14 +1063,14 @@ extension PlayerViewController { }) .store(in: &cancellables) } - + func sendPlayReport() { startTime = Int(Date().timeIntervalSince1970) * 10_000_000 - + print("sending play report!") - + let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - + PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo) .sink(receiveCompletion: { result in print(result) From 61e6893cd60be36bb43e58e5b0a8114662ec22d0 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sat, 24 Jul 2021 21:55:41 +0900 Subject: [PATCH 12/12] lint autocorrect --- JellyfinPlayer/JellyfinPlayerApp.swift | 4 +- JellyfinPlayer/LibrarySearchView.swift | 14 +- JellyfinPlayer/VideoPlayer.swift | 290 ++++++++++++------------- 3 files changed, 154 insertions(+), 154 deletions(-) diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift index 22c2114a..fddc0e4c 100644 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/JellyfinPlayerApp.swift @@ -210,7 +210,7 @@ class EmailHelper: NSObject, MFMailComposeViewControllerDelegate { @main struct JellyfinPlayerApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - + let persistenceController = PersistenceController.shared var body: some Scene { @@ -228,7 +228,7 @@ struct JellyfinPlayerApp: App { } class AppDelegate: NSObject, UIApplicationDelegate { - + static var orientationLock = UIInterfaceOrientationMask.all func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index c893f9d3..65b63b34 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -12,13 +12,13 @@ import SwiftUI struct LibrarySearchView: View { @StateObject var viewModel: LibrarySearchViewModel @State var searchQuery = "" - + @State private var tracks: [GridItem] = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125) - + func recalcTracks() { tracks = Array(repeating: .init(.flexible()), count: Int(UIScreen.main.bounds.size.width) / 125) } - + var body: some View { ZStack { VStack { @@ -40,7 +40,7 @@ struct LibrarySearchView: View { } .navigationBarTitle("Search", displayMode: .inline) } - + var suggestionsListView: some View { ScrollView { LazyVStack(spacing: 8) { @@ -61,7 +61,7 @@ struct LibrarySearchView: View { .padding(.horizontal, 16) } } - + var resultView: some View { let items = items(for: viewModel.selectedItemType) return VStack(alignment: .leading, spacing: 16) { @@ -90,7 +90,7 @@ struct LibrarySearchView: View { recalcTracks() } } - + func items(for type: ItemType) -> [BaseItemDto] { switch type { case .episode: @@ -106,7 +106,7 @@ struct LibrarySearchView: View { } private extension ItemType { - + var localized: String { switch self { case .episode: diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index e022b8b9..69a02cec 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -26,12 +26,12 @@ protocol PlayerViewControllerDelegate: AnyObject { } class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener { - + weak var delegate: PlayerViewControllerDelegate? - + var cancellables = Set() var mediaPlayer = VLCMediaPlayer() - + @IBOutlet weak var upNextView: UIView! @IBOutlet weak var timeText: UILabel! @IBOutlet weak var videoContentView: UIView! @@ -42,19 +42,19 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBOutlet weak var jumpForwardButton: UIButton! @IBOutlet weak var playerSettingsButton: UIButton! @IBOutlet weak var castButton: UIButton! - + var shouldShowLoadingScreen: Bool = false var ssTargetValueOffset: Int = 0 var ssStartValue: Int = 0 var optionsVC: VideoPlayerSettingsView? var castDeviceVC: VideoPlayerCastDeviceSelectorView? - + var paused: Bool = true var lastTime: Float = 0.0 var startTime: Int = 0 var controlsAppearTime: Double = 0 var isSeeking: Bool = false - + var playerDestination: PlayerDestination = .local var discoveredCastDevices: [GCKDevice] = [] var selectedCastDevice: GCKDevice? @@ -67,7 +67,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe return GCKCastContext.sharedInstance().sessionManager } var hasSentRemoteSeek: Bool = false - + var selectedPlaybackSpeedIndex: Int = 3 var selectedAudioTrack: Int32 = -1 var selectedCaptionTrack: Int32 = -1 @@ -76,13 +76,13 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe var subtitleTrackArray: [Subtitle] = [] var audioTrackArray: [AudioTrack] = [] let playbackSpeeds: [Float] = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] - + var manifest: BaseItemDto = BaseItemDto() var playbackItem = PlaybackItem() var remoteTimeUpdateTimer: Timer? var upNextViewModel: UpNextViewModel = UpNextViewModel() var lastOri: UIInterfaceOrientation! - + // MARK: IBActions @IBAction func seekSliderStart(_ sender: Any) { if playerDestination == .local { @@ -92,7 +92,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe isSeeking = true } } - + @IBAction func seekSliderValueChanged(_ sender: Any) { let videoDuration: Double = Double(manifest.runTimeTicks! / Int64(10_000_000)) let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) @@ -107,7 +107,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe timeText.text = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))" } } - + @IBAction func seekSliderEnd(_ sender: Any) { isSeeking = false let videoPosition = playerDestination == .local ? Double(mediaPlayer.time.intValue / 1000) : Double(remotePositionTicks / Int(10_000_000)) @@ -115,7 +115,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Scrub is value from 0..1 - find position in video and add / or remove. let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) let offset = secondsScrubbedTo - videoPosition - + if playerDestination == .local { if offset > 0 { mediaPlayer.jumpForward(Int32(offset)) @@ -130,18 +130,18 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe ]) } } - + @IBAction func exitButtonPressed(_ sender: Any) { sendStopReport() mediaPlayer.stop() - + if castSessionManager.hasConnectedCastSession() { castSessionManager.endSessionAndStopCasting(true) } - + delegate?.exitPlayer(self) } - + @IBAction func controlViewTapped(_ sender: Any) { if playerDestination == .local { videoControlsView.isHidden = true @@ -150,14 +150,14 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + @IBAction func contentViewTapped(_ sender: Any) { if playerDestination == .local { videoControlsView.isHidden = false controlsAppearTime = CACurrentMediaTime() } } - + @IBAction func jumpBackTapped(_ sender: Any) { if paused == false { if playerDestination == .local { @@ -167,7 +167,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + @IBAction func jumpForwardTapped(_ sender: Any) { if paused == false { if playerDestination == .local { @@ -177,7 +177,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + @IBOutlet weak var mainActionButton: UIButton! @IBAction func mainActionButtonPressed(_ sender: Any) { if paused { @@ -202,14 +202,14 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + @IBAction func settingsButtonTapped(_ sender: UIButton) { optionsVC = VideoPlayerSettingsView() optionsVC?.delegate = self - + optionsVC?.modalPresentationStyle = .popover optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton - + // Present the view controller (in a popover). self.present(optionsVC!, animated: true) { print("popover visible, pause playback") @@ -217,17 +217,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) } } - + // MARK: Cast methods @IBAction func castButtonPressed(_ sender: Any) { if selectedCastDevice == nil { LogManager.shared.log.debug("Presenting Cast modal") castDeviceVC = VideoPlayerCastDeviceSelectorView() castDeviceVC?.delegate = self - + castDeviceVC?.modalPresentationStyle = .popover castDeviceVC?.popoverPresentationController?.sourceView = castButton - + // Present the view controller (in a popover). self.present(castDeviceVC!, animated: true) { self.mediaPlayer.pause() @@ -242,7 +242,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe playerDestination = .local } } - + func castPopoverDismissed() { LogManager.shared.log.debug("Cast modal dismissed") castDeviceVC?.dismiss(animated: true, completion: nil) @@ -251,7 +251,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } - + func castDeviceChanged() { LogManager.shared.log.debug("Cast device changed") if selectedCastDevice != nil { @@ -261,7 +261,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castSessionManager.startSession(with: selectedCastDevice!) } } - + // MARK: Cast End func settingsPopoverDismissed() { optionsVC?.dismiss(animated: true, completion: nil) @@ -270,7 +270,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } } - + func setupNowPlayingCC() { let commandCenter = MPRemoteCommandCenter.shared() commandCenter.playCommand.isEnabled = true @@ -278,7 +278,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe commandCenter.seekForwardCommand.isEnabled = true commandCenter.seekBackwardCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.isEnabled = true - + // Add handler for Pause Command commandCenter.pauseCommand.addTarget { _ in if self.playerDestination == .local { @@ -290,7 +290,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) return .success } - + // Add handler for Play command commandCenter.playCommand.addTarget { _ in if self.playerDestination == .local { @@ -302,7 +302,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) return .success } - + // Add handler for FF command commandCenter.seekForwardCommand.addTarget { _ in if self.playerDestination == .local { @@ -313,7 +313,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } return .success } - + // Add handler for RW command commandCenter.seekBackwardCommand.addTarget { _ in if self.playerDestination == .local { @@ -324,17 +324,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } return .success } - + // Scrubber commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in guard let self = self else {return .commandFailed} - + if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent { let targetSeconds = event.positionTime - + let videoPosition = Double(self.mediaPlayer.time.intValue) let offset = targetSeconds - videoPosition - + if self.playerDestination == .local { if offset > 0 { self.mediaPlayer.jumpForward(Int32(offset)/1000) @@ -343,34 +343,34 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } self.sendProgressReport(eventName: "unpause") } else { - + } - + return .success } else { return .commandFailed } } - + var nowPlayingInfo = [String: Any]() - + var runTicks = 0 var playbackTicks = 0 - + if let ticks = manifest.runTimeTicks { runTicks = Int(ticks / 10_000_000) } - + if let ticks = manifest.userData?.playbackPositionTicks { playbackTicks = Int(ticks / 10_000_000) } - + nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video" nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks - + if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) { if let artworkImage = UIImage(data: imageData as Data) { let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in @@ -379,12 +379,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork } } - + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - + UIApplication.shared.beginReceivingRemoteControlEvents() } - + // MARK: viewDidLoad override func viewDidLoad() { super.viewDidLoad() @@ -392,28 +392,28 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe titleLabel.text = manifest.name ?? "" } else { titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0)) “\(manifest.name ?? "")”" - + setupNextUpView() upNextViewModel.delegate = self } - + DispatchQueue.main.async { self.lastOri = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation AppDelegate.orientationLock = .landscape - + if !self.lastOri.isLandscape { UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation") UIViewController.attemptRotationToDeviceOrientation() } } - + NotificationCenter.default.addObserver(self, selector: #selector(didChangedOrientation), name: UIDevice.orientationDidChangeNotification, object: nil) } - + @objc func didChangedOrientation() { lastOri = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation } - + func mediaHasStartedPlaying() { castButton.isHidden = true let discoveryCriteria = GCKDiscoveryCriteria(applicationID: "F007D354") @@ -423,7 +423,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castDiscoveryManager.add(self) castDiscoveryManager.startDiscovery() } - + func didUpdateDeviceList() { let totalDevices = castDiscoveryManager.deviceCount discoveredCastDevices = [] @@ -433,7 +433,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe discoveredCastDevices.append(device) } } - + if !discoveredCastDevices.isEmpty { castButton.isHidden = false castButton.isEnabled = true @@ -444,7 +444,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castButton.setImage(nil, for: .normal) } } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.tabBarController?.tabBar.isHidden = false @@ -456,25 +456,25 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe UIViewController.attemptRotationToDeviceOrientation() } } - + // MARK: viewDidAppear override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) overrideUserInterfaceStyle = .dark self.tabBarController?.tabBar.isHidden = true self.navigationController?.isNavigationBarHidden = true - + mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14) // mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate") - + mediaPlayer.delegate = self mediaPlayer.drawable = videoContentView - + setupMediaPlayer() } - + func setupMediaPlayer() { - + // Fetch max bitrate from UserDefaults depending on current connection mode let maxBitrate = Defaults[.inNetworkBandwidth] print(maxBitrate) @@ -484,7 +484,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let profile = builder.buildProfile() dump(profile) let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) - + DispatchQueue.global(qos: .userInitiated).async { [self] in delegate?.showLoadingView(self) MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) @@ -514,10 +514,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe let item = PlaybackItem() item.videoType = .transcode item.videoUrl = streamURL! - + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) - + // Loop through media streams and add to array for stream in mediaSource.mediaStreams ?? [] { if stream.type == .subtitle { @@ -528,12 +528,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe deliveryUrl = nil } let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") - + if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) } } - + if stream.type == .audio { let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { @@ -542,26 +542,26 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe audioTrackArray.append(subtitle) } } - + if selectedAudioTrack == -1 { if audioTrackArray.count > 0 { selectedAudioTrack = audioTrackArray[0].id } } - + self.sendPlayReport() playbackItem = item } else { // Item will be directly played by the client. let streamURL: URL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")! - + let item = PlaybackItem() item.videoUrl = streamURL item.videoType = .directPlay - + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) - + // Loop through media streams and add to array for stream in mediaSource.mediaStreams ?? [] { if stream.type == .subtitle { @@ -572,12 +572,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe deliveryUrl = nil } let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!, languageCode: stream.language ?? "") - + if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) } } - + if stream.type == .audio { let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { @@ -586,25 +586,25 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe audioTrackArray.append(subtitle) } } - + if selectedAudioTrack == -1 { if audioTrackArray.count > 0 { selectedAudioTrack = audioTrackArray[0].id } } - + self.sendPlayReport() playbackItem = item - + // self.setupNowPlayingCC() } - + startLocalPlaybackEngine(true) }) .store(in: &cancellables) } } - + func setupTracksForPreferredDefaults() { subtitleTrackArray.forEach { subtitle in if Defaults[.isAutoSelectSubtitles] { @@ -618,7 +618,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + audioTrackArray.forEach { audio in if audio.languageCode.contains(Defaults[.autoSelectAudioLangCode]) { selectedAudioTrack = audio.id @@ -626,12 +626,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + func startLocalPlaybackEngine(_ fetchCaptions: Bool) { mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) mediaPlayer.play() sendPlayReport() - + // 1 second = 10,000,000 ticks var startTicks: Int64 = 0 if remotePositionTicks == 0 { @@ -639,7 +639,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { startTicks = Int64(remotePositionTicks) } - + if startTicks != 0 { let videoPosition = Double(mediaPlayer.time.intValue / 1000) let secondsScrubbedTo = startTicks / 10_000_000 @@ -650,7 +650,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe mediaPlayer.jumpBackward(Int32(abs(offset))) } } - + if fetchCaptions { mediaPlayer.pause() subtitleTrackArray.forEach { sub in @@ -659,47 +659,47 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + self.mediaHasStartedPlaying() delegate?.hideLoadingView(self) - + videoContentView.setNeedsLayout() videoContentView.setNeedsDisplay() self.view.setNeedsLayout() self.view.setNeedsDisplay() self.videoControlsView.setNeedsLayout() self.videoControlsView.setNeedsDisplay() - + mediaPlayer.pause() mediaPlayer.play() setupTracksForPreferredDefaults() } - + // MARK: VideoPlayerSettings Delegate func subtitleTrackChanged(newTrackID: Int32) { selectedCaptionTrack = newTrackID mediaPlayer.currentVideoSubTitleIndex = newTrackID } - + func audioTrackChanged(newTrackID: Int32) { selectedAudioTrack = newTrackID mediaPlayer.currentAudioTrackIndex = newTrackID } - + func playbackSpeedChanged(index: Int) { selectedPlaybackSpeedIndex = index mediaPlayer.rate = playbackSpeeds[index] } - + func smallNextUpView() { UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn) { [self] in upNextViewModel.largeView = false } } - + func setupNextUpView() { getNextEpisode() - + // Create the swiftUI view let contentView = UIHostingController(rootView: VideoUpNextView(viewModel: upNextViewModel)) self.upNextView.addSubview(contentView.view) @@ -710,7 +710,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe contentView.view.leftAnchor.constraint(equalTo: upNextView.leftAnchor).isActive = true contentView.view.rightAnchor.constraint(equalTo: upNextView.rightAnchor).isActive = true } - + func getNextEpisode() { TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id, limit: 2) .sink(receiveCompletion: { completion in @@ -724,21 +724,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe }) .store(in: &cancellables) } - + func setPlayerToNextUp() { mediaPlayer.stop() - + ssTargetValueOffset = 0 ssStartValue = 0 - + paused = true lastTime = 0.0 startTime = 0 controlsAppearTime = 0 isSeeking = false - + remotePositionTicks = 0 - + selectedPlaybackSpeedIndex = 3 selectedAudioTrack = -1 selectedCaptionTrack = -1 @@ -746,22 +746,22 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe lastProgressReportTime = 0 subtitleTrackArray = [] audioTrackArray = [] - + manifest = upNextViewModel.item! playbackItem = PlaybackItem() - + upNextViewModel.item = nil - + upNextView.isHidden = true shouldShowLoadingScreen = true videoControlsView.isHidden = true - + titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0)) “\(manifest.name ?? "")”" - + setupMediaPlayer() getNextEpisode() } - + } // MARK: - GCKGenericChannelDelegate @@ -771,7 +771,7 @@ extension PlayerViewController: GCKGenericChannelDelegate { if !paused { remotePositionTicks = remotePositionTicks + 2_000_000; // add 0.2 secs every timer evt. } - + if isSeeking == false { let remainingTime = (manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000 let hours = remainingTime / 3600 @@ -784,12 +784,12 @@ extension PlayerViewController: GCKGenericChannelDelegate { timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))" } timeText.text = timeTextStr - + let playbackProgress = Float(remotePositionTicks) / Float(manifest.runTimeTicks!) seekSlider.setValue(playbackProgress, animated: true) } } - + func cast(_ channel: GCKGenericChannel, didReceiveTextMessage message: String, withNamespace protocolNamespace: String) { if let data = message.data(using: .utf8) { if let json = try? JSON(data: data) { @@ -813,7 +813,7 @@ extension PlayerViewController: GCKGenericChannelDelegate { } } } - + func sendJellyfinCommand(command: String, options: [String: Any]) { let payload: [String: Any] = [ "options": options, @@ -828,14 +828,14 @@ extension PlayerViewController: GCKGenericChannelDelegate { "subtitleBurnIn": false ] let jsonData = JSON(payload) - + jellyfinCastChannel?.sendTextMessage(jsonData.rawString()!, error: nil) - + if command == "Seek" { remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000) // Send playback report as Jellyfin Chromecast isn't smarter than a rock. let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false, positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - + PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) .sink(receiveCompletion: { result in print(result) @@ -852,20 +852,20 @@ extension PlayerViewController: GCKSessionManagerListener { func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) { self.sendStopReport() mediaPlayer.stop() - + playerDestination = .remote videoContentView.isHidden = true videoControlsView.isHidden = false castButton.setImage(UIImage(named: "CastConnected"), for: .normal) manager.currentCastSession?.start() - + jellyfinCastChannel!.delegate = self session.add(jellyfinCastChannel!) - + if let client = session.remoteMediaClient { client.add(self) } - + let playNowOptions: [String: Any] = [ "items": [[ "Id": self.manifest.id!, @@ -878,33 +878,33 @@ extension PlayerViewController: GCKSessionManagerListener { ] sendJellyfinCommand(command: "PlayNow", options: playNowOptions) } - + func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) { self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") self.sessionDidStart(manager: sessionManager, didStart: session) } - + func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) { self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") self.sessionDidStart(manager: sessionManager, didStart: session) } - + func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) { LogManager.shared.log.error((error as NSError).debugDescription) } - + func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKCastSession, withError error: Error?) { if error != nil { LogManager.shared.log.error((error! as NSError).debugDescription) } - + playerDestination = .local videoContentView.isHidden = false remoteTimeUpdateTimer?.invalidate() castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) startLocalPlaybackEngine(false) } - + func sessionManager(_ sessionManager: GCKSessionManager, didSuspend session: GCKCastSession, with reason: GCKConnectionSuspendReason) { playerDestination = .local videoContentView.isHidden = false @@ -947,7 +947,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate { break } } - + func mediaPlayerTimeChanged(_ aNotification: Notification!) { let time = mediaPlayer.position if abs(time-lastTime) > 0.00005 { @@ -955,7 +955,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate { mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) seekSlider.setValue(mediaPlayer.position, animated: true) delegate?.hideLoadingView(self) - + if manifest.type == "Episode" && upNextViewModel.item != nil { if time > 0.96 { upNextView.isHidden = false @@ -965,9 +965,9 @@ extension PlayerViewController: VLCMediaPlayerDelegate { self.jumpForwardButton.isHidden = false } } - + timeText.text = String(mediaPlayer.remainingTime.stringValue.dropFirst()) - + if CACurrentMediaTime() - controlsAppearTime > 5 { self.smallNextUpView() UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: { @@ -980,7 +980,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate { } lastTime = time } - + if CACurrentMediaTime() - lastProgressReportTime > 5 { mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack sendProgressReport(eventName: "timeupdate") @@ -993,36 +993,36 @@ extension PlayerViewController: VLCMediaPlayerDelegate { struct VLCPlayerWithControls: UIViewControllerRepresentable { var item: BaseItemDto @Environment(\.presentationMode) var presentationMode - + var loadBinding: Binding var pBinding: Binding - + class Coordinator: NSObject, PlayerViewControllerDelegate { let loadBinding: Binding let pBinding: Binding - + init(loadBinding: Binding, pBinding: Binding) { self.loadBinding = loadBinding self.pBinding = pBinding } - + func hideLoadingView(_ viewController: PlayerViewController) { self.loadBinding.wrappedValue = false } - + func showLoadingView(_ viewController: PlayerViewController) { self.loadBinding.wrappedValue = true } - + func exitPlayer(_ viewController: PlayerViewController) { self.pBinding.wrappedValue = false } } - + func makeCoordinator() -> Coordinator { Coordinator(loadBinding: self.loadBinding, pBinding: self.pBinding) } - + typealias UIViewControllerType = PlayerViewController func makeUIViewController(context: UIViewControllerRepresentableContext) -> VLCPlayerWithControls.UIViewControllerType { let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil) @@ -1031,7 +1031,7 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable { customViewController.delegate = context.coordinator return customViewController } - + func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType, context: UIViewControllerRepresentableContext) { } } @@ -1041,7 +1041,7 @@ extension PlayerViewController { func sendProgressReport(eventName: String) { if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - + PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) .sink(receiveCompletion: { result in print(result) @@ -1051,10 +1051,10 @@ extension PlayerViewController { .store(in: &cancellables) } } - + func sendStopReport() { let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: []) - + PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo) .sink(receiveCompletion: { result in print(result) @@ -1063,14 +1063,14 @@ extension PlayerViewController { }) .store(in: &cancellables) } - + func sendPlayReport() { startTime = Int(Date().timeIntervalSince1970) * 10_000_000 - + print("sending play report!") - + let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - + PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo) .sink(receiveCompletion: { result in print(result)