From 5fe8c3b7ccd8499c4ea9603e95ae9fbc79e54ed5 Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Tue, 22 Jun 2021 09:21:53 +1000 Subject: [PATCH 1/8] tvOS player --- .../ContinueWatchingView.swift | 2 +- JellyfinPlayer tvOS/NextUpView.swift | 2 +- .../VideoPlayer/AudioView.swift | 78 ++ .../InfoTabBarViewController.swift | 54 ++ .../VideoPlayer/MediaInfoView.swift | 117 +++ .../VideoPlayer/SubtitlesView.swift | 150 ++++ .../VideoPlayer/VideoPlayer.swift | 26 + .../VideoPlayerStoryboard.storyboard | 121 +++ .../VideoPlayerViewController.swift | 785 ++++++++++++++++++ JellyfinPlayer.xcodeproj/project.pbxproj | 40 +- .../Singleton}/DeviceProfileBuilder.swift | 0 11 files changed, 1372 insertions(+), 3 deletions(-) create mode 100644 JellyfinPlayer tvOS/VideoPlayer/AudioView.swift create mode 100644 JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift create mode 100644 JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift create mode 100644 JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift create mode 100644 JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift create mode 100644 JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard create mode 100644 JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift rename {JellyfinPlayer => Shared/Singleton}/DeviceProfileBuilder.swift (100%) diff --git a/JellyfinPlayer tvOS/ContinueWatchingView.swift b/JellyfinPlayer tvOS/ContinueWatchingView.swift index 0503c59b..f73167de 100644 --- a/JellyfinPlayer tvOS/ContinueWatchingView.swift +++ b/JellyfinPlayer tvOS/ContinueWatchingView.swift @@ -25,7 +25,7 @@ struct ContinueWatchingView: View { LazyHStack { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in - NavigationLink(destination: Text("itemv")) { + NavigationLink(destination: VideoPlayerView(item: item)) { LandscapeItemElement(item: item) } .buttonStyle(PlainNavigationLinkButtonStyle()) diff --git a/JellyfinPlayer tvOS/NextUpView.swift b/JellyfinPlayer tvOS/NextUpView.swift index 5304ee0c..8b12f5cc 100644 --- a/JellyfinPlayer tvOS/NextUpView.swift +++ b/JellyfinPlayer tvOS/NextUpView.swift @@ -24,7 +24,7 @@ struct NextUpView: View { LazyHStack { Spacer().frame(width: 45) ForEach(items, id: \.id) { item in - NavigationLink(destination: Text("itemv")) { + NavigationLink(destination: VideoPlayerView(item: item)) { LandscapeItemElement(item: item) }.buttonStyle(PlainNavigationLinkButtonStyle()) } diff --git a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift new file mode 100644 index 00000000..4a52baab --- /dev/null +++ b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift @@ -0,0 +1,78 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI + +class AudioViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + tabBarItem.title = "Audio" + + } + + 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 + contentView.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + 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 + + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} + +struct AudioView: View { + + @State var selectedTrack : Int32 = -1 + @State var audioTrackArray: [AudioTrack] = [] + + weak var delegate: VideoPlayerSettingsDelegate? + + var body : some View { + NavigationView { + VStack() { + List(audioTrackArray, id: \.id) { track in + Button(action: { + delegate?.selectNew(audioTrack: track.id) + selectedTrack = track.id + }, label: { + HStack(spacing: 10){ + if track.id == selectedTrack { + Image(systemName: "checkmark") + } + 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 new file mode 100644 index 00000000..cf96178e --- /dev/null +++ b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift @@ -0,0 +1,54 @@ +// +// InfoTabBarViewController.swift +// CustomPlayer +// +// Created by Stephen Byatt on 15/6/21. +// + +import TVUIKit +import JellyfinAPI + +class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate { + + var videoPlayer : VideoPlayerViewController? = nil + var subtitleViewController : SubtitlesViewController? = nil + var audioViewController : AudioViewController? = nil + var mediaInfoController : MediaInfoViewController? = nil + + + override func viewDidLoad() { + super.viewDidLoad() + mediaInfoController = MediaInfoViewController() + audioViewController = AudioViewController() + subtitleViewController = SubtitlesViewController() + + viewControllers = [mediaInfoController!, audioViewController!, subtitleViewController!] + + } + + 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) + + } + + 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 +// override func prepare(for segue: UIStoryboardSegue, sender: Any?) { +// // Get the new view controller using segue.destination. +// // Pass the selected object to the new view controller. +// } +// + +} diff --git a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift new file mode 100644 index 00000000..453e43e4 --- /dev/null +++ b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift @@ -0,0 +1,117 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import JellyfinAPI + +class MediaInfoViewController: UIViewController { + private var contentView: UIHostingController! + + + override func viewDidLoad() { + super.viewDidLoad() + + tabBarItem.title = "Info" + } + + func setMedia(item: BaseItemDto) + { + contentView = UIHostingController(rootView: MediaInfoView(item: item)) + self.view.addSubview(contentView.view) + contentView.view.translatesAutoresizingMaskIntoConstraints = false + contentView.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + 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 MediaInfoView: View { + @State var item : BaseItemDto? = nil + + var body: some View { + if let item = item { + HStack { + 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() + } + .padding(.leading, 200) + + VStack(alignment: .leading) { + if item.type == "Episode" { + Text(item.seriesName!) + .font(.title3) + + Text("S\(item.parentIndexNumber ?? 0):E\(item.indexNumber ?? 0) • \(item.name!)") + .font(.headline) + .foregroundColor(.secondary) + } + else + { + Text(item.name!) + .font(.title3) + } + + HStack(spacing: 10) { + Text(String(item.productionYear!)) + Text("•") + + Text(formatRunningtime()) + + if item.officialRating != nil { + Text("•") + + Text("\(item.officialRating!)").font(.subheadline) + .fontWeight(.semibold) + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + .overlay(RoundedRectangle(cornerRadius: 2) + .stroke(Color.secondary, lineWidth: 1)) + + } + } + .padding(.top) + .foregroundColor(.secondary) + + + Text(item.overview!) + .padding([.top, .trailing]) + .foregroundColor(.secondary) + + Spacer() + } + + + Spacer() + + + } + .frame(maxWidth: .infinity) + } + else { + EmptyView() + } + + } + + func formatRunningtime() -> String { + let timeHMSFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .brief + formatter.allowedUnits = [.hour, .minute] + return formatter + }() + + let text = timeHMSFormatter.string(from: Double(item!.runTimeTicks! / 10_000_000)) ?? "" + + return text + } +} diff --git a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift new file mode 100644 index 00000000..ec99c1d0 --- /dev/null +++ b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift @@ -0,0 +1,150 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI + +class SubtitlesViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + tabBarItem.title = "Subtitles" + + } + + + 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 + contentView.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + 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 + + } + + // + // + // func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + // return subtitleTrackArray.count + // } + // + // func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + // let cell = UITableViewCell() + // let subtitle = subtitleTrackArray[indexPath.row] + // cell.textLabel?.text = subtitle.name + // + // let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: (27), weight: .bold))?.withRenderingMode(.alwaysOriginal).withTintColor(.white) + // cell.imageView?.image = image + // + // if selectedTrack != subtitle.id { + // cell.imageView?.isHidden = true + // } + // else { + // selectedTrackCellRow = indexPath.row + // } + // + // return cell + // } + // + // func tableView(_ tableView: UITableView, didUpdateFocusIn context: UITableViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { + // + // if let path = context.nextFocusedIndexPath { + // if path.row == selectedTrackCellRow { + // let cell : UITableViewCell = tableView.cellForRow(at: path)! + // cell.imageView?.image = cell.imageView?.image?.withTintColor(.black) + // } + // } + // + // if let path = context.previouslyFocusedIndexPath { + // if path.row == selectedTrackCellRow { + // let cell : UITableViewCell = tableView.cellForRow(at: path)! + // cell.imageView?.image = cell.imageView?.image?.withTintColor(.white) + // } + // } + // + // } + // + // + // func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + // let oldPath = IndexPath(row: selectedTrackCellRow, section: 0) + // if let oldCell : UITableViewCell = tableView.cellForRow(at: oldPath) { + // oldCell.imageView?.isHidden = true + // } + // + // let cell : UITableViewCell = tableView.cellForRow(at: indexPath)! + // cell.imageView?.isHidden = false + // cell.imageView?.image = cell.imageView?.image?.withTintColor(.black) + // + // selectedTrack = Int32(subtitleTrackArray[indexPath.row].id) + // selectedTrackCellRow = indexPath.row + //// infoTabBar?.videoPlayer?.subtitleTrackChanged(newTrackID: selectedTrack) + // print("setting new subtitle") + // tableView.deselectRow(at: indexPath, animated: false) + // + // } + // + // func numberOfSections(in tableView: UITableView) -> Int { + // return 1 + // } + // + // + + + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} + +struct SubtitleView: View { + + @State var selectedTrack : Int32 = -1 + @State var subtitleTrackArray: [Subtitle] = [] + + weak var delegate: VideoPlayerSettingsDelegate? + + + var body : some View { + NavigationView { + VStack() { + List(subtitleTrackArray, id: \.id) { track in + Button(action: { + delegate?.selectNew(subtitleTrack: track.id) + selectedTrack = track.id + }, label: { + HStack(spacing: 10){ + if track.id == selectedTrack { + Image(systemName: "checkmark") + } + else { + Image(systemName: "checkmark") + .hidden() + } + Text(track.name) + } + }) + + } + } + .frame(width: 400) + + } + } + +} diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift new file mode 100644 index 00000000..0f4c5e39 --- /dev/null +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift @@ -0,0 +1,26 @@ +// +// VideoPlayer.swift +// CustomPlayer +// +// Created by Stephen Byatt on 25/5/21. +// + +import SwiftUI +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/VideoPlayerStoryboard.storyboard b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard new file mode 100644 index 00000000..1f7535cb --- /dev/null +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift new file mode 100644 index 00000000..173f1f97 --- /dev/null +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -0,0 +1,785 @@ +// +// VideoPlayerViewController.swift +// CustomPlayer +// +// Created by Stephen Byatt on 15/6/21. +// + +import TVUIKit +import TVVLCKit +import MediaPlayer +import JellyfinAPI +import Combine + +struct Subtitle { + var name: String + var id: Int32 + var url: URL? + var delivery: SubtitleDeliveryMethod + var codec: String +} + +struct AudioTrack { + var name: String + var id: Int32 +} + +class PlaybackItem: ObservableObject { + @Published var videoType: PlayMethod = .directPlay + @Published var videoUrl: URL = URL(string: "https://example.com")! +} + +protocol VideoPlayerSettingsDelegate: AnyObject { + func selectNew(audioTrack id: Int32) + func selectNew(subtitleTrack id: Int32) +} + +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 containerViewController: InfoTabBarViewController? + 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) + } + } + var selectedCaptionTrack: Int32 = -1 { + didSet { + 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 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") + { + // 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 + { + 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() + 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 + + transportBarView.layer.cornerRadius = CGFloat(5) + + setupGestures() + + fetchVideo() + + setupNowPlayingCC() + + mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16) + + } + + func fetchVideo() { + // Fetch max bitrate from UserDefaults depending on current connection mode + let defaults = UserDefaults.standard + let maxBitrate = defaults.integer(forKey: "InNetworkBandwidth") + + // Build a device profile + let builder = DeviceProfileBuilder() + builder.setMaxBitrate(bitrate: maxBitrate) + let profile = builder.buildProfile() + + let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) + + DispatchQueue.global(qos: .userInitiated).async { [self] in + // delegate?.showLoadingView(self) + MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) + .sink(receiveCompletion: { result in + print(result) + }, receiveValue: { [self] response in + + videoContentView.setNeedsLayout() + videoContentView.setNeedsDisplay() + + playSessionId = response.playSessionId ?? "" + + let mediaSource = response.mediaSources!.first.self! + + let item = PlaybackItem() + let streamURL : URL? + + // Item is being transcoded by request of server + if mediaSource.transcodingUrl != nil + { + item.videoType = .transcode + streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(mediaSource.transcodingUrl!)") + } + // Item will be directly played by the client + 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: "") + subtitleTrackArray.append(disableSubtitleTrack) + + // Loop through media streams and add to array + for stream in mediaSource.mediaStreams! { + + if stream.type == .subtitle { + var deliveryUrl: URL? = nil + + 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") + + 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!, 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" { + shouldHaveSubtitleTracks = shouldHaveSubtitleTracks + 1 + 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() + + activityIndicator.isHidden = true + loading = false + + }) + .store(in: &cancellables) + + + } + } + + func setupNowPlayingCC() { + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.playCommand.isEnabled = true + commandCenter.pauseCommand.isEnabled = true + commandCenter.seekForwardCommand.isEnabled = true + commandCenter.seekBackwardCommand.isEnabled = true + commandCenter.changePlaybackPositionCommand.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 { + self.mediaPlayer.jumpForward(Int32(offset)/1000) + } else { + self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000) + } + self.sendProgressReport(eventName: "unpause") + + return .success + } else { + return .commandFailed + } + } + + + MPNowPlayingInfoCenter.default().nowPlayingInfo = [ + MPMediaItemPropertyTitle: "TestVideo", + MPNowPlayingInfoPropertyPlaybackRate : 1.0, + MPNowPlayingInfoPropertyMediaType : AVMediaType.video, + MPMediaItemPropertyPlaybackDuration : manifest.runTimeTicks ?? 0 / 10_000_000, + MPNowPlayingInfoPropertyElapsedPlaybackTime : mediaPlayer.time.intValue/1000 + ] + + UIApplication.shared.beginReceivingRemoteControlEvents() + } + + + + // 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 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") + + 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) + } + + func play () { + playing = true + mediaPlayer.play() + + 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: { + self.scrubberView.frame = CGRect(x: self.initialSeekPos, y: self.scrubberView.frame.minY, width: 2, height: self.scrubberView.frame.height) + }) + seeking = false + + } + + 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)]; + view.addGestureRecognizer(playPauseGesture) + + let selectGesture = UITapGestureRecognizer(target: self, action: #selector(self.selectButtonTapped)) + let selectType = UIPress.PressType.select + 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)]; + 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) { + + // Dismiss info panel + if showingInfoPanel { + if focusedOnTabBar { + toggleInfoContainer() + } + return + } + + // Cancel seek and move back to initial position + 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 + { + // Dismiss view + mediaPlayer.stop() + sendStopReport() + self.navigationController?.popViewController(animated: true) + } + } + + @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) { + 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") + switch swipe.direction { + case .left: + print("swiped left") + mediaPlayer.pause() + // player.seek(to: CMTime(value: Int64(self.currentSeconds) + 10, timescale: 1)) + mediaPlayer.play() + case .right: + print("swiped right") + mediaPlayer.pause() + // player.seek(to: CMTime(value: Int64(self.currentSeconds) + 10, timescale: 1)) + mediaPlayer.play() + case .up: + break + case .down: + break + 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) { + 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) + }, receiveValue: { _ in + print("Playback progress report sent!") + }) + .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) + }, receiveValue: { _ in + print("Playback stop report sent!") + }) + .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) + }, receiveValue: { _ in + print("Playback start report sent!") + }) + .store(in: &cancellables) + } + + + // MARK: VLC Delegate + + func mediaPlayerStateChanged(_ aNotification: Notification!) { + let currentState: VLCMediaPlayerState = mediaPlayer.state + switch currentState { + case .buffering: + print("Video is buffering") + loading = true + activityIndicator.isHidden = false + activityIndicator.startAnimating() + mediaPlayer.pause() + usleep(10000) + mediaPlayer.play() + 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") + loading = false + sendProgressReport(eventName: "unpause") + DispatchQueue.main.async { [self] in + activityIndicator.isHidden = true + activityIndicator.stopAnimating() + } + playing = true + break + case .error: + print("error") + break + case .esAdded: + print("esAdded") + break + default: + print("default") + break + + } + + } + + // Move time along transport bar + func mediaPlayerTimeChanged(_ aNotification: Notification!) { + + if loading { + loading = false + DispatchQueue.main.async { [self] in + activityIndicator.isHidden = true + activityIndicator.stopAnimating() + } + } + + 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 + UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: { + self.controlsView.alpha = 0.0 + }, completion: { (_: Bool) in + self.controlsView.isHidden = true + self.controlsView.alpha = 1 + }) + 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() + formatter.unitsStyle = .positional + formatter.allowedUnits = seconds >= 3600 ? + [.hour, .minute, .second] : + [.minute, .second] + 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") + return true + } +} + +extension Comparable { + func clamped(to limits: ClosedRange) -> Self { + return min(max(self, limits.lowerBound), limits.upperBound) + } +} diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 5d0f0316..86b882cb 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -7,6 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + 09389CBE26814DF600AE350E /* VideoPlayerStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 09389CB726814DF500AE350E /* VideoPlayerStoryboard.storyboard */; }; + 09389CBF26814DF600AE350E /* MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CB826814DF600AE350E /* MediaInfoView.swift */; }; + 09389CC026814DF600AE350E /* SubtitlesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CB926814DF600AE350E /* SubtitlesView.swift */; }; + 09389CC126814DF600AE350E /* InfoTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CBA26814DF600AE350E /* InfoTabBarViewController.swift */; }; + 09389CC226814DF600AE350E /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CBB26814DF600AE350E /* VideoPlayer.swift */; }; + 09389CC326814DF600AE350E /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CBC26814DF600AE350E /* VideoPlayerViewController.swift */; }; + 09389CC426814DF600AE350E /* AudioView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CBD26814DF600AE350E /* AudioView.swift */; }; + 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E4267ABD5C005D8AB9 /* MainTabView.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; }; @@ -181,6 +189,13 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 09389CB726814DF500AE350E /* VideoPlayerStoryboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = VideoPlayerStoryboard.storyboard; sourceTree = ""; }; + 09389CB826814DF600AE350E /* MediaInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaInfoView.swift; sourceTree = ""; }; + 09389CB926814DF600AE350E /* SubtitlesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubtitlesView.swift; sourceTree = ""; }; + 09389CBA26814DF600AE350E /* InfoTabBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoTabBarViewController.swift; sourceTree = ""; }; + 09389CBB26814DF600AE350E /* VideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; + 09389CBC26814DF600AE350E /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = ""; }; + 09389CBD26814DF600AE350E /* AudioView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioView.swift; sourceTree = ""; }; 3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.debug.xcconfig"; sourceTree = ""; }; 3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 531690E4267ABD5C005D8AB9 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; @@ -345,6 +360,20 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 09389CB626814DD700AE350E /* VideoPlayer */ = { + isa = PBXGroup; + children = ( + 09389CBD26814DF600AE350E /* AudioView.swift */, + 09389CBA26814DF600AE350E /* InfoTabBarViewController.swift */, + 09389CB826814DF600AE350E /* MediaInfoView.swift */, + 09389CB926814DF600AE350E /* SubtitlesView.swift */, + 09389CBB26814DF600AE350E /* VideoPlayer.swift */, + 09389CB726814DF500AE350E /* VideoPlayerStoryboard.storyboard */, + 09389CBC26814DF600AE350E /* VideoPlayerViewController.swift */, + ); + path = VideoPlayer; + sourceTree = ""; + }; 532175392671BCED005491E6 /* ViewModels */ = { isa = PBXGroup; children = ( @@ -371,6 +400,7 @@ 535870612669D21600D05A09 /* JellyfinPlayer tvOS */ = { isa = PBXGroup; children = ( + 09389CB626814DD700AE350E /* VideoPlayer */, 536D3D77267BB9650004248C /* Components */, 53ABFDDA267972BF00886593 /* JellyfinPlayer tvOS.entitlements */, 531690F9267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift */, @@ -459,7 +489,6 @@ 5377CBF8263B596B003A4E83 /* Assets.xcassets */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, 5389276D263C25100035E14B /* ContinueWatchingView.swift */, - 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */, 5377CC02263B596B003A4E83 /* Info.plist */, 535BAE9E2649E569005FA86D /* ItemView.swift */, @@ -560,6 +589,7 @@ 62EC352A26766657000E9F2D /* Singleton */ = { isa = PBXGroup; children = ( + 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, 62EC352B26766675000E9F2D /* ServerEnvironment.swift */, 62EC352E267666A5000E9F2D /* SessionManager.swift */, 536D3D73267BA8170004248C /* BackgroundManager.swift */, @@ -728,6 +758,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 09389CBE26814DF600AE350E /* VideoPlayerStoryboard.storyboard in Resources */, 5358706A2669D21700D05A09 /* Preview Assets.xcassets in Resources */, 535870672669D21700D05A09 /* Assets.xcassets in Resources */, 5358707E2669D64F00D05A09 /* bitrates.json in Resources */, @@ -862,6 +893,7 @@ 53ABFDE9267974EF00886593 /* HomeViewModel.swift in Sources */, 62EC352D26766675000E9F2D /* ServerEnvironment.swift in Sources */, 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */, + 09389CC126814DF600AE350E /* InfoTabBarViewController.swift in Sources */, 53ABFDDE267974E300886593 /* SplashView.swift in Sources */, 53ABFDE8267974EF00886593 /* SplashViewModel.swift in Sources */, 62E632DE267D2E170063E547 /* LatestMediaViewModel.swift in Sources */, @@ -879,6 +911,7 @@ 62E632F4267D54030063E547 /* DetailItemViewModel.swift in Sources */, 6267B3D826710B9800A7371D /* CollectionExtensions.swift in Sources */, 62E632E7267D3F5B0063E547 /* EpisodeItemViewModel.swift in Sources */, + 09389CBF26814DF600AE350E /* MediaInfoView.swift in Sources */, 535870A52669D8AE00D05A09 /* ParallaxHeader.swift in Sources */, 531690F0267ABF72005D8AB9 /* NextUpView.swift in Sources */, 535870A72669D8AE00D05A09 /* MultiSelectorView.swift in Sources */, @@ -891,16 +924,21 @@ 5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */, 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, 53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */, + 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, 535870A62669D8AE00D05A09 /* LazyView.swift in Sources */, 5321753E2671DE9C005491E6 /* Typings.swift in Sources */, 53ABFDEB2679753200886593 /* ConnectToServerView.swift in Sources */, + 09389CC026814DF600AE350E /* SubtitlesView.swift in Sources */, 536D3D76267BA9BB0004248C /* MainTabViewModel.swift in Sources */, 536D3D74267BA8170004248C /* BackgroundManager.swift in Sources */, + 09389CC226814DF600AE350E /* VideoPlayer.swift in Sources */, 535870632669D21600D05A09 /* JellyfinPlayer_tvOSApp.swift in Sources */, 53ABFDE4267974EF00886593 /* LibraryListViewModel.swift in Sources */, 5364F456266CA0DC0026ECBA /* APIExtensions.swift in Sources */, + 09389CC326814DF600AE350E /* VideoPlayerViewController.swift in Sources */, 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, 535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */, + 09389CC426814DF600AE350E /* AudioView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/JellyfinPlayer/DeviceProfileBuilder.swift b/Shared/Singleton/DeviceProfileBuilder.swift similarity index 100% rename from JellyfinPlayer/DeviceProfileBuilder.swift rename to Shared/Singleton/DeviceProfileBuilder.swift From cd4cb28adec1c3dbb07ac1494db2055b9a0eaed2 Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Tue, 22 Jun 2021 11:07:44 +1000 Subject: [PATCH 2/8] Now playing centre changes --- .../VideoPlayerViewController.swift | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 173f1f97..200961e9 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -236,7 +236,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, // If no default audio tracks select the first one if selectedAudioTrack == -1 && !audioTrackArray.isEmpty{ selectedAudioTrack = audioTrackArray.first!.id - } + } self.sendPlayReport() @@ -291,6 +291,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, commandCenter.seekForwardCommand.isEnabled = true commandCenter.seekBackwardCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.isEnabled = true + commandCenter.enableLanguageOptionCommand.isEnabled = true // Add handler for Pause Command commandCenter.pauseCommand.addTarget { _ in @@ -340,18 +341,58 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } } +// commandCenter.enableLanguageOptionCommand.addTarget { [weak self](remoteEvent) in +// guard let self = self else {return .commandFailed} +// +// +// +// } - MPNowPlayingInfoCenter.default().nowPlayingInfo = [ - MPMediaItemPropertyTitle: "TestVideo", - MPNowPlayingInfoPropertyPlaybackRate : 1.0, - MPNowPlayingInfoPropertyMediaType : AVMediaType.video, - MPMediaItemPropertyPlaybackDuration : manifest.runTimeTicks ?? 0 / 10_000_000, - MPNowPlayingInfoPropertyElapsedPlaybackTime : mediaPlayer.time.intValue/1000 - ] + 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! + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 + nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks + + if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) { + let artworkImage = UIImage(data: imageData as Data) + nowPlayingInfo[MPMediaItemPropertyArtwork] = artworkImage + } + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + UIApplication.shared.beginReceivingRemoteControlEvents() } + func updateNowPlayingCenter(time : Double?, playing : Bool?) { + + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]() + + if let playing = playing { + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = playing ? 1.0 : 0.0 + } + if let time = time { + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = time + } + + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + + } + // Grabs a refference to the info panel view controller @@ -381,6 +422,8 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, 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) @@ -390,6 +433,8 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, playing = true mediaPlayer.play() + self.updateNowPlayingCenter(time: nil, playing: true) + self.sendProgressReport(eventName: "unpause") animateScrubber() @@ -696,6 +741,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, activityIndicator.isHidden = true activityIndicator.stopAnimating() } + updateNowPlayingCenter(time: nil, playing: true) } let time = mediaPlayer.position From f05eee3593e9566320e219389cac20517a6383c6 Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Tue, 22 Jun 2021 14:18:01 +1000 Subject: [PATCH 3/8] Move duplicate objects in both Video Players to a shared file --- .../VideoPlayerViewController.swift | 19 ------------ JellyfinPlayer.xcodeproj/project.pbxproj | 6 ++++ JellyfinPlayer/VideoPlayer.swift | 17 ----------- Shared/ViewModels/VideoPlayerModel.swift | 29 +++++++++++++++++++ 4 files changed, 35 insertions(+), 36 deletions(-) create mode 100644 Shared/ViewModels/VideoPlayerModel.swift diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 200961e9..3a4b8bff 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -11,24 +11,6 @@ import MediaPlayer import JellyfinAPI import Combine -struct Subtitle { - var name: String - var id: Int32 - var url: URL? - var delivery: SubtitleDeliveryMethod - var codec: String -} - -struct AudioTrack { - var name: String - var id: Int32 -} - -class PlaybackItem: ObservableObject { - @Published var videoType: PlayMethod = .directPlay - @Published var videoUrl: URL = URL(string: "https://example.com")! -} - protocol VideoPlayerSettingsDelegate: AnyObject { func selectNew(audioTrack id: Int32) func selectNew(subtitleTrack id: Int32) @@ -167,7 +149,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) DispatchQueue.global(qos: .userInitiated).async { [self] in - // delegate?.showLoadingView(self) MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) .sink(receiveCompletion: { result in print(result) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 86b882cb..1662a1be 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ 09389CC326814DF600AE350E /* VideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CBC26814DF600AE350E /* VideoPlayerViewController.swift */; }; 09389CC426814DF600AE350E /* AudioView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CBD26814DF600AE350E /* AudioView.swift */; }; 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; + 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; }; + 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09389CC626819B4500AE350E /* VideoPlayerModel.swift */; }; 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E4267ABD5C005D8AB9 /* MainTabView.swift */; }; 531690E7267ABD79005D8AB9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690E6267ABD79005D8AB9 /* HomeView.swift */; }; 531690ED267ABF46005D8AB9 /* ContinueWatchingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531690EB267ABF46005D8AB9 /* ContinueWatchingView.swift */; }; @@ -196,6 +198,7 @@ 09389CBB26814DF600AE350E /* VideoPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 09389CBC26814DF600AE350E /* VideoPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewController.swift; sourceTree = ""; }; 09389CBD26814DF600AE350E /* AudioView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioView.swift; sourceTree = ""; }; + 09389CC626819B4500AE350E /* VideoPlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerModel.swift; sourceTree = ""; }; 3773C07648173CE7FEC083D5 /* Pods-JellyfinPlayer iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JellyfinPlayer iOS.debug.xcconfig"; path = "Target Support Files/Pods-JellyfinPlayer iOS/Pods-JellyfinPlayer iOS.debug.xcconfig"; sourceTree = ""; }; 3F905C1D3D3A0C9E13E7A0BC /* Pods_JellyfinPlayer_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_JellyfinPlayer_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 531690E4267ABD5C005D8AB9 /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; @@ -393,6 +396,7 @@ 62E632EB267D410B0063E547 /* SeriesItemViewModel.swift */, 62E632EE267D43320063E547 /* LibraryFilterViewModel.swift */, 62E632F2267D54030063E547 /* DetailItemViewModel.swift */, + 09389CC626819B4500AE350E /* VideoPlayerModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -939,6 +943,7 @@ 531690FA267AD6EC005D8AB9 /* PlainNavigationLinkButton.swift in Sources */, 535870A32669D89F00D05A09 /* Model.xcdatamodeld in Sources */, 09389CC426814DF600AE350E /* AudioView.swift in Sources */, + 09389CC826819B4600AE350E /* VideoPlayerModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -995,6 +1000,7 @@ 625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, + 09389CC726819B4600AE350E /* VideoPlayerModel.swift in Sources */, 53AD124D267029D60094A276 /* SeriesItemView.swift in Sources */, 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */, 53EE24E6265060780068F029 /* LibrarySearchView.swift in Sources */, diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 73137cd0..4aadc9ad 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -13,29 +13,12 @@ import Combine import GoogleCast import SwiftyJSON -struct Subtitle { - var name: String - var id: Int32 - var url: URL? - var delivery: SubtitleDeliveryMethod - var codec: String -} - -struct AudioTrack { - var name: String - var id: Int32 -} enum PlayerDestination { case remote case local } -class PlaybackItem: ObservableObject { - @Published var videoType: PlayMethod = .directPlay - @Published var videoUrl: URL = URL(string: "https://example.com")! -} - protocol PlayerViewControllerDelegate: AnyObject { func hideLoadingView(_ viewController: PlayerViewController) func showLoadingView(_ viewController: PlayerViewController) diff --git a/Shared/ViewModels/VideoPlayerModel.swift b/Shared/ViewModels/VideoPlayerModel.swift new file mode 100644 index 00000000..cb219a0d --- /dev/null +++ b/Shared/ViewModels/VideoPlayerModel.swift @@ -0,0 +1,29 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import SwiftUI +import JellyfinAPI + +struct Subtitle { + var name: String + var id: Int32 + var url: URL? + var delivery: SubtitleDeliveryMethod + var codec: String +} + +struct AudioTrack { + var name: String + var id: Int32 +} + +class PlaybackItem: ObservableObject { + @Published var videoType: PlayMethod = .directPlay + @Published var videoUrl: URL = URL(string: "https://example.com")! +} From 8f97d4e2e896d2c7564b825512a2c8cc3fdb9619 Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Tue, 22 Jun 2021 18:36:38 +1000 Subject: [PATCH 4/8] TvOS video player changes: Media info view changes Fix crash when trying to set now playing artwork --- .../VideoPlayer/MediaInfoView.swift | 106 +++++++++--------- .../VideoPlayerStoryboard.storyboard | 16 +-- .../VideoPlayerViewController.swift | 9 +- 3 files changed, 71 insertions(+), 60 deletions(-) diff --git a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift index 453e43e4..5c7364a1 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift @@ -38,63 +38,74 @@ struct MediaInfoView: View { var body: some View { if let item = item { - HStack { + 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() } - .padding(.leading, 200) + + VStack(alignment: .leading, spacing: 10) { + if item.type == "Episode" { + Text(item.seriesName ?? "Series") + .fontWeight(.bold) + + Text(item.name ?? "Episode") + .foregroundColor(.secondary) + } + else + { + Text(item.name ?? "Movie") + .fontWeight(.bold) + } - VStack(alignment: .leading) { + HStack(spacing: 10) { if item.type == "Episode" { - Text(item.seriesName!) - .font(.title3) - - Text("S\(item.parentIndexNumber ?? 0):E\(item.indexNumber ?? 0) • \(item.name!)") - .font(.headline) - .foregroundColor(.secondary) - } - else - { - Text(item.name!) - .font(.title3) + Text("S\(item.parentIndexNumber ?? 0) • E\(item.indexNumber ?? 0)") + + if let date = item.premiereDate! { + Text("•") + Text(formatDate(date: date)) + } + + } else if let year = item.productionYear { + Text(String(year)) } - HStack(spacing: 10) { - Text(String(item.productionYear!)) + if item.runTimeTicks != nil { + Text("•") + Text(item.getItemRuntime()) + } + + if let rating = item.officialRating { Text("•") - Text(formatRunningtime()) + 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)) - if item.officialRating != nil { - Text("•") - - Text("\(item.officialRating!)").font(.subheadline) - .fontWeight(.semibold) - .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) - .overlay(RoundedRectangle(cornerRadius: 2) - .stroke(Color.secondary, lineWidth: 1)) - - } } - .padding(.top) - .foregroundColor(.secondary) - - - Text(item.overview!) - .padding([.top, .trailing]) - .foregroundColor(.secondary) - - Spacer() } - + .foregroundColor(.secondary) + + if let overview = item.overview { + Text(overview) + .padding(.top) + .foregroundColor(.secondary) + } + + + Spacer() + } Spacer() - - + } - .frame(maxWidth: .infinity) + .padding(.leading, 350) + .padding(.trailing, 200) } else { EmptyView() @@ -102,16 +113,11 @@ struct MediaInfoView: View { } - func formatRunningtime() -> String { - let timeHMSFormatter: DateComponentsFormatter = { - let formatter = DateComponentsFormatter() - formatter.unitsStyle = .brief - formatter.allowedUnits = [.hour, .minute] - return formatter - }() + + func formatDate(date : Date) -> String{ + let formatter = DateFormatter() + formatter.dateFormat = "d MMM yyyy" - let text = timeHMSFormatter.string(from: Double(item!.runTimeTicks! / 10_000_000)) ?? "" - - return text + return formatter.string(from: date) } } diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard index 1f7535cb..df8fad54 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard @@ -80,17 +80,17 @@ + + + + + + + + - - - - - - - - diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 3a4b8bff..0029ab6f 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -349,8 +349,13 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) { - let artworkImage = UIImage(data: imageData as Data) - nowPlayingInfo[MPMediaItemPropertyArtwork] = artworkImage + if let artworkImage = UIImage(data: imageData as Data) { + let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (size) -> UIImage in + return artworkImage + }) + nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork + print("set artwork") + } } MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo From a8aefd186f1bb4d4fce0e7c1b6b08f46a7ee4822 Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Tue, 22 Jun 2021 23:15:32 +1000 Subject: [PATCH 5/8] Info panel changes height based on current selection --- .../VideoPlayer/AudioView.swift | 5 +- .../InfoTabBarViewController.swift | 63 +++++++++++++++++++ .../VideoPlayer/MediaInfoView.swift | 4 ++ .../VideoPlayer/SubtitlesView.swift | 4 ++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift index 4a52baab..abccf5da 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift @@ -11,6 +11,9 @@ import SwiftUI class AudioViewController: UIViewController { + var height : CGFloat = 400 + + override func viewDidLoad() { super.viewDidLoad() @@ -72,7 +75,7 @@ struct AudioView: View { } } .frame(width: 400) - + .frame(maxHeight: 400) } } } diff --git a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift index cf96178e..518bcda9 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift @@ -14,6 +14,8 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate var subtitleViewController : SubtitlesViewController? = nil var audioViewController : AudioViewController? = nil var mediaInfoController : MediaInfoViewController? = nil + var infoContainerPos : CGRect? = nil + var tabBarHeight : CGFloat = 0 override func viewDidLoad() { @@ -22,6 +24,9 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate audioViewController = AudioViewController() subtitleViewController = SubtitlesViewController() + tabBarHeight = tabBar.frame.size.height + + viewControllers = [mediaInfoController!, audioViewController!, subtitleViewController!] } @@ -34,6 +39,64 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate subtitleViewController?.prepareSubtitleView(subtitleTracks: subtitleTracks, selectedTrack: selectedSubtitleTrack, delegate: delegate) + if let videoPlayer = videoPlayer { + infoContainerPos = CGRect(x: 88, y: 57, width: videoPlayer.infoViewContainer.frame.width, height: videoPlayer.infoViewContainer.frame.height) + + } + + + + } + + override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { + guard let pos = infoContainerPos else { + return + } + + print(item.title!) + + switch item.title { + case "Audio": + if var height = audioViewController?.height { + print(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) + + } + + + } + break + case "Info": + if var height = mediaInfoController?.height { + print(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) + + } + + } + break + case "Subtitles": + if var height = subtitleViewController?.height{ + print(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) + + } + + } + break + default: + break + } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { diff --git a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift index 5c7364a1..2c3cc8e9 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift @@ -13,6 +13,8 @@ import JellyfinAPI class MediaInfoViewController: UIViewController { private var contentView: UIHostingController! + var height : CGFloat = 0 + override func viewDidLoad() { super.viewDidLoad() @@ -30,6 +32,8 @@ class MediaInfoViewController: UIViewController { contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true + height = self.view.frame.height + } } diff --git a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift index ec99c1d0..66f9b0ff 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift @@ -11,6 +11,9 @@ import SwiftUI class SubtitlesViewController: UIViewController { + var height : CGFloat = 400 + + override func viewDidLoad() { super.viewDidLoad() @@ -143,6 +146,7 @@ struct SubtitleView: View { } } .frame(width: 400) + .frame(maxHeight: 400) } } From 77e6777b91f4157f3b14454d2fe8df6992f9ad8e Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Wed, 23 Jun 2021 00:12:47 +1000 Subject: [PATCH 6/8] Add blurred background to info panel --- .../VideoPlayer/AudioView.swift | 2 +- .../InfoTabBarViewController.swift | 25 ++++++++++++++++--- .../VideoPlayer/MediaInfoView.swift | 2 +- .../VideoPlayer/SubtitlesView.swift | 2 +- .../VideoPlayerStoryboard.storyboard | 1 - .../VideoPlayerViewController.swift | 8 ++++++ 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift index abccf5da..c3e8d2aa 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift @@ -11,7 +11,7 @@ import SwiftUI class AudioViewController: UIViewController { - var height : CGFloat = 400 + var height : CGFloat = 420 override func viewDidLoad() { diff --git a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift index 518bcda9..3701521a 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift @@ -18,17 +18,36 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate var tabBarHeight : CGFloat = 0 +// override func viewWillAppear(_ animated: Bool) { +// tabBar.standardAppearance.backgroundColor = .clear +// tabBar.standardAppearance.backgroundImage = UIImage() +// tabBar.standardAppearance.backgroundEffect = .none +// tabBar.barTintColor = .clear +// for view in tabBar.subviews { +// print(view.description) +//// if view.description.contains("_UIBarBackground") { +//// +//// view.removeFromSuperview() +//// } +// } +// +// } +// override func viewDidLoad() { super.viewDidLoad() mediaInfoController = MediaInfoViewController() audioViewController = AudioViewController() subtitleViewController = SubtitlesViewController() - - tabBarHeight = tabBar.frame.size.height - 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) { diff --git a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift index 2c3cc8e9..364a90eb 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift @@ -109,7 +109,7 @@ struct MediaInfoView: View { } .padding(.leading, 350) - .padding(.trailing, 200) + .padding(.trailing, 125) } else { EmptyView() diff --git a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift index 66f9b0ff..e2ddedbd 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift @@ -11,7 +11,7 @@ import SwiftUI class SubtitlesViewController: UIViewController { - var height : CGFloat = 400 + var height : CGFloat = 420 override func viewDidLoad() { diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard index df8fad54..915fb742 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard @@ -83,7 +83,6 @@ - diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 0029ab6f..146958df 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -124,6 +124,14 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, infoViewContainer.center = infoPanelHiddenPoint infoViewContainer.layer.cornerRadius = 40 + let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) + blurEffectView.frame = infoViewContainer.bounds + blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + blurEffectView.layer.cornerRadius = 40 + blurEffectView.clipsToBounds = true + infoViewContainer.addSubview(blurEffectView) + infoViewContainer.sendSubviewToBack(blurEffectView) + transportBarView.layer.cornerRadius = CGFloat(5) setupGestures() From 099e138eb50bec270cebbf29a5f244392e9b90b8 Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:11:33 +1000 Subject: [PATCH 7/8] Refactored some force unwraps --- .../VideoPlayer/AudioView.swift | 10 --- .../InfoTabBarViewController.swift | 9 --- .../VideoPlayer/MediaInfoView.swift | 5 +- .../VideoPlayer/SubtitlesView.swift | 81 ------------------- .../VideoPlayerViewController.swift | 41 +++++----- 5 files changed, 26 insertions(+), 120 deletions(-) diff --git a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift index c3e8d2aa..66e3a035 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift @@ -33,16 +33,6 @@ class AudioViewController: UIViewController { } - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - } struct AudioView: View { diff --git a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift index 3701521a..689ed895 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift @@ -72,15 +72,10 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate return } - print(item.title!) - switch item.title { case "Audio": if var height = audioViewController?.height { - print(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) @@ -91,8 +86,6 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate break case "Info": if var height = mediaInfoController?.height { - print(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) @@ -103,8 +96,6 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate break case "Subtitles": if var height = subtitleViewController?.height{ - print(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) diff --git a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift index 364a90eb..b210c572 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift @@ -45,7 +45,8 @@ struct MediaInfoView: View { 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) + 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() } @@ -68,7 +69,7 @@ struct MediaInfoView: View { if item.type == "Episode" { Text("S\(item.parentIndexNumber ?? 0) • E\(item.indexNumber ?? 0)") - if let date = item.premiereDate! { + if let date = item.premiereDate { Text("•") Text(formatDate(date: date)) } diff --git a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift index e2ddedbd..1ea20c2c 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift @@ -21,7 +21,6 @@ class SubtitlesViewController: UIViewController { } - func prepareSubtitleView(subtitleTracks: [Subtitle], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) { let contentView = UIHostingController(rootView: SubtitleView(selectedTrack: selectedTrack, subtitleTrackArray: subtitleTracks, delegate: delegate)) @@ -33,86 +32,6 @@ class SubtitlesViewController: UIViewController { contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true } - - // - // - // func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - // return subtitleTrackArray.count - // } - // - // func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - // let cell = UITableViewCell() - // let subtitle = subtitleTrackArray[indexPath.row] - // cell.textLabel?.text = subtitle.name - // - // let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: (27), weight: .bold))?.withRenderingMode(.alwaysOriginal).withTintColor(.white) - // cell.imageView?.image = image - // - // if selectedTrack != subtitle.id { - // cell.imageView?.isHidden = true - // } - // else { - // selectedTrackCellRow = indexPath.row - // } - // - // return cell - // } - // - // func tableView(_ tableView: UITableView, didUpdateFocusIn context: UITableViewFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { - // - // if let path = context.nextFocusedIndexPath { - // if path.row == selectedTrackCellRow { - // let cell : UITableViewCell = tableView.cellForRow(at: path)! - // cell.imageView?.image = cell.imageView?.image?.withTintColor(.black) - // } - // } - // - // if let path = context.previouslyFocusedIndexPath { - // if path.row == selectedTrackCellRow { - // let cell : UITableViewCell = tableView.cellForRow(at: path)! - // cell.imageView?.image = cell.imageView?.image?.withTintColor(.white) - // } - // } - // - // } - // - // - // func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // let oldPath = IndexPath(row: selectedTrackCellRow, section: 0) - // if let oldCell : UITableViewCell = tableView.cellForRow(at: oldPath) { - // oldCell.imageView?.isHidden = true - // } - // - // let cell : UITableViewCell = tableView.cellForRow(at: indexPath)! - // cell.imageView?.isHidden = false - // cell.imageView?.image = cell.imageView?.image?.withTintColor(.black) - // - // selectedTrack = Int32(subtitleTrackArray[indexPath.row].id) - // selectedTrackCellRow = indexPath.row - //// infoTabBar?.videoPlayer?.subtitleTrackChanged(newTrackID: selectedTrack) - // print("setting new subtitle") - // tableView.deselectRow(at: indexPath, animated: false) - // - // } - // - // func numberOfSections(in tableView: UITableView) -> Int { - // return 1 - // } - // - // - - - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - } struct SubtitleView: View { diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 146958df..3e6bcab2 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -140,6 +140,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, setupNowPlayingCC() + // Adjust subtitle size mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16) } @@ -154,10 +155,14 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, builder.setMaxBitrate(bitrate: maxBitrate) let profile = builder.buildProfile() - let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) + 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: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) + 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 @@ -167,16 +172,17 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, playSessionId = response.playSessionId ?? "" - let mediaSource = response.mediaSources!.first.self! + 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 mediaSource.transcodingUrl != nil - { + if let transcodiungUrl = mediaSource.transcodingUrl { item.videoType = .transcode - streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(mediaSource.transcodingUrl!)") + streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(transcodiungUrl)")! } // Item will be directly played by the client else @@ -185,7 +191,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, 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! + item.videoUrl = streamURL let disableSubtitleTrack = Subtitle(name: "None", id: -1, url: nil, delivery: .embed, codec: "") subtitleTrackArray.append(disableSubtitleTrack) @@ -223,7 +229,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } // If no default audio tracks select the first one - if selectedAudioTrack == -1 && !audioTrackArray.isEmpty{ + if selectedAudioTrack == -1 && !audioTrackArray.isEmpty { selectedAudioTrack = audioTrackArray.first!.id } @@ -349,8 +355,8 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } var nowPlayingInfo = [String: Any]() - - nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name! + + nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video" nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks @@ -359,10 +365,9 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, 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 - return artworkImage + return artworkImage }) - nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork - print("set artwork") + nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork } } @@ -571,14 +576,14 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, switch swipe.direction { case .left: print("swiped left") - mediaPlayer.pause() +// mediaPlayer.pause() // player.seek(to: CMTime(value: Int64(self.currentSeconds) + 10, timescale: 1)) - mediaPlayer.play() +// mediaPlayer.play() case .right: print("swiped right") - mediaPlayer.pause() +// mediaPlayer.pause() // player.seek(to: CMTime(value: Int64(self.currentSeconds) + 10, timescale: 1)) - mediaPlayer.play() +// mediaPlayer.play() case .up: break case .down: From 911bd602ab5ec359211ab4461eb3fc48c3106e28 Mon Sep 17 00:00:00 2001 From: Stephen Byatt <47413006+stephenb10@users.noreply.github.com> Date: Wed, 23 Jun 2021 13:36:38 +1000 Subject: [PATCH 8/8] Fix info panel hiding with transport bar after 5 seconds Fix flashing loading spinner on initial loading of video --- .../VideoPlayer/InfoTabBarViewController.swift | 2 +- .../VideoPlayer/VideoPlayerStoryboard.storyboard | 14 +++++++------- .../VideoPlayer/VideoPlayerViewController.swift | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift index 689ed895..2bf5d387 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift @@ -59,7 +59,7 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate subtitleViewController?.prepareSubtitleView(subtitleTracks: subtitleTracks, selectedTrack: selectedSubtitleTrack, delegate: delegate) if let videoPlayer = videoPlayer { - infoContainerPos = CGRect(x: 88, y: 57, width: videoPlayer.infoViewContainer.frame.width, height: videoPlayer.infoViewContainer.frame.height) + infoContainerPos = CGRect(x: 88, y: 87, width: videoPlayer.infoViewContainer.frame.width, height: videoPlayer.infoViewContainer.frame.height) } diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard index 915fb742..eee0e161 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerStoryboard.storyboard @@ -80,16 +80,16 @@ - - - - - - - + + + + + + + diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 3e6bcab2..f6a370c9 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -269,9 +269,6 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, setupInfoPanel() - activityIndicator.isHidden = true - loading = false - }) .store(in: &cancellables) @@ -449,7 +446,10 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, scrubLabel.isHidden = true UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: { self.scrubberView.frame = CGRect(x: self.initialSeekPos, y: self.scrubberView.frame.minY, width: 2, height: self.scrubberView.frame.height) - }) + }) { _ in + self.scrubLabel.frame = CGRect(x: (self.initialSeekPos - self.scrubLabel.frame.width/2), y: self.scrubLabel.frame.minY, width: self.scrubLabel.frame.width, height: self.scrubLabel.frame.height) + self.scrubLabel.text = self.currentTimeLabel.text + } seeking = false }