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)