From e6ede1280d31d77843b0889ee6d549f7181e0462 Mon Sep 17 00:00:00 2001 From: acvigue Date: Fri, 25 Jun 2021 18:46:43 +0000 Subject: [PATCH] [create-pull-request] automated change --- JellyfinPlayer tvOS/ConnectToServerView.swift | 7 +- .../VideoPlayer/AudioView.swift | 31 +- .../InfoTabBarViewController.swift | 52 +-- .../VideoPlayer/MediaInfoView.swift | 65 ++- .../VideoPlayer/SubtitlesView.swift | 38 +- .../VideoPlayer/VideoPlayer.swift | 8 +- .../VideoPlayerViewController.swift | 419 +++++++++--------- JellyfinPlayer/ConnectToServerView.swift | 4 +- JellyfinPlayer/ContinueWatchingView.swift | 10 +- JellyfinPlayer/HomeView.swift | 6 +- JellyfinPlayer/LatestMediaView.swift | 7 +- JellyfinPlayer/LibraryListView.swift | 20 +- JellyfinPlayer/LibrarySearchView.swift | 7 +- JellyfinPlayer/LibraryView.swift | 5 +- JellyfinPlayer/NextUpView.swift | 4 +- JellyfinPlayer/SeasonItemView.swift | 5 +- JellyfinPlayer/SettingsView.swift | 19 +- JellyfinPlayer/VideoPlayer.swift | 205 ++++----- .../VideoPlayerCastDeviceSelector.swift | 9 +- JellyfinPlayer/VideoPlayerSettingsView.swift | 2 +- Shared/Extensions/APIExtensions.swift | 4 +- Shared/ServerLocator/ServerDiscovery.swift | 20 +- .../UDPBroadCastConnection.swift | 135 +++--- Shared/Singleton/SessionManager.swift | 2 +- .../ViewModels/ConnectToServerViewModel.swift | 16 +- .../ViewModels/LibraryFilterViewModel.swift | 6 +- Shared/ViewModels/SettingsViewModel.swift | 2 +- WidgetExtension/NextUpWidget.swift | 8 +- 28 files changed, 524 insertions(+), 592 deletions(-) diff --git a/JellyfinPlayer tvOS/ConnectToServerView.swift b/JellyfinPlayer tvOS/ConnectToServerView.swift index 6bda55ec..998e3689 100644 --- a/JellyfinPlayer tvOS/ConnectToServerView.swift +++ b/JellyfinPlayer tvOS/ConnectToServerView.swift @@ -105,7 +105,7 @@ struct ConnectToServerView: View { } } else { if !viewModel.isLoading { - + Form { Section(header: Text("Server Information")) { TextField("Jellyfin Server URL", text: $uri) @@ -144,15 +144,14 @@ struct ConnectToServerView: View { Image(systemName: "chevron.forward") .padding() } - + }) .disabled(viewModel.isLoading) } } .onAppear(perform: self.viewModel.discoverServers) } - } - else { + } else { ProgressView() } } diff --git a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift index 66e3a035..a513c98a 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift @@ -10,19 +10,17 @@ import SwiftUI class AudioViewController: UIViewController { - - var height : CGFloat = 420 - + var height: CGFloat = 420 + override func viewDidLoad() { super.viewDidLoad() - + tabBarItem.title = "Audio" - + } - - func prepareAudioView(audioTracks: [AudioTrack], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) - { + + func prepareAudioView(audioTracks: [AudioTrack], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) { let contentView = UIHostingController(rootView: AudioView(selectedTrack: selectedTrack, audioTrackArray: audioTracks, delegate: delegate)) self.view.addSubview(contentView.view) contentView.view.translatesAutoresizingMaskIntoConstraints = false @@ -30,38 +28,37 @@ class AudioViewController: UIViewController { contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - + } } struct AudioView: View { - - @State var selectedTrack : Int32 = -1 + + @State var selectedTrack: Int32 = -1 @State var audioTrackArray: [AudioTrack] = [] - + weak var delegate: VideoPlayerSettingsDelegate? var body : some View { NavigationView { - VStack() { + VStack { List(audioTrackArray, id: \.id) { track in Button(action: { delegate?.selectNew(audioTrack: track.id) selectedTrack = track.id }, label: { - HStack(spacing: 10){ + HStack(spacing: 10) { if track.id == selectedTrack { Image(systemName: "checkmark") - } - else { + } else { Image(systemName: "checkmark") .hidden() } Text(track.name) } }) - + } } .frame(width: 400) diff --git a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift index c7cfd218..f8077693 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift @@ -11,15 +11,14 @@ import TVUIKit import JellyfinAPI class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate { - - var videoPlayer : VideoPlayerViewController? = nil - var subtitleViewController : SubtitlesViewController? = nil - var audioViewController : AudioViewController? = nil - var mediaInfoController : MediaInfoViewController? = nil - var infoContainerPos : CGRect? = nil - var tabBarHeight : CGFloat = 0 - + var videoPlayer: VideoPlayerViewController? + var subtitleViewController: SubtitlesViewController? + var audioViewController: AudioViewController? + var mediaInfoController: MediaInfoViewController? + var infoContainerPos: CGRect? + var tabBarHeight: CGFloat = 0 + // override func viewWillAppear(_ animated: Bool) { // tabBar.standardAppearance.backgroundColor = .clear // tabBar.standardAppearance.backgroundImage = UIImage() @@ -40,40 +39,38 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate mediaInfoController = MediaInfoViewController() audioViewController = AudioViewController() subtitleViewController = SubtitlesViewController() - + viewControllers = [mediaInfoController!, audioViewController!, subtitleViewController!] tabBarHeight = tabBar.frame.size.height - + tabBar.standardAppearance.backgroundColor = .clear tabBar.standardAppearance.backgroundImage = UIImage() tabBar.standardAppearance.backgroundEffect = .none tabBar.barTintColor = .clear - + } - - func setupInfoViews(mediaItem: BaseItemDto, subtitleTracks: [Subtitle], selectedSubtitleTrack : Int32, audioTracks: [AudioTrack], selectedAudioTrack: Int32, delegate: VideoPlayerSettingsDelegate) { - + + func setupInfoViews(mediaItem: BaseItemDto, subtitleTracks: [Subtitle], selectedSubtitleTrack: Int32, audioTracks: [AudioTrack], selectedAudioTrack: Int32, delegate: VideoPlayerSettingsDelegate) { + mediaInfoController?.setMedia(item: mediaItem) - + audioViewController?.prepareAudioView(audioTracks: audioTracks, selectedTrack: selectedAudioTrack, delegate: delegate) - + subtitleViewController?.prepareSubtitleView(subtitleTracks: subtitleTracks, selectedTrack: selectedSubtitleTrack, delegate: delegate) - + if let videoPlayer = videoPlayer { infoContainerPos = CGRect(x: 88, y: 87, width: videoPlayer.infoViewContainer.frame.width, height: videoPlayer.infoViewContainer.frame.height) - + } - - - + } - + override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { guard let pos = infoContainerPos else { return } - + switch item.title { case "Audio": if var height = audioViewController?.height { @@ -83,7 +80,6 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate } - } break case "Info": @@ -97,7 +93,7 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate } break case "Subtitles": - if var height = subtitleViewController?.height{ + if var height = subtitleViewController?.height { height += tabBarHeight UIView.animate(withDuration: 0.6, delay: 0, options: .curveEaseOut) { [self] in videoPlayer?.infoViewContainer.frame = CGRect(x: pos.minX, y: pos.minY, width: pos.width, height: height) @@ -110,13 +106,11 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate break } } - + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } - - - + // MARK: - Navigation // // In a storyboard-based application, you will often want to do a little preparation before navigation diff --git a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift index b210c572..68d952b7 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift @@ -12,18 +12,16 @@ import JellyfinAPI class MediaInfoViewController: UIViewController { private var contentView: UIHostingController! - - var height : CGFloat = 0 - - + + var height: CGFloat = 0 + override func viewDidLoad() { super.viewDidLoad() - + tabBarItem.title = "Info" } - - func setMedia(item: BaseItemDto) - { + + func setMedia(item: BaseItemDto) { contentView = UIHostingController(rootView: MediaInfoView(item: item)) self.view.addSubview(contentView.view) contentView.view.translatesAutoresizingMaskIntoConstraints = false @@ -31,40 +29,38 @@ class MediaInfoViewController: UIViewController { contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - + height = self.view.frame.height - + } } struct MediaInfoView: View { - @State var item : BaseItemDto? = nil - + @State var item: BaseItemDto? + var body: some View { if let item = item { HStack(spacing: 30) { - + VStack { ImageView(src: item.type == "Episode" ? item.getSeriesPrimaryImage(maxWidth: 200) : item.getPrimaryImage(maxWidth: 200), bh: item.type == "Episode" ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash()) .frame(width: 200, height: 300) .cornerRadius(10) Spacer() } - + VStack(alignment: .leading, spacing: 10) { if item.type == "Episode" { Text(item.seriesName ?? "Series") .fontWeight(.bold) - + Text(item.name ?? "Episode") .foregroundColor(.secondary) - } - else - { + } else { Text(item.name ?? "Movie") .fontWeight(.bold) } - + HStack(spacing: 10) { if item.type == "Episode" { Text("S\(item.parentIndexNumber ?? 0) • E\(item.indexNumber ?? 0)") @@ -73,56 +69,53 @@ struct MediaInfoView: View { Text("•") Text(formatDate(date: date)) } - + } else if let year = item.productionYear { Text(String(year)) } - + if item.runTimeTicks != nil { Text("•") Text(item.getItemRuntime()) } - + if let rating = item.officialRating { Text("•") - + Text("\(rating)").font(.subheadline) .fontWeight(.semibold) .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) .overlay(RoundedRectangle(cornerRadius: 2) .stroke(Color.secondary, lineWidth: 1)) - + } } .foregroundColor(.secondary) - + if let overview = item.overview { Text(overview) .padding(.top) .foregroundColor(.secondary) } - - + Spacer() } - + Spacer() - + } .padding(.leading, 350) .padding(.trailing, 125) - } - else { + } else { EmptyView() } - + } - - - func formatDate(date : Date) -> String{ + + func formatDate(date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "d MMM yyyy" - + return formatter.string(from: date) } } diff --git a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift index 1ea20c2c..4d43864d 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift @@ -10,19 +10,17 @@ import SwiftUI class SubtitlesViewController: UIViewController { - - var height : CGFloat = 420 - + var height: CGFloat = 420 + override func viewDidLoad() { super.viewDidLoad() - + tabBarItem.title = "Subtitles" - + } - - func prepareSubtitleView(subtitleTracks: [Subtitle], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) - { + + func prepareSubtitleView(subtitleTracks: [Subtitle], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) { let contentView = UIHostingController(rootView: SubtitleView(selectedTrack: selectedTrack, subtitleTrackArray: subtitleTracks, delegate: delegate)) self.view.addSubview(contentView.view) contentView.view.translatesAutoresizingMaskIntoConstraints = false @@ -30,44 +28,42 @@ class SubtitlesViewController: UIViewController { contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - + } } struct SubtitleView: View { - - @State var selectedTrack : Int32 = -1 + + @State var selectedTrack: Int32 = -1 @State var subtitleTrackArray: [Subtitle] = [] - + weak var delegate: VideoPlayerSettingsDelegate? - - + var body : some View { NavigationView { - VStack() { + VStack { List(subtitleTrackArray, id: \.id) { track in Button(action: { delegate?.selectNew(subtitleTrack: track.id) selectedTrack = track.id }, label: { - HStack(spacing: 10){ + HStack(spacing: 10) { if track.id == selectedTrack { Image(systemName: "checkmark") - } - else { + } else { Image(systemName: "checkmark") .hidden() } Text(track.name) } }) - + } } .frame(width: 400) .frame(maxHeight: 400) - + } } - + } diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift index bc007e0c..d2017909 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift @@ -12,17 +12,17 @@ import JellyfinAPI struct VideoPlayerView: UIViewControllerRepresentable { var item: BaseItemDto - + func makeUIViewController(context: Context) -> some UIViewController { - + let storyboard = UIStoryboard(name: "VideoPlayerStoryboard", bundle: nil) let viewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! VideoPlayerViewController viewController.manifest = item return viewController } - + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { - + } } diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 12d8a4af..1e2ff466 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -19,37 +19,36 @@ protocol VideoPlayerSettingsDelegate: AnyObject { func selectNew(subtitleTrack id: Int32) } -class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, VLCMediaPlayerDelegate, VLCMediaDelegate, UIGestureRecognizerDelegate { - +class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, VLCMediaPlayerDelegate, VLCMediaDelegate, UIGestureRecognizerDelegate { @IBOutlet weak var videoContentView: UIView! @IBOutlet weak var controlsView: UIView! - + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! - + @IBOutlet weak var transportBarView: UIView! @IBOutlet weak var scrubberView: UIView! @IBOutlet weak var scrubLabel: UILabel! @IBOutlet weak var gradientView: UIView! - + @IBOutlet weak var currentTimeLabel: UILabel! @IBOutlet weak var remainingTimeLabel: UILabel! - + @IBOutlet weak var infoViewContainer: UIView! - - var infoPanelDisplayPoint : CGPoint = .zero - var infoPanelHiddenPoint : CGPoint = .zero - + + var infoPanelDisplayPoint: CGPoint = .zero + var infoPanelHiddenPoint: CGPoint = .zero + var containerViewController: InfoTabBarViewController? - var focusedOnTabBar : Bool = false - var showingInfoPanel : Bool = false - + var focusedOnTabBar: Bool = false + var showingInfoPanel: Bool = false + var mediaPlayer = VLCMediaPlayer() - + var lastProgressReportTime: Double = 0 var lastTime: Float = 0.0 var startTime: Int = 0 - + var selectedAudioTrack: Int32 = -1 { didSet { print(selectedAudioTrack) @@ -60,73 +59,68 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, print(selectedCaptionTrack) } } - + var subtitleTrackArray: [Subtitle] = [] var audioTrackArray: [AudioTrack] = [] - + var playing: Bool = false var seeking: Bool = false var showingControls: Bool = false var loading: Bool = true - - var initialSeekPos : CGFloat = 0 + + var initialSeekPos: CGFloat = 0 var videoPos: Double = 0 var videoDuration: Double = 0 var controlsAppearTime: Double = 0 - - + var manifest: BaseItemDto = BaseItemDto() var playbackItem = PlaybackItem() var playSessionId: String = "" - + var cancellables = Set() - - + override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { - + super.didUpdateFocus(in: context, with: coordinator) - + // Check if focused on the tab bar, allows for swipe up to dismiss the info panel - if context.nextFocusedView!.description.contains("UITabBarButton") - { + if context.nextFocusedView!.description.contains("UITabBarButton") { // Set value after half a second so info panel is not dismissed instantly when swiping up from content DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.focusedOnTabBar = true } - } - else - { + } else { focusedOnTabBar = false } - + } - + override func viewDidLoad() { super.viewDidLoad() - + activityIndicator.isHidden = false activityIndicator.startAnimating() - + mediaPlayer.delegate = self mediaPlayer.drawable = videoContentView - + if let runTimeTicks = manifest.runTimeTicks { videoDuration = Double(runTimeTicks / 10_000_000) } - + // Black gradient behind transport bar - let gradientLayer:CAGradientLayer = CAGradientLayer() + let gradientLayer: CAGradientLayer = CAGradientLayer() gradientLayer.frame.size = self.gradientView.frame.size gradientLayer.colors = [UIColor.black.withAlphaComponent(0.6).cgColor, UIColor.black.withAlphaComponent(0).cgColor] gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0) gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.0) self.gradientView.layer.addSublayer(gradientLayer) - + infoPanelDisplayPoint = infoViewContainer.center infoPanelHiddenPoint = CGPoint(x: infoPanelDisplayPoint.x, y: -infoViewContainer.frame.height) infoViewContainer.center = infoPanelHiddenPoint infoViewContainer.layer.cornerRadius = 40 - + let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) blurEffectView.frame = infoViewContainer.bounds blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] @@ -134,124 +128,122 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, blurEffectView.clipsToBounds = true infoViewContainer.addSubview(blurEffectView) infoViewContainer.sendSubviewToBack(blurEffectView) - + transportBarView.layer.cornerRadius = CGFloat(5) - + setupGestures() - + fetchVideo() - + setupNowPlayingCC() - + // Adjust subtitle size mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16) - + } - + func fetchVideo() { // Fetch max bitrate from UserDefaults depending on current connection mode let maxBitrate = Defaults[.inNetworkBandwidth] - + // Build a device profile let builder = DeviceProfileBuilder() builder.setMaxBitrate(bitrate: maxBitrate) let profile = builder.buildProfile() - + guard let currentUser = SessionManager.current.user else { return } - + let playbackInfo = PlaybackInfoDto(userId: currentUser.user_id ?? "", maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) - + DispatchQueue.global(qos: .userInitiated).async { [self] in MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: currentUser.user_id ?? "", maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) .sink(receiveCompletion: { result in print(result) }, receiveValue: { [self] response in - + videoContentView.setNeedsLayout() videoContentView.setNeedsDisplay() - + playSessionId = response.playSessionId ?? "" - + guard let mediaSource = response.mediaSources?.first.self else { return } - + let item = PlaybackItem() - let streamURL : URL - + let streamURL: URL + // Item is being transcoded by request of server if let transcodiungUrl = mediaSource.transcodingUrl { item.videoType = .transcode streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(transcodiungUrl)")! } // Item will be directly played by the client - else - { + else { item.videoType = .directPlay streamURL = 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!)")! } - + item.videoUrl = streamURL - + let disableSubtitleTrack = Subtitle(name: "None", 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 { - var deliveryUrl: URL? = nil - + var deliveryUrl: URL? + if stream.deliveryMethod == .external { deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! } - + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") - - if stream.isDefault == true{ + + if stream.isDefault == true { selectedCaptionTrack = Int32(stream.index!) } - + if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) } } - + if stream.type == .audio { let track = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) - + if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) } - + audioTrackArray.append(track) } } - + // If no default audio tracks select the first one if selectedAudioTrack == -1 && !audioTrackArray.isEmpty { selectedAudioTrack = audioTrackArray.first!.id } - - + self.sendPlayReport() playbackItem = item - + mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) mediaPlayer.media.delegate = self mediaPlayer.play() - + // 1 second = 10,000,000 ticks - + if let rawStartTicks = manifest.userData?.playbackPositionTicks { mediaPlayer.jumpForward(Int32(rawStartTicks / 10_000_000)) } - + // Pause and load captions into memory. mediaPlayer.pause() - + var shouldHaveSubtitleTracks = 0 subtitleTrackArray.forEach { sub in if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" { @@ -259,25 +251,24 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, mediaPlayer.addPlaybackSlave(sub.url!, type: .subtitle, enforce: false) } } - + // Wait for captions to load while mediaPlayer.numberOfSubtitlesTracks != shouldHaveSubtitleTracks {} - + // Select default track & resume playback mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack mediaPlayer.pause() mediaPlayer.play() playing = true - + setupInfoPanel() - + }) .store(in: &cancellables) - - + } } - + func setupNowPlayingCC() { let commandCenter = MPRemoteCommandCenter.shared() commandCenter.playCommand.isEnabled = true @@ -286,40 +277,40 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, commandCenter.seekBackwardCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.isEnabled = true commandCenter.enableLanguageOptionCommand.isEnabled = true - + // Add handler for Pause Command commandCenter.pauseCommand.addTarget { _ in self.pause() return .success } - + // Add handler for Play command commandCenter.playCommand.addTarget { _ in self.play() return .success } - + // Add handler for FF command commandCenter.seekForwardCommand.addTarget { _ in self.mediaPlayer.jumpForward(30) self.sendProgressReport(eventName: "timeupdate") return .success } - + // Add handler for RW command commandCenter.seekBackwardCommand.addTarget { _ in self.mediaPlayer.jumpBackward(15) self.sendProgressReport(eventName: "timeupdate") 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 offset > 0 { @@ -328,56 +319,55 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000) } self.sendProgressReport(eventName: "unpause") - + return .success } else { return .commandFailed } } - + // commandCenter.enableLanguageOptionCommand.addTarget { [weak self](remoteEvent) in // guard let self = self else {return .commandFailed} // // // // } - + 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) } - + var nowPlayingInfo = [String: Any]() - + nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video" nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.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: { (size) -> UIImage in + let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in return artworkImage }) nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork } } - + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - UIApplication.shared.beginReceivingRemoteControlEvents() } - - func updateNowPlayingCenter(time : Double?, playing : Bool?) { - + + func updateNowPlayingCenter(time: Double?, playing: Bool?) { + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]() if let playing = playing { @@ -386,64 +376,60 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, if let time = time { nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = time } - + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } - - - + // Grabs a refference to the info panel view controller override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "infoView" { containerViewController = segue.destination as? InfoTabBarViewController containerViewController?.videoPlayer = self - + } } - + // MARK: Player functions // Animate the scrubber when playing state changes func animateScrubber() { - let y : CGFloat = playing ? 0 : -20 + let y: CGFloat = playing ? 0 : -20 let height: CGFloat = playing ? 10 : 30 - + UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn, animations: { self.scrubberView.frame = CGRect(x: self.scrubberView.frame.minX, y: y, width: 2, height: height) }) } - - + func pause() { playing = false mediaPlayer.pause() - + self.sendProgressReport(eventName: "pause") - + self.updateNowPlayingCenter(time: nil, playing: false) - + animateScrubber() - - self.scrubLabel.frame = CGRect(x: self.scrubberView.frame.minX - self.scrubLabel.frame.width/2, y:self.scrubLabel.frame.minY, width: self.scrubLabel.frame.width, height: self.scrubLabel.frame.height) + + self.scrubLabel.frame = CGRect(x: self.scrubberView.frame.minX - self.scrubLabel.frame.width/2, y: self.scrubLabel.frame.minY, width: self.scrubLabel.frame.width, height: self.scrubLabel.frame.height) } - + func play () { playing = true mediaPlayer.play() - + self.updateNowPlayingCenter(time: nil, playing: true) self.sendProgressReport(eventName: "unpause") - + animateScrubber() } - - + func toggleInfoContainer() { showingInfoPanel.toggle() - + containerViewController?.view.isUserInteractionEnabled = showingInfoPanel - + if showingInfoPanel && seeking { scrubLabel.isHidden = true UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: { @@ -453,49 +439,48 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, self.scrubLabel.text = self.currentTimeLabel.text } seeking = false - + } - - UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut) { [self] in + + UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut) { [self] in infoViewContainer.center = showingInfoPanel ? infoPanelDisplayPoint : infoPanelHiddenPoint } - + } - + // MARK: Gestures func setupGestures() { - + let playPauseGesture = UITapGestureRecognizer(target: self, action: #selector(self.selectButtonTapped)) let playPauseType = UIPress.PressType.playPause - playPauseGesture.allowedPressTypes = [NSNumber(value: playPauseType.rawValue)]; + playPauseGesture.allowedPressTypes = [NSNumber(value: playPauseType.rawValue)] view.addGestureRecognizer(playPauseGesture) - + let selectGesture = UITapGestureRecognizer(target: self, action: #selector(self.selectButtonTapped)) let selectType = UIPress.PressType.select - selectGesture.allowedPressTypes = [NSNumber(value: selectType.rawValue)]; + selectGesture.allowedPressTypes = [NSNumber(value: selectType.rawValue)] view.addGestureRecognizer(selectGesture) - + let backTapGesture = UITapGestureRecognizer(target: self, action: #selector(self.backButtonPressed(tap:))) let backPress = UIPress.PressType.menu - backTapGesture.allowedPressTypes = [NSNumber(value: backPress.rawValue)]; + backTapGesture.allowedPressTypes = [NSNumber(value: backPress.rawValue)] view.addGestureRecognizer(backTapGesture) - + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.userPanned(panGestureRecognizer:))) view.addGestureRecognizer(panGestureRecognizer) - - + let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(self.swipe(swipe:))) swipeRecognizer.direction = .right view.addGestureRecognizer(swipeRecognizer) - + let swipeRecognizerl = UISwipeGestureRecognizer(target: self, action: #selector(self.swipe(swipe:))) swipeRecognizerl.direction = .left view.addGestureRecognizer(swipeRecognizerl) - + } - - @objc func backButtonPressed(tap : UITapGestureRecognizer) { - + + @objc func backButtonPressed(tap: UITapGestureRecognizer) { + // Dismiss info panel if showingInfoPanel { if focusedOnTabBar { @@ -503,75 +488,72 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } return } - + // Cancel seek and move back to initial position - if(seeking) { + if seeking { scrubLabel.isHidden = true UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: { self.scrubberView.frame = CGRect(x: self.initialSeekPos, y: 0, width: 2, height: 10) }) play() seeking = false - } - else - { + } else { // Dismiss view mediaPlayer.stop() sendStopReport() self.navigationController?.popViewController(animated: true) } } - - @objc func userPanned(panGestureRecognizer : UIPanGestureRecognizer) { + + @objc func userPanned(panGestureRecognizer: UIPanGestureRecognizer) { if loading { return } - + let translation = panGestureRecognizer.translation(in: view) let velocity = panGestureRecognizer.velocity(in: view) - + // Swiped up - Handle dismissing info panel if translation.y < -700 && (focusedOnTabBar && showingInfoPanel) { toggleInfoContainer() return } - + if showingInfoPanel { return } - + // Swiped down - Show the info panel if translation.y > 700 { toggleInfoContainer() return } - + // Ignore seek if video is playing if playing { return } - + // Save current position if seek is cancelled and show the scrubLabel - if(!seeking) { + if !seeking { initialSeekPos = self.scrubberView.frame.minX seeking = true self.scrubLabel.isHidden = false } - + let newPos = (self.scrubberView.frame.minX + velocity.x/100).clamped(to: 0...transportBarView.frame.width) - + UIView.animate(withDuration: 0.8, delay: 0, options: .curveEaseOut, animations: { let time = (Double(self.scrubberView.frame.minX) * self.videoDuration) / Double(self.transportBarView.frame.width) - + self.scrubberView.frame = CGRect(x: newPos, y: self.scrubberView.frame.minY, width: 2, height: 30) self.scrubLabel.frame = CGRect(x: (newPos - self.scrubLabel.frame.width/2), y: self.scrubLabel.frame.minY, width: self.scrubLabel.frame.width, height: self.scrubLabel.frame.height) self.scrubLabel.text = (self.formatSecondsToHMS(time)) - + }) - - + } - + // Not currently used @objc func swipe(swipe: UISwipeGestureRecognizer!) { print("swiped") @@ -593,53 +575,51 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, default: break } - + } - + /// Play/Pause or Select is pressed on the AppleTV remote @objc func selectButtonTapped() { if loading { return } - + showingControls = true controlsView.isHidden = false controlsAppearTime = CACurrentMediaTime() - - + // Move to seeked position - if(seeking) { + if seeking { scrubLabel.isHidden = true - + // Move current time to the scrubbed position UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: { [self] in - + self.currentTimeLabel.frame = CGRect(x: CGFloat(scrubLabel.frame.minX + transportBarView.frame.minX), y: currentTimeLabel.frame.minY, width: currentTimeLabel.frame.width, height: currentTimeLabel.frame.height) - + }) - + let time = (Double(self.scrubberView.frame.minX) * self.videoDuration) / Double(self.transportBarView.frame.width) - + self.currentTimeLabel.text = self.scrubLabel.text self.remainingTimeLabel.text = "-" + formatSecondsToHMS(videoDuration - time) - + mediaPlayer.position = Float(self.scrubberView.frame.minX) / Float(self.transportBarView.frame.width) - + play() - + seeking = false return } - + playing ? pause() : play() } - - + // MARK: Jellyfin Playstate updates 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: (!playing), 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) @@ -649,10 +629,10 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, .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) @@ -661,14 +641,14 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, }) .store(in: &cancellables) } - + func sendPlayReport() { startTime = Int(Date().timeIntervalSince1970) * 10000000 - + 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) @@ -677,10 +657,9 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, }) .store(in: &cancellables) } - - + // MARK: VLC Delegate - + func mediaPlayerStateChanged(_ aNotification: Notification!) { let currentState: VLCMediaPlayerState = mediaPlayer.state switch currentState { @@ -695,19 +674,19 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, break case .stopped: print("stopped") - + break case .ended: print("ended") - + break case .opening: print("opening") - + break case .paused: print("paused") - + break case .playing: print("Video is playing") @@ -728,14 +707,14 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, default: print("default") break - + } - + } - + // Move time along transport bar func mediaPlayerTimeChanged(_ aNotification: Notification!) { - + if loading { loading = false DispatchQueue.main.async { [self] in @@ -744,20 +723,20 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } updateNowPlayingCenter(time: nil, playing: true) } - + let time = mediaPlayer.position if time != lastTime { self.currentTimeLabel.text = formatSecondsToHMS(Double(mediaPlayer.time.intValue/1000)) self.remainingTimeLabel.text = "-" + formatSecondsToHMS(Double(abs(mediaPlayer.remainingTime.intValue/1000))) - + self.videoPos = Double(mediaPlayer.position) - + let newPos = videoPos * Double(self.transportBarView.frame.width) if !newPos.isNaN && self.playing { self.scrubberView.frame = CGRect(x: newPos, y: 0, width: 2, height: 10) self.currentTimeLabel.frame = CGRect(x: CGFloat(newPos) + transportBarView.frame.minX - currentTimeLabel.frame.width/2, y: currentTimeLabel.frame.minY, width: currentTimeLabel.frame.width, height: currentTimeLabel.frame.height) } - + if showingControls { if CACurrentMediaTime() - controlsAppearTime > 5 { showingControls = false @@ -770,34 +749,32 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, controlsAppearTime = 999_999_999_999_999 } } - + } - + lastTime = time - + if CACurrentMediaTime() - lastProgressReportTime > 5 { sendProgressReport(eventName: "timeupdate") lastProgressReportTime = CACurrentMediaTime() } } - - + // MARK: Settings Delegate func selectNew(audioTrack id: Int32) { selectedAudioTrack = id mediaPlayer.currentAudioTrackIndex = id } - + func selectNew(subtitleTrack id: Int32) { selectedCaptionTrack = id mediaPlayer.currentVideoSubTitleIndex = id } - + func setupInfoPanel() { containerViewController?.setupInfoViews(mediaItem: manifest, subtitleTracks: subtitleTrackArray, selectedSubtitleTrack: selectedCaptionTrack, audioTracks: audioTrackArray, selectedAudioTrack: selectedAudioTrack, delegate: self) } - - + func formatSecondsToHMS(_ seconds: Double) -> String { let timeHMSFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() @@ -808,16 +785,16 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, formatter.zeroFormattingBehavior = .pad return formatter }() - + guard !seconds.isNaN, let text = timeHMSFormatter.string(from: seconds) else { return "00:00" } - + return text.hasPrefix("0") && text.count > 4 ? .init(text.dropFirst()) : text } - + // When VLC video starts playing a real device can no longer receive gesture recognisers, adding this in hopes to fix the issue but no luck func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { print("recognisesimultaneousvideoplayer") diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index a5ad623d..d3d651bd 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -122,7 +122,7 @@ struct ConnectToServerView: View { } .disabled(viewModel.isLoading || uri.isEmpty) } - + Section(header: Text("Discovered Servers")) { if self.viewModel.searching { ProgressView() @@ -142,7 +142,7 @@ struct ConnectToServerView: View { ProgressView() } } - + }) } } diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index d5bb2129..67396556 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -12,27 +12,27 @@ import JellyfinAPI struct ProgressBar: Shape { func path(in rect: CGRect) -> Path { var path = Path() - + let tl = CGPoint(x: rect.minX, y: rect.minY) let tr = CGPoint(x: rect.maxX, y: rect.minY) let br = CGPoint(x: rect.maxX, y: rect.maxY) let bls = CGPoint(x: rect.minX + 10, y: rect.maxY) let blc = CGPoint(x: rect.minX + 10, y: rect.maxY - 10) - + path.move(to: tl) path.addLine(to: tr) path.addLine(to: br) path.addLine(to: bls) path.addRelativeArc(center: blc, radius: 10, startAngle: Angle.degrees(90), delta: Angle.degrees(90)) - + return path } } struct ContinueWatchingView: View { var items: [BaseItemDto] - + var body: some View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { @@ -56,7 +56,7 @@ struct ContinueWatchingView: View { .fontWeight(.semibold) .foregroundColor(.primary) .lineLimit(1) - if(item.type == "Episode") { + if item.type == "Episode" { Text("• S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0)) - \(item.name ?? "")") .font(.callout) .fontWeight(.semibold) diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index e7f5b220..7c3767e9 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -13,10 +13,10 @@ import SwiftUI struct HomeView: View { @StateObject var viewModel = HomeViewModel() @State var showingSettings = false - + @ViewBuilder var innerBody: some View { - if(viewModel.isLoading) { + if viewModel.isLoading { ProgressView() } else { ScrollView { @@ -53,7 +53,7 @@ struct HomeView: View { } } } - + var body: some View { innerBody .navigationTitle(MainTabView.Tab.home.localized) diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 8d9e8402..2595bf18 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -9,7 +9,7 @@ import SwiftUI struct LatestMediaView: View { @StateObject var viewModel: LatestMediaViewModel - + var body: some View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { @@ -23,15 +23,14 @@ struct LatestMediaView: View { .shadow(radius: 4) .overlay( ZStack { - if(item.userData!.played ?? false) { + if item.userData!.played ?? false { Image(systemName: "circle.fill") .foregroundColor(.white) Image(systemName: "checkmark.circle.fill") .foregroundColor(Color(.systemBlue)) } }.padding(2) - .opacity(1) - , alignment: .topTrailing).opacity(1) + .opacity(1), alignment: .topTrailing).opacity(1) Text(item.seriesName ?? item.name ?? "") .font(.caption) .fontWeight(.semibold) diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index 5a1faef9..9b7c450b 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -13,12 +13,12 @@ struct LibraryListView: View { var body: some View { ScrollView { - LazyVStack() { + LazyVStack { NavigationLink(destination: LazyView { LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites") }) { - ZStack() { - HStack() { + ZStack { + HStack { Spacer() Text("Your Favorites") .foregroundColor(.black) @@ -34,12 +34,12 @@ struct LibraryListView: View { .cornerRadius(10) .shadow(radius: 5) .padding(.bottom, 5) - + NavigationLink(destination: LazyView { Text("WIP") }) { - ZStack() { - HStack() { + ZStack { + HStack { Spacer() Text("All Genres") .foregroundColor(.black) @@ -55,16 +55,16 @@ struct LibraryListView: View { .cornerRadius(10) .shadow(radius: 5) .padding(.bottom, 15) - + ForEach(viewModel.libraries, id: \.id) { library in - if(library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows") { + if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { NavigationLink(destination: LazyView { LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "") }) { - ZStack() { + ZStack { ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) .opacity(0.4) - HStack() { + HStack { Spacer() Text(library.name ?? "") .foregroundColor(.white) diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index 5a514538..59b12808 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -24,7 +24,7 @@ struct LibrarySearchView: View { Spacer().frame(height: 6) SearchBar(text: $searchQuery) ZStack { - if(!viewModel.isLoading) { + if !viewModel.isLoading { ScrollView(.vertical) { if !viewModel.items.isEmpty { Spacer().frame(height: 16) @@ -37,15 +37,14 @@ struct LibrarySearchView: View { .cornerRadius(10) .overlay( ZStack { - if(item.userData!.played ?? false) { + if item.userData!.played ?? false { Image(systemName: "circle.fill") .foregroundColor(.white) Image(systemName: "checkmark.circle.fill") .foregroundColor(Color(.systemBlue)) } }.padding(2) - .opacity(1) - , alignment: .topTrailing).opacity(1) + .opacity(1), alignment: .topTrailing).opacity(1) Text(item.name ?? "") .font(.caption) .fontWeight(.semibold) diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index bd0b5bea..576b27a9 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -41,15 +41,14 @@ struct LibraryView: View { .cornerRadius(10) .overlay( ZStack { - if(item.userData!.played ?? false) { + if item.userData!.played ?? false { Image(systemName: "circle.fill") .foregroundColor(.white) Image(systemName: "checkmark.circle.fill") .foregroundColor(Color(.systemBlue)) } }.padding(2) - .opacity(1) - , alignment: .topTrailing).opacity(1) + .opacity(1), alignment: .topTrailing).opacity(1) Text(item.name ?? "") .font(.caption) .fontWeight(.semibold) diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index 411a1a37..7fbd811d 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -10,9 +10,9 @@ import Combine import JellyfinAPI struct NextUpView: View { - + var items: [BaseItemDto] - + var body: some View { VStack(alignment: .leading) { Text("Next Up") diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 2939fd3f..451756fc 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -78,15 +78,14 @@ struct SeasonItemView: View { ) .overlay( ZStack { - if(episode.userData!.played ?? false) { + if episode.userData!.played ?? false { Image(systemName: "circle.fill") .foregroundColor(.white) Image(systemName: "checkmark.circle.fill") .foregroundColor(Color(.systemBlue)) } }.padding(2) - .opacity(1) - , alignment: .topTrailing).opacity(1) + .opacity(1), alignment: .topTrailing).opacity(1) VStack(alignment: .leading) { HStack { Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline) diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 9c15655f..b3cab36d 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -11,9 +11,9 @@ import Defaults struct SettingsView: View { @Environment(\.managedObjectContext) private var viewContext - + @ObservedObject var viewModel: SettingsViewModel - + @Binding var close: Bool @Default(.inNetworkBandwidth) var inNetworkStreamBitrate @Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate @@ -21,11 +21,11 @@ struct SettingsView: View { @Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode @Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode @State private var username: String = "" - + func onAppear() { username = SessionManager.current.user.username ?? "" } - + var body: some View { NavigationView { Form { @@ -35,20 +35,20 @@ struct SettingsView: View { Text(bitrate.name).tag(bitrate.value) } } - + Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in Text(bitrate.name).tag(bitrate.value) } } } - + Section(header: Text("Accessibility")) { Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) SearchablePicker(label: "Preferred subtitle language", options: viewModel.langs, optionToString: { $0.name }, - selected:Binding( + selected: Binding( get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto }, set: {autoSelectSubtitlesLangcode = $0.isoCode} ) @@ -62,7 +62,7 @@ struct SettingsView: View { ) ) } - + Section { HStack { Text("Signed in as \(username)").foregroundColor(.primary) @@ -70,8 +70,7 @@ struct SettingsView: View { Button { let nc = NotificationCenter.default nc.post(name: Notification.Name("didSignOut"), object: nil) - - + SessionManager.current.logout() } label: { Text("Log out").font(.callout) diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 198dd4cd..3f52b644 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -53,10 +53,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe var startTime: Int = 0 var controlsAppearTime: Double = 0 var isSeeking: Bool = false - - var playerDestination: PlayerDestination = .local; - var discoveredCastDevices: [GCKDevice] = []; - var selectedCastDevice: GCKDevice?; + + var playerDestination: PlayerDestination = .local + var discoveredCastDevices: [GCKDevice] = [] + var selectedCastDevice: GCKDevice? var jellyfinCastChannel: GCKGenericChannel? var remotePositionTicks: Int = 0 private var castDiscoveryManager: GCKDiscoveryManager { @@ -65,7 +65,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe private var castSessionManager: GCKSessionManager { return GCKCastContext.sharedInstance().sessionManager } - var hasSentRemoteSeek: Bool = false; + var hasSentRemoteSeek: Bool = false var selectedAudioTrack: Int32 = -1 var selectedCaptionTrack: Int32 = -1 @@ -77,11 +77,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe var manifest: BaseItemDto = BaseItemDto() var playbackItem = PlaybackItem() var remoteTimeUpdateTimer: Timer? - // MARK: IBActions @IBAction func seekSliderStart(_ sender: Any) { - if(playerDestination == .local) { + if playerDestination == .local { sendProgressReport(eventName: "pause") mediaPlayer.pause() } else { @@ -111,8 +110,8 @@ 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 playerDestination == .local { if offset > 0 { mediaPlayer.jumpForward(Int32(offset)) } else { @@ -130,22 +129,22 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBAction func exitButtonPressed(_ sender: Any) { sendStopReport() mediaPlayer.stop() - - if(castSessionManager.hasConnectedCastSession()) { + + if castSessionManager.hasConnectedCastSession() { castSessionManager.endSessionAndStopCasting(true) } - + delegate?.exitPlayer(self) } @IBAction func controlViewTapped(_ sender: Any) { - if(playerDestination == .local) { + if playerDestination == .local { videoControlsView.isHidden = true } } @IBAction func contentViewTapped(_ sender: Any) { - if(playerDestination == .local) { + if playerDestination == .local { videoControlsView.isHidden = false controlsAppearTime = CACurrentMediaTime() } @@ -153,7 +152,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBAction func jumpBackTapped(_ sender: Any) { if paused == false { - if(playerDestination == .local) { + if playerDestination == .local { mediaPlayer.jumpBackward(15) } else { self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000)-15]) @@ -163,7 +162,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBAction func jumpForwardTapped(_ sender: Any) { if paused == false { - if(playerDestination == .local) { + if playerDestination == .local { mediaPlayer.jumpForward(30) } else { self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000)+30]) @@ -174,7 +173,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBOutlet weak var mainActionButton: UIButton! @IBAction func mainActionButtonPressed(_ sender: Any) { if paused { - if(playerDestination == .local) { + if playerDestination == .local { mediaPlayer.play() mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) paused = false @@ -184,7 +183,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe paused = false } } else { - if(playerDestination == .local) { + if playerDestination == .local { mediaPlayer.pause() mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) paused = true @@ -210,10 +209,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) } } - - //MARK: Cast methods + + // MARK: Cast methods @IBAction func castButtonPressed(_ sender: Any) { - if(selectedCastDevice == nil) { + if selectedCastDevice == nil { castDeviceVC = VideoPlayerCastDeviceSelectorView() castDeviceVC?.delegate = self @@ -228,33 +227,33 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } else { castSessionManager.endSessionAndStopCasting(true) - selectedCastDevice = nil; + selectedCastDevice = nil self.castButton.isEnabled = true self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) playerDestination = .local } } - + func castPopoverDismissed() { castDeviceVC?.dismiss(animated: true, completion: nil) - if(playerDestination == .local) { + if playerDestination == .local { self.mediaPlayer.play() } self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } - + func castDeviceChanged() { - if(selectedCastDevice != nil) { + if selectedCastDevice != nil { playerDestination = .remote castSessionManager.add(self) castSessionManager.startSession(with: selectedCastDevice!) } } - - //MARK: Cast End + + // MARK: Cast End func settingsPopoverDismissed() { optionsVC?.dismiss(animated: true, completion: nil) - if(playerDestination == .local) { + if playerDestination == .local { self.mediaPlayer.play() self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } @@ -270,7 +269,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Add handler for Pause Command commandCenter.pauseCommand.addTarget { _ in - if(self.playerDestination == .local) { + if self.playerDestination == .local { self.mediaPlayer.pause() self.sendProgressReport(eventName: "pause") } else { @@ -282,7 +281,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Add handler for Play command commandCenter.playCommand.addTarget { _ in - if(self.playerDestination == .local) { + if self.playerDestination == .local { self.mediaPlayer.play() self.sendProgressReport(eventName: "unpause") } else { @@ -294,7 +293,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Add handler for FF command commandCenter.seekForwardCommand.addTarget { _ in - if(self.playerDestination == .local) { + if self.playerDestination == .local { self.mediaPlayer.jumpForward(30) self.sendProgressReport(eventName: "timeupdate") } else { @@ -305,7 +304,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Add handler for RW command commandCenter.seekBackwardCommand.addTarget { _ in - if(self.playerDestination == .local) { + if self.playerDestination == .local { self.mediaPlayer.jumpBackward(15) self.sendProgressReport(eventName: "timeupdate") } else { @@ -320,11 +319,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe 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 self.playerDestination == .local { if offset > 0 { self.mediaPlayer.jumpForward(Int32(offset)/1000) } else { @@ -332,7 +331,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } self.sendProgressReport(eventName: "unpause") } else { - + } return .success @@ -356,8 +355,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0)) “\(manifest.name ?? "")”" } - - if(!UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat) { + + if !UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat { let value = UIInterfaceOrientation.landscapeRight.rawValue UIDevice.current.setValue(value, forKey: "orientation") UIViewController.attemptRotationToDeviceOrientation() @@ -366,7 +365,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } func mediaHasStartedPlaying() { - castButton.isHidden = true; + castButton.isHidden = true let discoveryCriteria = GCKDiscoveryCriteria(applicationID: "F007D354") let gckCastOptions = GCKCastOptions(discoveryCriteria: discoveryCriteria) GCKCastContext.setSharedInstanceWith(gckCastOptions) @@ -374,11 +373,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castDiscoveryManager.add(self) castDiscoveryManager.startDiscovery() } - + func didUpdateDeviceList() { - let totalDevices = castDiscoveryManager.deviceCount; + let totalDevices = castDiscoveryManager.deviceCount discoveredCastDevices = [] - if(totalDevices > 0) { + if totalDevices > 0 { for i in 0...totalDevices-1 { let device = castDiscoveryManager.device(at: i) discoveredCastDevices.append(device) @@ -395,15 +394,15 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castButton.setImage(nil, for: .normal) } } - + override func viewWillDisappear(_ animated: Bool) { self.tabBarController?.tabBar.isHidden = false self.navigationController?.isNavigationBarHidden = false overrideUserInterfaceStyle = .unspecified super.viewWillDisappear(animated) } - - //MARK: viewDidAppear + + // MARK: viewDidAppear override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) overrideUserInterfaceStyle = .dark @@ -532,7 +531,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe selectedAudioTrack = audioTrackArray[0].id } } - + self.sendPlayReport() playbackItem = item } @@ -542,7 +541,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe .store(in: &cancellables) } } - + func setupTracksForPreferredDefaults() { subtitleTrackArray.forEach { subtitle in if Defaults[.isAutoSelectSubtitles] { @@ -556,7 +555,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + audioTrackArray.forEach { audio in if audio.languageCode.contains(Defaults[.autoSelectAudioLangCode]) { selectedAudioTrack = audio.id @@ -564,7 +563,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + func startLocalPlaybackEngine(_ fetchCaptions: Bool) { print("Local playback engine starting.") mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) @@ -572,17 +571,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe sendPlayReport() // 1 second = 10,000,000 ticks - var startTicks: Int64 = 0; - if(remotePositionTicks == 0) { + var startTicks: Int64 = 0 + if remotePositionTicks == 0 { print("Using server-reported start time") startTicks = manifest.userData?.playbackPositionTicks ?? 0 } else { print("Using remote-reported start time") startTicks = Int64(remotePositionTicks) } - + if startTicks != 0 { - let videoPosition = Double(mediaPlayer.time.intValue / 1000); + let videoPosition = Double(mediaPlayer.time.intValue / 1000) let secondsScrubbedTo = startTicks / 10_000_000 let offset = secondsScrubbedTo - Int64(videoPosition) print("Seeking to position: \(secondsScrubbedTo)") @@ -592,8 +591,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe mediaPlayer.jumpBackward(Int32(abs(offset))) } } - - if(fetchCaptions) { + + if fetchCaptions { print("Fetching captions.") // Pause and load captions into memory. mediaPlayer.pause() @@ -603,21 +602,21 @@ 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() - + print("Local engine started.") } @@ -633,15 +632,15 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } -//MARK: - GCKGenericChannelDelegate +// MARK: - GCKGenericChannelDelegate extension PlayerViewController: GCKGenericChannelDelegate { @objc func updateRemoteTime() { castButton.setImage(UIImage(named: "CastConnected"), for: .normal) - if(!paused) { - remotePositionTicks = remotePositionTicks + 2_000_000; //add 0.2 secs every timer evt. + if !paused { + remotePositionTicks = remotePositionTicks + 2_000_000; // add 0.2 secs every timer evt. } - - if(isSeeking == false) { + + if isSeeking == false { let remainingTime = (manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000 let hours = remainingTime / 3600 let minutes = (remainingTime % 3600) / 60 @@ -653,36 +652,36 @@ 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) { let messageType = json["type"].string ?? "" - if(messageType == "playbackprogress") { + if messageType == "playbackprogress" { dump(json) - if(remotePositionTicks > 100) { - if(hasSentRemoteSeek == false) { - hasSentRemoteSeek = true; + if remotePositionTicks > 100 { + if hasSentRemoteSeek == false { + hasSentRemoteSeek = true sendJellyfinCommand(command: "Seek", options: [ "position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position) ]) } } paused = json["data"]["PlayState"]["IsPaused"].boolValue - self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0; - if(remoteTimeUpdateTimer == nil) { + self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0 + if remoteTimeUpdateTimer == nil { remoteTimeUpdateTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateRemoteTime), userInfo: nil, repeats: true) } } } } } - + func sendJellyfinCommand(command: String, options: [String: Any]) { let payload: [String: Any] = [ "options": options, @@ -698,12 +697,12 @@ extension PlayerViewController: GCKGenericChannelDelegate { ] print(payload) let jsonData = JSON(payload) - + jellyfinCastChannel?.sendTextMessage(jsonData.rawString()!, error: nil) - - if(command == "Seek") { + + if command == "Seek" { remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000) - //Send playback report as Jellyfin Chromecast isn't smarter than a rock. + // 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) @@ -717,25 +716,25 @@ extension PlayerViewController: GCKGenericChannelDelegate { } } -//MARK: - GCKSessionManagerListener +// MARK: - GCKSessionManagerListener extension PlayerViewController: GCKSessionManagerListener { func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) { self.sendStopReport() mediaPlayer.stop() - + playerDestination = .remote - videoContentView.isHidden = true; - videoControlsView.isHidden = false; + 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!, @@ -748,44 +747,43 @@ extension PlayerViewController: GCKSessionManagerListener { ] sendJellyfinCommand(command: "PlayNow", options: playNowOptions) } - + func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) { print("starting session") 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") print("resuming session") self.sessionDidStart(manager: sessionManager, didStart: session) } - + func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) { dump(error) } - func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKCastSession, withError error: Error?) { print("didEnd") - playerDestination = .local; - videoContentView.isHidden = false; + 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) { print("didSuspend") - playerDestination = .local; - videoContentView.isHidden = false; + playerDestination = .local + videoContentView.isHidden = false remoteTimeUpdateTimer?.invalidate() castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) startLocalPlaybackEngine(false) } } -//MARK: - VLCMediaPlayer Delegates +// MARK: - VLCMediaPlayer Delegates extension PlayerViewController: VLCMediaPlayerDelegate { func mediaPlayerStateChanged(_ aNotification: Notification!) { let currentState: VLCMediaPlayerState = mediaPlayer.state @@ -820,7 +818,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate { break } } - + func mediaPlayerTimeChanged(_ aNotification: Notification!) { let time = mediaPlayer.position if abs(time-lastTime) > 0.00005 { @@ -851,18 +849,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate { } } - - - - - - - - - - - -//MARK: End VideoPlayerVC +// MARK: End VideoPlayerVC struct VLCPlayerWithControls: UIViewControllerRepresentable { var item: BaseItemDto @Environment(\.presentationMode) var presentationMode @@ -909,7 +896,7 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable { } } -//MARK: - Play State Update Methods +// MARK: - Play State Update Methods extension PlayerViewController { func sendProgressReport(eventName: String) { if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { diff --git a/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift b/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift index 02a253b5..5608b1a4 100644 --- a/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift +++ b/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift @@ -15,7 +15,7 @@ class VideoPlayerCastDeviceSelectorView: UIViewController { override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .landscape } - + override func viewDidLoad() { super.viewDidLoad() contentView = UIHostingController(rootView: VideoPlayerCastDeviceSelector(delegate: self.delegate ?? PlayerViewController())) @@ -43,9 +43,9 @@ struct VideoPlayerCastDeviceSelector: View { var body: some View { NavigationView { Group { - if(!delegate.discoveredCastDevices.isEmpty) { + if !delegate.discoveredCastDevices.isEmpty { List(delegate.discoveredCastDevices, id: \.deviceID) { device in - HStack() { + HStack { Text(device.friendlyName!) .font(.subheadline) .fontWeight(.medium) @@ -55,7 +55,7 @@ struct VideoPlayerCastDeviceSelector: View { delegate?.castDeviceChanged() delegate?.castPopoverDismissed() } label: { - HStack() { + HStack { Text("Connect") .font(.caption) .fontWeight(.medium) @@ -91,4 +91,3 @@ struct VideoPlayerCastDeviceSelector: View { }.offset(y: UIDevice.current.userInterfaceIdiom == .pad ? 14 : 0) } } - diff --git a/JellyfinPlayer/VideoPlayerSettingsView.swift b/JellyfinPlayer/VideoPlayerSettingsView.swift index 051258af..896d92b8 100644 --- a/JellyfinPlayer/VideoPlayerSettingsView.swift +++ b/JellyfinPlayer/VideoPlayerSettingsView.swift @@ -15,7 +15,7 @@ class VideoPlayerSettingsView: UIViewController { override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .landscape } - + override func viewDidLoad() { super.viewDidLoad() contentView = UIHostingController(rootView: VideoPlayerSettings(delegate: self.delegate ?? PlayerViewController())) diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift index 139842f2..b4db6390 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/APIExtensions.swift @@ -95,12 +95,12 @@ extension BaseItemDto { let imageType = "Primary" var imageTag = self.imageTags?["Primary"] ?? "" var imageItemId = self.id ?? "" - + if imageTag == "" || imageItemId == "" { imageTag = self.seriesPrimaryImageTag ?? "" imageItemId = self.seriesId ?? "" } - + let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" diff --git a/Shared/ServerLocator/ServerDiscovery.swift b/Shared/ServerLocator/ServerDiscovery.swift index d35a44e7..04dfec15 100644 --- a/Shared/ServerLocator/ServerDiscovery.swift +++ b/Shared/ServerLocator/ServerDiscovery.swift @@ -17,7 +17,7 @@ public class ServerDiscovery { public let username: String public let password: String public let deviceId: String - + public init(_ host: String, _ port: Int, _ username: String, _ password: String, _ deviceId: String = UUID().uuidString) { self.host = host self.port = port @@ -26,17 +26,17 @@ public class ServerDiscovery { self.deviceId = deviceId } } - + public struct ServerLookupResponse: Codable, Hashable, Identifiable { - + public func hash(into hasher: inout Hasher) { return hasher.combine(id) } - + private let address: String public let id: String public let name: String - + public var url: URL { URL(string: self.address)! } @@ -47,7 +47,7 @@ public class ServerDiscovery { } return self.address } - + public var port: Int { let components = URLComponents(string: self.address) if let port = components?.port { @@ -55,7 +55,7 @@ public class ServerDiscovery { } return 8096 } - + enum CodingKeys: String, CodingKey { case address = "Address" case id = "Id" @@ -63,16 +63,16 @@ public class ServerDiscovery { } } private let broadcastConn: UDPBroadcastConnection - + public init() { func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) { } - + func errorHandler(error: UDPBroadcastConnection.ConnectionError) { } self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler) } - + public func locateServer(completion: @escaping (ServerLookupResponse?) -> Void) { func receiveHandler(_ ipAddress: String, _ port: Int, _ data: Data) { do { diff --git a/Shared/ServerLocator/UDPBroadCastConnection.swift b/Shared/ServerLocator/UDPBroadCastConnection.swift index d28ab54e..de0cd770 100644 --- a/Shared/ServerLocator/UDPBroadCastConnection.swift +++ b/Shared/ServerLocator/UDPBroadCastConnection.swift @@ -16,38 +16,37 @@ import Darwin let INADDR_ANY = in_addr(s_addr: 0) let INADDR_BROADCAST = in_addr(s_addr: 0xffffffff) - /// An object representing the UDP broadcast connection. Uses a dispatch source to handle the incoming traffic on the UDP socket. open class UDPBroadcastConnection { - + // MARK: Properties - + /// The address of the UDP socket. var address: sockaddr_in - + /// Type of a closure that handles incoming UDP packets. public typealias ReceiveHandler = (_ ipAddress: String, _ port: Int, _ response: Data) -> Void /// Closure that handles incoming UDP packets. var handler: ReceiveHandler? - + /// Type of a closure that handles errors that were encountered during receiving UDP packets. public typealias ErrorHandler = (_ error: ConnectionError) -> Void /// Closure that handles errors that were encountered during receiving UDP packets. var errorHandler: ErrorHandler? - + /// A dispatch source for reading data from the UDP socket. var responseSource: DispatchSourceRead? - + /// The dispatch queue to run responseSource & reconnection on var dispatchQueue: DispatchQueue = DispatchQueue.main - + /// Bind to port to start listening without first sending a message var shouldBeBound: Bool = false - + // MARK: Initializers - + /// Initializes the UDP connection with the correct port address. - + /// - Note: This doesn't open a socket! The socket is opened transparently as needed when sending broadcast messages. If you want to open a socket immediately, use the `bindIt` parameter. This will also try to reopen the socket if it gets closed. /// /// - Parameters: @@ -58,13 +57,13 @@ open class UDPBroadcastConnection { /// - Throws: Throws a `ConnectionError` if an error occurs. public init(port: UInt16, bindIt: Bool = false, handler: ReceiveHandler?, errorHandler: ErrorHandler?) throws { self.address = sockaddr_in( - sin_len: __uint8_t(MemoryLayout.size), + sin_len: __uint8_t(MemoryLayout.size), sin_family: sa_family_t(AF_INET), - sin_port: UDPBroadcastConnection.htonsPort(port: port), - sin_addr: INADDR_BROADCAST, - sin_zero: ( 0, 0, 0, 0, 0, 0, 0, 0 ) + sin_port: UDPBroadcastConnection.htonsPort(port: port), + sin_addr: INADDR_BROADCAST, + sin_zero: ( 0, 0, 0, 0, 0, 0, 0, 0 ) ) - + self.handler = handler self.errorHandler = errorHandler self.shouldBeBound = bindIt @@ -72,34 +71,33 @@ open class UDPBroadcastConnection { try createSocket() } } - + deinit { if responseSource != nil { responseSource!.cancel() } } - + // MARK: Interface - - + /// Create a UDP socket for broadcasting and set up cancel and event handlers /// /// - Throws: Throws a `ConnectionError` if an error occurs. fileprivate func createSocket() throws { - + // Create new socket let newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) guard newSocket > 0 else { throw ConnectionError.createSocketFailed } - + // Enable broadcast on socket - var broadcastEnable = Int32(1); - let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout.size)); + var broadcastEnable = Int32(1) + let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout.size)) if ret == -1 { debugPrint("Couldn't enable broadcast on socket") close(newSocket) throw ConnectionError.enableBroadcastFailed } - + // Bind socket if needed if shouldBeBound { var saddr = sockaddr(sa_len: 0, sa_family: 0, @@ -114,34 +112,34 @@ open class UDPBroadcastConnection { throw ConnectionError.bindSocketFailed } } - + // Disable global SIGPIPE handler so that the app doesn't crash setNoSigPipe(socket: newSocket) - + // Set up a dispatch source let newResponseSource = DispatchSource.makeReadSource(fileDescriptor: newSocket, queue: dispatchQueue) - + // Set up cancel handler newResponseSource.setCancelHandler { - //debugPrint("Closing UDP socket") + // debugPrint("Closing UDP socket") let UDPSocket = Int32(newResponseSource.handle) shutdown(UDPSocket, SHUT_RDWR) close(UDPSocket) } - + // Set up event handler (gets called when data arrives at the UDP socket) newResponseSource.setEventHandler { [unowned self] in guard let source = self.responseSource else { return } - + var socketAddress = sockaddr_storage() var socketAddressLength = socklen_t(MemoryLayout.size) let response = [UInt8](repeating: 0, count: 4096) let UDPSocket = Int32(source.handle) - + let bytesRead = withUnsafeMutablePointer(to: &socketAddress) { recvfrom(UDPSocket, UnsafeMutableRawPointer(mutating: response), response.count, 0, UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength) } - + do { guard bytesRead > 0 else { self.closeConnection() @@ -155,18 +153,18 @@ open class UDPBroadcastConnection { throw ConnectionError.receiveFailed(code: errno) } } - + guard let endpoint = withUnsafePointer(to: &socketAddress, { self.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1)) }) else { - //debugPrint("Failed to get the address and port from the socket address received from recvfrom") + // debugPrint("Failed to get the address and port from the socket address received from recvfrom") self.closeConnection() return } - - //debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") - + + // debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") + let responseBytes = Data(response[0.. 0 else { if let errorString = String(validatingUTF8: strerror(errno)) { - //debugPrint("UDP connection failed to send data: \(errorString)") + // debugPrint("UDP connection failed to send data: \(errorString)") } closeConnection() throw ConnectionError.sendingMessageFailed(code: errno) } - + if sent == broadcastMessageLength { // Success - //debugPrint("UDP connection sent \(broadcastMessageLength) bytes") + // debugPrint("UDP connection sent \(broadcastMessageLength) bytes") } } } - + /// Close the connection. /// /// - Parameter reopen: Automatically reopens the connection if true. Defaults to true. @@ -244,16 +242,16 @@ open class UDPBroadcastConnection { } } } - + // MARK: - Helper - + /// Convert a sockaddr structure into an IP address string and port. /// /// - Parameter socketAddressPointer: socketAddressPointer: Pointer to a socket address. /// - Returns: Returns a tuple of the host IP address and the port in the socket address given. func getEndpointFromSocketAddress(socketAddressPointer: UnsafePointer) -> (host: String, port: Int)? { let socketAddress = UnsafePointer(socketAddressPointer).pointee - + switch Int32(socketAddress.sa_family) { case AF_INET: var socketAddressInet = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in.self) @@ -262,7 +260,7 @@ open class UDPBroadcastConnection { let hostCString = inet_ntop(AF_INET, &socketAddressInet.sin_addr, &buffer, socklen_t(length)) let port = Int(UInt16(socketAddressInet.sin_port).byteSwapped) return (String(cString: hostCString!), port) - + case AF_INET6: var socketAddressInet6 = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in6.self) let length = Int(INET6_ADDRSTRLEN) + 2 @@ -270,60 +268,57 @@ open class UDPBroadcastConnection { let hostCString = inet_ntop(AF_INET6, &socketAddressInet6.sin6_addr, &buffer, socklen_t(length)) let port = Int(UInt16(socketAddressInet6.sin6_port).byteSwapped) return (String(cString: hostCString!), port) - + default: return nil } } - - + // MARK: - Private - + /// Prevents crashes when blocking calls are pending and the app is paused (via Home button). /// /// - Parameter socket: The socket for which the signal should be disabled. fileprivate func setNoSigPipe(socket: CInt) { - var no_sig_pipe: Int32 = 1; - setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout.size)); + var no_sig_pipe: Int32 = 1 + setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout.size)) } - + fileprivate class func htonsPort(port: in_port_t) -> in_port_t { let isLittleEndian = Int(OSHostByteOrder()) == OSLittleEndian return isLittleEndian ? _OSSwapInt16(port) : port } - + fileprivate class func ntohs(value: CUnsignedShort) -> CUnsignedShort { return (value << 8) + (value >> 8) } - + } - - // Created by Gunter Hager on 25.03.19. // Copyright © 2019 Gunter Hager. All rights reserved. // public extension UDPBroadcastConnection { - + enum ConnectionError: Error { // Creating socket case createSocketFailed case enableBroadcastFailed case bindSocketFailed - + // Sending message case messageEncodingFailed case sendingMessageFailed(code: Int32) - + // Receiving data case receivedEndOfFile case receiveFailed(code: Int32) - + // Closing socket case reopeningSocketFailed(error: Error) - + // Underlying case underlying(error: Error) } - + } diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index f0f27de8..bcfc516d 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -149,7 +149,7 @@ final class SessionManager { func logout() { let nc = NotificationCenter.default nc.post(name: Notification.Name("didSignOut"), object: nil) - + let keychain = KeychainSwift() keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" keychain.delete("AccessToken_\(user?.user_id ?? "")") diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 0c34b20d..5c3898e3 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -14,7 +14,7 @@ import JellyfinAPI final class ConnectToServerViewModel: ViewModel { @Published var isConnectedServer = false - + var uriSubject = CurrentValueSubject("") var usernameSubject = CurrentValueSubject("") var passwordSubject = CurrentValueSubject("") @@ -25,11 +25,11 @@ final class ConnectToServerViewModel: ViewModel { var publicUsers = [UserDto]() @Published var selectedPublicUser = UserDto() - + private let discovery: ServerDiscovery = ServerDiscovery() @Published var servers: [ServerDiscovery.ServerLookupResponse] = [] @Published var searching = false - + override init() { super.init() getPublicUsers() @@ -74,8 +74,8 @@ final class ConnectToServerViewModel: ViewModel { }) .store(in: &cancellables) } - - func connectToServer(at url : URL) { + + func connectToServer(at url: URL) { ServerEnvironment.current.create(with: url.absoluteString) .trackActivity(loading) .sink(receiveCompletion: { result in @@ -90,15 +90,15 @@ final class ConnectToServerViewModel: ViewModel { }) .store(in: &cancellables) } - + func discoverServers() { searching = true - + // Timeout after 5 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 5) { self.searching = false } - + discovery.locateServer { [self] (server) in if let server = server, !servers.contains(server) { servers.append(server) diff --git a/Shared/ViewModels/LibraryFilterViewModel.swift b/Shared/ViewModels/LibraryFilterViewModel.swift index 8b38372c..bf6a01ec 100644 --- a/Shared/ViewModels/LibraryFilterViewModel.swift +++ b/Shared/ViewModels/LibraryFilterViewModel.swift @@ -39,12 +39,12 @@ final class LibraryFilterViewModel: ViewModel { var selectedSortOrder: APISortOrder = .descending @Published var selectedSortBy: SortBy = .name - + func updateModifiedFilter() { modifiedFilters.sortOrder = [selectedSortOrder] modifiedFilters.sortBy = [selectedSortBy] } - + func resetFilters() { modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) } @@ -54,7 +54,7 @@ final class LibraryFilterViewModel: ViewModel { self.enabledFilterType = enabledFilterType self.selectedSortBy = filters!.sortBy.first! self.selectedSortOrder = filters!.sortOrder.first! - + super.init() if let filters = filters { self.modifiedFilters = filters diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index eba576d6..8ad1033a 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -26,7 +26,7 @@ struct Bitrates: Codable, Hashable { struct TrackLanguage: Hashable { var name: String var isoCode: String - + static let auto = TrackLanguage(name: "Auto", isoCode: "Auto") } diff --git a/WidgetExtension/NextUpWidget.swift b/WidgetExtension/NextUpWidget.swift index 83b89c98..26f91c61 100644 --- a/WidgetExtension/NextUpWidget.swift +++ b/WidgetExtension/NextUpWidget.swift @@ -28,8 +28,8 @@ struct NextUpWidgetProvider: TimelineProvider { let server = ServerEnvironment.current.server let savedUser = SessionManager.current.user var tempCancellables = Set() - - if(server != nil && savedUser != nil) { + + if server != nil && savedUser != nil { JellyfinAPI.basePath = server!.baseURI ?? "" TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], @@ -73,8 +73,8 @@ struct NextUpWidgetProvider: TimelineProvider { let savedUser = SessionManager.current.user var tempCancellables = Set() - - if(server != nil && savedUser != nil) { + + if server != nil && savedUser != nil { JellyfinAPI.basePath = server!.baseURI ?? "" TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],