From 208bec783a4728c8fd81d208f96e2fbfbeb5a12d Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Wed, 26 May 2021 21:24:01 -0400 Subject: [PATCH 1/6] initial --- JellyfinPlayer.xcodeproj/project.pbxproj | 6 +- JellyfinPlayer/Info.plist | 2 +- JellyfinPlayer/ItemView.swift | 13 +- JellyfinPlayer/VLCPlayer.swift | 98 ---- .../VideoPlayerViewRefactored.swift | 241 +++++---- JellyfinPlayer/Views/VLCPlayer.swift | 498 ++++++++++++++++++ JellyfinPlayer/Views/VideoPlayer.storyboard | 156 ++++++ 7 files changed, 799 insertions(+), 215 deletions(-) delete mode 100644 JellyfinPlayer/VLCPlayer.swift create mode 100644 JellyfinPlayer/Views/VLCPlayer.swift create mode 100644 JellyfinPlayer/Views/VideoPlayer.storyboard diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 82fa766e..78e7efd9 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 5302F82A2658791C00647A2E /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 5302F8292658791C00647A2E /* Sentry */; }; 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; + 53313B90265EEA6D00947AA3 /* VideoPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */; }; 5335256E265E8D5A006CCA86 /* VideoPlayerViewRefactored.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5335256D265E8D5A006CCA86 /* VideoPlayerViewRefactored.swift */; }; 53352571265EA0A0006CCA86 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53352570265EA0A0006CCA86 /* Introspect */; }; 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; }; @@ -72,6 +73,7 @@ /* Begin PBXFileReference section */ 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = ""; }; + 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = VideoPlayer.storyboard; sourceTree = ""; }; 5335256D265E8D5A006CCA86 /* VideoPlayerViewRefactored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewRefactored.swift; sourceTree = ""; }; 5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; 535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; @@ -169,7 +171,6 @@ 53E4E648263F725B00F67C6B /* MultiSelector.swift */, 535BAE9E2649E569005FA86D /* ItemView.swift */, 53A089CF264DA9DA00D57806 /* MovieItemView.swift */, - 535BAEA4264A151C005FA86D /* VLCPlayer.swift */, 535BAEA6264A18AA005FA86D /* VideoPlayerView.swift */, 53EE24E5265060780068F029 /* LibrarySearchView.swift */, 53987CA326572C1300E7EA70 /* SeasonItemView.swift */, @@ -200,7 +201,9 @@ AE8C3150265D5FE1008AA076 /* Views */ = { isa = PBXGroup; children = ( + 535BAEA4264A151C005FA86D /* VLCPlayer.swift */, 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, + 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, ); path = Views; sourceTree = ""; @@ -308,6 +311,7 @@ buildActionMask = 2147483647; files = ( 5377CBFC263B596B003A4E83 /* Preview Assets.xcassets in Resources */, + 53313B90265EEA6D00947AA3 /* VideoPlayer.storyboard in Resources */, AE8C3159265D6F90008AA076 /* bitrates.json in Resources */, 5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */, ); diff --git a/JellyfinPlayer/Info.plist b/JellyfinPlayer/Info.plist index de1bc59e..1b0f697e 100644 --- a/JellyfinPlayer/Info.plist +++ b/JellyfinPlayer/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - SwiftFin + Jellyfin SUI CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/JellyfinPlayer/ItemView.swift b/JellyfinPlayer/ItemView.swift index 8d6a8067..b94254be 100644 --- a/JellyfinPlayer/ItemView.swift +++ b/JellyfinPlayer/ItemView.swift @@ -16,6 +16,7 @@ class ItemPlayback: ObservableObject { struct ItemView: View { var item: ResumeItem; @StateObject private var playback: ItemPlayback = ItemPlayback() + @State private var shouldShowLoadingView: Bool = false; init(item: ResumeItem) { self.item = item; @@ -23,7 +24,17 @@ struct ItemView: View { var body: some View { if(playback.shouldPlay) { - VideoPlayerViewRefactored(itemPlayback: playback) + LoadingView(isShowing: $shouldShowLoadingView) { + VLCPlayerWithControls(item: playback.itemToPlay, loadBinding: $shouldShowLoadingView, pBinding: _playback.projectedValue.shouldPlay) + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + .statusBar(hidden: true) + .prefersHomeIndicatorAutoHidden(true) + .preferredColorScheme(.dark) + .edgesIgnoringSafeArea(.all) + .overrideViewPreference(.unspecified) + .supportedOrientations(.landscape) + } } else { Group { if(item.Type == "Movie") { diff --git a/JellyfinPlayer/VLCPlayer.swift b/JellyfinPlayer/VLCPlayer.swift deleted file mode 100644 index 6d536700..00000000 --- a/JellyfinPlayer/VLCPlayer.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// VideoPlayerView.swift -// JellyfinPlayer -// -// Created by Aiden Vigue on 5/10/21. -// - -import SwiftUI -import MobileVLCKit - -extension NSNotification { - static let PlayerUpdate = NSNotification.Name.init("PlayerUpdate") -} - -enum VideoType { - case hls; - case direct; -} - -struct Subtitle { - var name: String; - var id: Int32; - var url: URL; - var delivery: String; - var codec: String; -} - -class PlaybackItem: ObservableObject { - @Published var videoType: VideoType = .hls; - @Published var videoUrl: URL = URL(string: "https://example.com")!; - @Published var subtitles: [Subtitle] = []; -} - -struct VLCPlayer: UIViewRepresentable{ - var url: Binding; - var player: Binding; - var startTime: Int; - - func updateUIView(_ uiView: PlayerUIView, context: UIViewRepresentableContext) { - uiView.url = self.url - if(self.url.wrappedValue.videoUrl.absoluteString != "https://example.com") { - uiView.videoSetup() - } - } - - func makeUIView(context: Context) -> PlayerUIView { - return PlayerUIView(frame: .zero, url: url, player: self.player, startTime: self.startTime); - } -} - -class PlayerUIView: UIView, VLCMediaPlayerDelegate { - - private var mediaPlayer: Binding; - var url:Binding - var lastUrl: PlaybackItem? - var startTime: Int - - init(frame: CGRect, url: Binding, player: Binding, startTime: Int) { - self.mediaPlayer = player; - self.url = url; - self.startTime = startTime; - super.init(frame: frame) - mediaPlayer.wrappedValue.delegate = self - mediaPlayer.wrappedValue.drawable = self - } - - func videoSetup() { - if(lastUrl == nil || lastUrl?.videoUrl != url.wrappedValue.videoUrl) { - lastUrl = url.wrappedValue - mediaPlayer.wrappedValue.stop() - mediaPlayer.wrappedValue.media = VLCMedia(url: url.wrappedValue.videoUrl) - self.url.wrappedValue.subtitles.forEach() { sub in - if(sub.id != -1 && sub.delivery == "External" && sub.codec != "subrip") { - mediaPlayer.wrappedValue.addPlaybackSlave(sub.url, type: .subtitle, enforce: false) - } - } - - mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFontSize:")), with: 14) - //mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate") - - DispatchQueue.global(qos: .utility).async { [weak self] in - self?.mediaPlayer.wrappedValue.play() - if(self?.startTime != 0) { - print(self?.startTime ?? "") - self?.mediaPlayer.wrappedValue.jumpForward(Int32(self!.startTime/10000000)) - } - } - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - } -} diff --git a/JellyfinPlayer/VideoPlayerViewRefactored.swift b/JellyfinPlayer/VideoPlayerViewRefactored.swift index 8ac8c95a..fd295ff1 100644 --- a/JellyfinPlayer/VideoPlayerViewRefactored.swift +++ b/JellyfinPlayer/VideoPlayerViewRefactored.swift @@ -25,7 +25,8 @@ struct VideoPlayerViewRefactored: View { @State private var selectedAudioTrack: Int32 = 0; @State private var selectedCaptionTrack: Int32 = 0; @State private var playSessionId: String = ""; - @State private var shouldOverlayShow: Bool = false; + @State private var shouldOverlayShow: Bool = true; + @State private var show: Bool = true; @State private var subtitles: [Subtitle] = []; @State private var audioTracks: [Subtitle] = []; // can reuse the same struct @@ -37,118 +38,129 @@ struct VideoPlayerViewRefactored: View { } var body: some View { - LoadingView(isShowing: $shouldShowLoadingView) { - VLCPlayer(url: $VLCItem, player: $VLCPlayerObj, startTime: Int(itemPlayback.itemToPlay.Progress)).onDisappear(perform: { - VLCPlayerObj.stop() - }) - .padding(EdgeInsets(top: 0, leading: UIDevice.current.hasNotch ? 30 : 0, bottom: 0, trailing: UIDevice.current.hasNotch ? 30 : 0)) - } - .overlay( - Group { - if(shouldOverlayShow) { - VStack() { - HStack() { - HStack() { - Button() { - sendStopReport() - self.itemPlayback.shouldPlay = false; - } label: { - HStack() { - Image(systemName: "chevron.left").font(.system(size: 20)).foregroundColor(.white) - } - }.frame(width: 20) - Spacer() - Text(itemPlayback.itemToPlay.Name).font(.headline).fontWeight(.semibold).foregroundColor(.white).offset(x:20) - Spacer() - Button() { - VLCPlayerObj.pause() - } label: { - HStack() { - Image(systemName: "gear").font(.system(size: 20)).foregroundColor(.white) - } - }.frame(width: 20).padding(.trailing,15) - Button() { - VLCPlayerObj.pause() - } label: { - HStack() { - Image(systemName: "captions.bubble").font(.system(size: 20)).foregroundColor(.white) - } - }.frame(width: 20) - } - Spacer() - }.padding(EdgeInsets(top: 55, leading: 40, bottom: 0, trailing: 40)) - Spacer() - HStack() { - Spacer() - Button() { - VLCPlayerObj.jumpBackward(15) - } label: { - Image(systemName: "gobackward.15").font(.system(size: 40)).foregroundColor(.white) - }.padding(20) - Spacer() - Button() { - if(VLCPlayerObj.state != .paused) { - VLCPlayerObj.pause() - sendProgressReport(eventName: "pause") - } else { - VLCPlayerObj.play() - sendProgressReport(eventName: "unpause") - } - } label: { - Image(systemName: VLCPlayerObj.state == .paused ? "play" : "pause").font(.system(size: 55)).foregroundColor(.white) - }.padding(20).frame(width: 60, height: 60) - Spacer() - Button() { - VLCPlayerObj.jumpForward(15) - } label: { - Image(systemName: "goforward.15").font(.system(size: 40)).foregroundColor(.white) - }.padding(20) - Spacer() - }.padding(.leading, -20) - Spacer() - HStack() { - Slider(value: $scrub, onEditingChanged: { bool in - let videoPosition = Double(VLCPlayerObj.time.intValue) - let videoDuration = Double(VLCPlayerObj.time.intValue + abs(VLCPlayerObj.remainingTime.intValue)) - if(bool == true) { - VLCPlayerObj.pause() - sendProgressReport(eventName: "pause") - } else { - //Scrub is value from 0..1 - find position in video and add / or remove. - let secondsScrubbedTo = round(_scrub.wrappedValue * videoDuration); - let offset = secondsScrubbedTo - videoPosition; - sendProgressReport(eventName: "unpause") - VLCPlayerObj.play() - if(offset > 0) { - VLCPlayerObj.jumpForward(Int32(offset)/1000); - } else { - VLCPlayerObj.jumpBackward(Int32(abs(offset))/1000); - } - } - }) - .accentColor(Color(red: 172/255, green: 92/255, blue: 195/255)) - Text(timeText).fontWeight(.semibold).frame(width: 80).foregroundColor(.white) - }.padding(EdgeInsets(top: -20, leading: 44, bottom: 42, trailing: 40)) - } - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .background(Color(.black).opacity(0.4)) - } + if(show) { + LoadingView(isShowing: $shouldShowLoadingView) { + EmptyView() + .padding(EdgeInsets(top: 0, leading: UIDevice.current.hasNotch ? 30 : 0, bottom: 0, trailing: UIDevice.current.hasNotch ? 30 : 0)) } - , alignment: .topLeading) - .introspectTabBarController { (UITabBarController) in - UITabBarController.tabBar.isHidden = true + .overlay( + Group { + if(shouldOverlayShow) { + VStack() { + HStack() { + HStack() { + Button() { + sendStopReport() + VLCPlayerObj.stop() + self.itemPlayback.shouldPlay = false; + } label: { + HStack() { + Image(systemName: "chevron.left").font(.system(size: 20)).foregroundColor(.white) + } + }.frame(width: 20) + Spacer() + Text(itemPlayback.itemToPlay.Name).font(.headline).fontWeight(.semibold).foregroundColor(.white).offset(x:20) + Spacer() + Button() { + VLCPlayerObj.pause() + } label: { + HStack() { + Image(systemName: "gear").font(.system(size: 20)).foregroundColor(.white) + } + }.frame(width: 20).padding(.trailing,15) + Button() { + VLCPlayerObj.pause() + } label: { + HStack() { + Image(systemName: "captions.bubble").font(.system(size: 20)).foregroundColor(.white) + } + }.frame(width: 20) + } + Spacer() + }.padding(EdgeInsets(top: 55, leading: 40, bottom: 0, trailing: 40)) + Spacer() + HStack() { + Spacer() + Button() { + VLCPlayerObj.jumpBackward(15) + } label: { + Image(systemName: "gobackward.15").font(.system(size: 40)).foregroundColor(.white) + }.padding(20) + Spacer() + Button() { + if(VLCPlayerObj.state != .paused) { + VLCPlayerObj.pause() + sendProgressReport(eventName: "pause") + } else { + VLCPlayerObj.play() + sendProgressReport(eventName: "unpause") + } + } label: { + if(VLCPlayerObj.state == .paused) { + Image(systemName: "play").font(.system(size: 55)).foregroundColor(.white) + } else { + Image(systemName: "pause").font(.system(size: 55)).foregroundColor(.white) + } + }.padding(20).frame(width: 60, height: 60) + Spacer() + Button() { + VLCPlayerObj.jumpForward(15) + } label: { + Image(systemName: "goforward.15").font(.system(size: 40)).foregroundColor(.white) + }.padding(20) + Spacer() + }.padding(.leading, -20) + Spacer() + HStack() { + Slider(value: $scrub, onEditingChanged: { bool in + let videoPosition = Double(VLCPlayerObj.time.intValue) + let videoDuration = Double(VLCPlayerObj.time.intValue + abs(VLCPlayerObj.remainingTime.intValue)) + if(bool == true) { + VLCPlayerObj.pause() + sendProgressReport(eventName: "pause") + } else { + //Scrub is value from 0..1 - find position in video and add / or remove. + let secondsScrubbedTo = round(_scrub.wrappedValue * videoDuration); + let offset = secondsScrubbedTo - videoPosition; + sendProgressReport(eventName: "unpause") + VLCPlayerObj.play() + if(offset > 0) { + VLCPlayerObj.jumpForward(Int32(offset)/1000); + } else { + VLCPlayerObj.jumpBackward(Int32(abs(offset))/1000); + } + } + }) + .accentColor(Color(red: 172/255, green: 92/255, blue: 195/255)) + Text(timeText).fontWeight(.semibold).frame(width: 80).foregroundColor(.white) + }.padding(EdgeInsets(top: -20, leading: 44, bottom: 42, trailing: 40)) + } + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) + .background(Color(.black).opacity(0.4)) + } + } + , alignment: .topLeading) + .introspectTabBarController { (UITabBarController) in + UITabBarController.tabBar.isHidden = true + } + .onTapGesture(perform: resetTimer) + .navigationBarHidden(true) + .navigationBarBackButtonHidden(true) + .statusBar(hidden: true) + .prefersHomeIndicatorAutoHidden(true) + .preferredColorScheme(.dark) + .edgesIgnoringSafeArea(.all) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) + .overrideViewPreference(.unspecified) + .supportedOrientations(.landscape) + .onAppear(perform: onAppear) + } else { + Text("test").onAppear(perform: { + print("ev appear") + usleep(10000); + _show.wrappedValue = true; + }) } - .onTapGesture(perform: resetTimer) - .navigationBarHidden(true) - .navigationBarBackButtonHidden(true) - .statusBar(hidden: true) - .prefersHomeIndicatorAutoHidden(true) - .preferredColorScheme(.dark) - .edgesIgnoringSafeArea(.all) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .overrideViewPreference(.unspecified) - .supportedOrientations(.landscape) - .onAppear(perform: onAppear) } func onAppear() { @@ -296,11 +308,12 @@ struct VideoPlayerViewRefactored: View { func resetTimer() { print("rt running") - if(_shouldOverlayShow.wrappedValue == true) { - _shouldOverlayShow.wrappedValue = false + show = false; + if(shouldOverlayShow == true) { + shouldOverlayShow = false return; } - _shouldOverlayShow.wrappedValue = true; + shouldOverlayShow = true; } func sendStopReport() { diff --git a/JellyfinPlayer/Views/VLCPlayer.swift b/JellyfinPlayer/Views/VLCPlayer.swift new file mode 100644 index 00000000..b530f1ef --- /dev/null +++ b/JellyfinPlayer/Views/VLCPlayer.swift @@ -0,0 +1,498 @@ +// +// VLCPlayer.swift +// JellyfinPlayer +// +// Created by Aiden Vigue on 5/10/21. +// + +//me realizing i shouldve just written the whole app in the mvvm system bc it makes so much more sense + +import SwiftUI +import MobileVLCKit +import SwiftyJSON +import SwiftyRequest + +enum VideoType { + case hls; + case direct; +} + +struct Subtitle { + var name: String; + var id: Int32; + var url: URL; + var delivery: String; + var codec: String; +} + +struct AudioTrack { + var name: String; + var id: Int32; +} + +class PlaybackItem: ObservableObject { + @Published var videoType: VideoType = .hls; + @Published var videoUrl: URL = URL(string: "https://example.com")!; + @Published var subtitles: [Subtitle] = []; +} + +protocol PlayerViewControllerDelegate: AnyObject { + func hideLoadingView(_ viewController: PlayerViewController) + func showLoadingView(_ viewController: PlayerViewController) + func exitPlayer(_ viewController: PlayerViewController) +} + +class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate { + + weak var delegate: PlayerViewControllerDelegate? + + var mediaPlayer = VLCMediaPlayer() + var globalData = GlobalData() + + @IBOutlet weak var timeText: UILabel! + @IBOutlet weak var videoContentView: UIView! + @IBOutlet weak var videoControlsView: UIView! + @IBOutlet weak var seekSlider: UISlider! + @IBOutlet weak var titleLabel: UILabel! + + var shouldShowLoadingScreen: Bool = false; + var ssTargetValueOffset: Int = 0; + var ssStartValue: Int = 0; + + var paused: Bool = true; + var lastTime: Float = 0.0; + var startTime: Int = 0; + var selectedAudioTrack: Int32 = 0; + var selectedCaptionTrack: Int32 = 0; + var playSessionId: String = ""; + + var subtitleTrackArray: [Subtitle] = []; + var audioTrackArray: [AudioTrack] = []; + + var manifest: DetailItem = DetailItem(); + var playbackItem = PlaybackItem(); + + @IBAction func seekSliderStart(_ sender: Any) { + print("ss start") + mediaPlayer.pause() + } + @IBAction func seekSliderValueChanged(_ sender: Any) { + print("ss mv " + String(seekSlider.value)) + } + @IBAction func seekSliderEnd(_ sender: Any) { + print("ss end") + mediaPlayer.play() + } + + @IBAction func exitButtonPressed(_ sender: Any) { + print("exit tap") + delegate?.exitPlayer(self) + } + + @IBAction func controlViewTapped(_ sender: Any) { + print("control view tap") + videoControlsView.isHidden = !videoControlsView.isHidden + } + + @IBAction func contentViewTapped(_ sender: Any) { + print("content view tap") + videoControlsView.isHidden = !videoControlsView.isHidden + } + + + @IBOutlet weak var mainActionButton: UIButton! + @IBAction func mainActionButtonPressed(_ sender: Any) { + print("mab press") + print(mediaPlayer.state.rawValue) + if(paused) { + mediaPlayer.play() + mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) + paused = false; + } else { + mediaPlayer.pause() + mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) + paused = true; + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + //View has loaded. + //Show loading screen + delegate?.showLoadingView(self) + + //Fetch max bitrate from UserDefaults depending on current connection mode + let defaults = UserDefaults.standard + let maxBitrate = globalData.isInNetwork ? defaults.integer(forKey: "InNetworkBandwidth") : defaults.integer(forKey: "OutOfNetworkBandwidth") + + //Build a device profile + let builder = DeviceProfileBuilder() + builder.setMaxBitrate(bitrate: maxBitrate) + let profile = builder.buildProfile() + + let jsonEncoder = JSONEncoder() + let jsonData = try! jsonEncoder.encode(profile) + + let url = (globalData.server?.baseURI ?? "") + "/Items/\(manifest.Id)/PlaybackInfo?UserId=\(globalData.user?.user_id ?? "")&StartTimeTicks=\(Int(manifest.Progress))&IsPlayback=true&AutoOpenLiveStream=true&MaxStreamingBitrate=\(profile.DeviceProfile.MaxStreamingBitrate)"; + + let request = RestRequest(method: .post, url: url) + + request.headerParameters["X-Emby-Authorization"] = globalData.authHeader + request.contentType = "application/json" + request.acceptType = "application/json" + request.messageBody = jsonData + + request.responseData() { [self] (result: Result, RestError>) in + switch result { + case .success(let response): + let body = response.body + do { + let json = try JSON(data: body) + playSessionId = json["PlaySessionId"].string ?? ""; + if(json["MediaSources"][0]["TranscodingUrl"].string != nil) { + let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")\((json["MediaSources"][0]["TranscodingUrl"].string ?? ""))")! + let item = PlaybackItem() + item.videoType = .hls + item.videoUrl = streamURL + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "") + subtitleTrackArray.append(disableSubtitleTrack); + + for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] { + if(stream["Type"].string == "Subtitle") { //ignore ripped subtitles - we don't want to extract subtitles + let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")! + let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "") + subtitleTrackArray.append(subtitle); + } + + if(stream["Type"].string == "Audio") { + let deliveryUrl = URL(string: "https://example.com")! + let subtitle = AudioTrack(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0)) + if(stream["IsDefault"].boolValue) { + selectedAudioTrack = Int32(stream["Index"].int ?? 0); + } + audioTrackArray.append(subtitle); + } + } + + if(selectedAudioTrack == -1) { + if(audioTrackArray.count > 0) { + selectedAudioTrack = audioTrackArray[0].id; + } + } + + self.sendPlayReport() + item.subtitles = subtitleTrackArray + playbackItem = item; + } else { + print("Direct playing!"); + let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")/Videos/\(manifest.Id)/stream?Static=true&mediaSourceId=\(manifest.Id)&deviceId=\(globalData.user?.device_uuid ?? "")&api_key=\(globalData.authToken)&Tag=\(json["MediaSources"][0]["ETag"])")!; + let item = PlaybackItem() + item.videoUrl = streamURL + item.videoType = .direct + + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "") + subtitleTrackArray.append(disableSubtitleTrack); + for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] { + if(stream["Type"].string == "Subtitle") { + let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")! + let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "") + subtitleTrackArray.append(subtitle); + } + + if(stream["Type"].string == "Audio") { + let deliveryUrl = URL(string: "https://example.com")! + let subtitle = AudioTrack(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0)) + if(stream["IsDefault"].boolValue) { + selectedAudioTrack = Int32(stream["Index"].int ?? 0); + } + audioTrackArray.append(subtitle); + } + } + + if(selectedAudioTrack == -1) { + if(audioTrackArray.count > 0) { + selectedAudioTrack = audioTrackArray[0].id; + } + } + + sendPlayReport() + item.subtitles = subtitleTrackArray + playbackItem = item; + } + } catch { + + } + break + case .failure(let error): + debugPrint(error) + break + } + } + + mediaPlayer.media = media + + mediaPlayer.delegate = self + mediaPlayer.drawable = videoContentView + + if(manifest.Type == "Episode") { + titleLabel.text = "\(manifest.Name) - S\(String(manifest.ParentIndexNumber ?? 0)):E\(String(manifest.IndexNumber ?? 0)) - \(manifest.SeriesName ?? "")" + } else { + titleLabel.text = manifest.Name + } + mediaPlayer.play() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.tabBarController?.tabBar.isHidden = true; + } + + + //MARK: VLCMediaPlayer Delegates + func mediaPlayerStateChanged(_ aNotification: Notification!) { + let currentState: VLCMediaPlayerState = mediaPlayer.state + switch currentState { + case .stopped : + print("Video is done playing)") + + case .ended : + print("Video is done playing)") + + case .playing : + print("Video is playing") + delegate?.hideLoadingView(self) + paused = false; + + case .paused : + print("Video is paused)") + paused = true; + + case .opening : + print("Video is opening)") + + case .buffering : + print("Video is buffering)") + delegate?.showLoadingView(self) + mediaPlayer.pause() + usleep(10000) + mediaPlayer.play() + + case .error : + print("Video has error)") + + case .esAdded: + print("Es Added") + mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) + @unknown default: + break + } + } + + func mediaPlayerTimeChanged(_ aNotification: Notification!) { + let time = mediaPlayer.position; + if(time != lastTime) { + paused = false; + delegate?.hideLoadingView(self) + } else { + paused = true; + } + lastTime = time; + } + + //MARK: Jellyfin Playstate updates + func sendProgressReport(eventName: String) { + var progressBody: String = ""; + progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":\(mediaPlayer.state == .paused ? "true" : "false"),\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(mediaPlayer.position * Float(manifest.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":569735888.888889}],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"EventName\":\"\(eventName)\"}"; + + print(""); + print("Sending progress report") + print(progressBody) + + let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Progress") + request.headerParameters["X-Emby-Authorization"] = globalData.authHeader + request.contentType = "application/json" + request.acceptType = "application/json" + request.messageBody = progressBody.data(using: .ascii); + request.responseData() { (result: Result, RestError>) in + switch result { + case .success(let resp): + print(resp.body) + break + case .failure(let error): + debugPrint(error) + break + } + } + } + + func sendStopReport() { + var progressBody: String = ""; + + progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":true,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(mediaPlayer.position * Float(manifest.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":100000}],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(manifest.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; + + print(""); + print("Sending stop report") + print(progressBody) + + let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Stopped") + request.headerParameters["X-Emby-Authorization"] = globalData.authHeader + request.contentType = "application/json" + request.acceptType = "application/json" + request.messageBody = progressBody.data(using: .ascii); + request.responseData() { (result: Result, RestError>) in + switch result { + case .success(let resp): + print(resp.body) + break + case .failure(let error): + debugPrint(error) + break + } + } + } + + func sendPlayReport() { + var progressBody: String = ""; + startTime = Int(Date().timeIntervalSince1970) * 10000000 + + progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":false,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(manifest.Progress)),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[],\"PlayMethod\":\"\(playbackItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(manifest.Id)\",\"CanSeek\":true,\"ItemId\":\"\(manifest.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(manifest.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; + + print(""); + print("Sending play report") + print(progressBody) + + let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing") + request.headerParameters["X-Emby-Authorization"] = globalData.authHeader + request.contentType = "application/json" + request.acceptType = "application/json" + request.messageBody = progressBody.data(using: .ascii); + request.responseData() { (result: Result, RestError>) in + switch result { + case .success(let resp): + print(resp.body) + break + case .failure(let error): + debugPrint(error) + break + } + } + } +} + +struct VLCPlayerWithControls: UIViewControllerRepresentable { + var item: DetailItem + @Environment(\.presentationMode) var presentationMode + @EnvironmentObject private var globalData: GlobalData; + + var loadBinding: Binding + var pBinding: Binding + + class Coordinator: NSObject, PlayerViewControllerDelegate { + let loadBinding: Binding + let pBinding: Binding + + init(loadBinding: Binding, pBinding: Binding) { + self.loadBinding = loadBinding + self.pBinding = pBinding + } + + func hideLoadingView(_ viewController: PlayerViewController) { + self.loadBinding.wrappedValue = false; + } + + func showLoadingView(_ viewController: PlayerViewController) { + self.loadBinding.wrappedValue = true; + } + + func exitPlayer(_ viewController: PlayerViewController) { + self.pBinding.wrappedValue = false; + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(loadBinding: self.loadBinding, pBinding: self.pBinding) + } + + + typealias UIViewControllerType = PlayerViewController + func makeUIViewController(context: UIViewControllerRepresentableContext) -> VLCPlayerWithControls.UIViewControllerType { + let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil) + let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController + customViewController.manifest = item; + customViewController.delegate = context.coordinator; + customViewController.globalData = globalData; + return customViewController + } + + func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType, context: UIViewControllerRepresentableContext) { + } +} + +/* +struct VLCPlayer: UIViewRepresentable{ + var url: Binding; + var player: Binding; + var startTime: Int; + + func updateUIView(_ uiView: PlayerUIView, context: UIViewRepresentableContext) { + uiView.url = self.url + if(self.url.wrappedValue.videoUrl.absoluteString != "https://example.com") { + uiView.videoSetup() + } + } + + func makeUIView(context: Context) -> PlayerUIView { + return PlayerUIView(frame: .zero, url: url, player: self.player, startTime: self.startTime); + } +} + +class PlayerUIView: UIView, VLCMediaPlayerDelegate { + + private var mediaPlayer: Binding; + var url:Binding + var lastUrl: PlaybackItem? + var startTime: Int + + init(frame: CGRect, url: Binding, player: Binding, startTime: Int) { + self.mediaPlayer = player; + self.url = url; + self.startTime = startTime; + super.init(frame: frame) + mediaPlayer.wrappedValue.delegate = self + mediaPlayer.wrappedValue.drawable = self + } + + func videoSetup() { + if(lastUrl == nil || lastUrl?.videoUrl != url.wrappedValue.videoUrl) { + lastUrl = url.wrappedValue + mediaPlayer.wrappedValue.stop() + mediaPlayer.wrappedValue.media = VLCMedia(url: url.wrappedValue.videoUrl) + self.url.wrappedValue.subtitles.forEach() { sub in + if(sub.id != -1 && sub.delivery == "External" && sub.codec != "subrip") { + mediaPlayer.wrappedValue.addPlaybackSlave(sub.url, type: .subtitle, enforce: false) + } + } + + mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFontSize:")), with: 14) + //mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate") + + DispatchQueue.global(qos: .utility).async { [weak self] in + self?.mediaPlayer.wrappedValue.play() + if(self?.startTime != 0) { + print(self?.startTime ?? "") + self?.mediaPlayer.wrappedValue.jumpForward(Int32(self!.startTime/10000000)) + } + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + } +} + */ diff --git a/JellyfinPlayer/Views/VideoPlayer.storyboard b/JellyfinPlayer/Views/VideoPlayer.storyboard new file mode 100644 index 00000000..0e86e10f --- /dev/null +++ b/JellyfinPlayer/Views/VideoPlayer.storyboard @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 57b39c932bb4b20d283e6e28ad9495800fbb246f Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Wed, 26 May 2021 21:44:56 -0400 Subject: [PATCH 2/6] Playback reporting works now --- JellyfinPlayer/JellyfinPlayerApp.swift | 2 +- JellyfinPlayer/Views/VLCPlayer.swift | 49 +++++++++++++++++++------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/JellyfinPlayer/JellyfinPlayerApp.swift b/JellyfinPlayer/JellyfinPlayerApp.swift index 3cc35ede..6f7230ac 100644 --- a/JellyfinPlayer/JellyfinPlayerApp.swift +++ b/JellyfinPlayer/JellyfinPlayerApp.swift @@ -21,7 +21,7 @@ class GlobalData: ObservableObject { extension UIDevice { var hasNotch: Bool { - let bottom = UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0 + let bottom = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.safeAreaInsets.bottom ?? 0 return bottom > 0 } } diff --git a/JellyfinPlayer/Views/VLCPlayer.swift b/JellyfinPlayer/Views/VLCPlayer.swift index b530f1ef..b6800599 100644 --- a/JellyfinPlayer/Views/VLCPlayer.swift +++ b/JellyfinPlayer/Views/VLCPlayer.swift @@ -6,6 +6,7 @@ // //me realizing i shouldve just written the whole app in the mvvm system bc it makes so much more sense +//Please don't touch this ifle import SwiftUI import MobileVLCKit @@ -65,6 +66,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe var selectedAudioTrack: Int32 = 0; var selectedCaptionTrack: Int32 = 0; var playSessionId: String = ""; + var lastProgressReportTime: Double = 0; var subtitleTrackArray: [Subtitle] = []; var audioTrackArray: [AudioTrack] = []; @@ -85,24 +87,21 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } @IBAction func exitButtonPressed(_ sender: Any) { - print("exit tap") + sendStopReport() delegate?.exitPlayer(self) } @IBAction func controlViewTapped(_ sender: Any) { - print("control view tap") videoControlsView.isHidden = !videoControlsView.isHidden } @IBAction func contentViewTapped(_ sender: Any) { - print("content view tap") videoControlsView.isHidden = !videoControlsView.isHidden } @IBOutlet weak var mainActionButton: UIButton! @IBAction func mainActionButtonPressed(_ sender: Any) { - print("mab press") print(mediaPlayer.state.rawValue) if(paused) { mediaPlayer.play() @@ -120,6 +119,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe //View has loaded. //Show loading screen + usleep(10000); delegate?.showLoadingView(self) //Fetch max bitrate from UserDefaults depending on current connection mode @@ -166,7 +166,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } if(stream["Type"].string == "Audio") { - let deliveryUrl = URL(string: "https://example.com")! let subtitle = AudioTrack(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0)) if(stream["IsDefault"].boolValue) { selectedAudioTrack = Int32(stream["Index"].int ?? 0); @@ -201,7 +200,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } if(stream["Type"].string == "Audio") { - let deliveryUrl = URL(string: "https://example.com")! let subtitle = AudioTrack(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0)) if(stream["IsDefault"].boolValue) { selectedAudioTrack = Int32(stream["Index"].int ?? 0); @@ -220,6 +218,15 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe item.subtitles = subtitleTrackArray playbackItem = item; } + + mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) + playbackItem.subtitles.forEach() { sub in + if(sub.id != -1 && sub.delivery == "External" && sub.codec != "subrip") { + mediaPlayer.addPlaybackSlave(sub.url, type: .subtitle, enforce: false) + } + } + mediaPlayer.play() + mediaPlayer.jumpForward(Int32(manifest.Progress/10000000)) } catch { } @@ -230,8 +237,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } } - mediaPlayer.media = media - mediaPlayer.delegate = self mediaPlayer.drawable = videoContentView @@ -240,7 +245,6 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } else { titleLabel.text = manifest.Name } - mediaPlayer.play() } override func viewWillAppear(_ animated: Bool) { @@ -255,12 +259,13 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe switch currentState { case .stopped : print("Video is done playing)") - + sendStopReport() case .ended : print("Video is done playing)") - + sendStopReport() case .playing : print("Video is playing") + sendProgressReport(eventName: "unpause") delegate?.hideLoadingView(self) paused = false; @@ -273,6 +278,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe case .buffering : print("Video is buffering)") + sendProgressReport(eventName: "pause") delegate?.showLoadingView(self) mediaPlayer.pause() usleep(10000) @@ -280,9 +286,8 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe case .error : print("Video has error)") - + sendStopReport() case .esAdded: - print("Es Added") mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) @unknown default: break @@ -293,11 +298,29 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe let time = mediaPlayer.position; if(time != lastTime) { paused = false; + seekSlider.setValue(mediaPlayer.position, animated: true) delegate?.hideLoadingView(self) + + let remainingTime = abs(mediaPlayer.remainingTime.intValue)/1000; + let hours = remainingTime / 3600; + let minutes = (remainingTime % 3600) / 60; + let seconds = (remainingTime % 3600) % 60; + var timeTextStr = ""; + if(hours != 0) { + timeTextStr = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))"; + } else { + timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))"; + } + timeText.text = timeTextStr } else { paused = true; } lastTime = time; + + if(CACurrentMediaTime() - lastProgressReportTime > 5) { + sendProgressReport(eventName: "timeupdate") + lastProgressReportTime = CACurrentMediaTime() + } } //MARK: Jellyfin Playstate updates From 422880013ef52480986fe4038007b6cf21522971 Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Wed, 26 May 2021 22:04:38 -0400 Subject: [PATCH 3/6] Seeking works - direct play complete. --- JellyfinPlayer/Views/VLCPlayer.swift | 27 ++++++++++- JellyfinPlayer/Views/VideoPlayer.storyboard | 50 ++++++++++----------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/JellyfinPlayer/Views/VLCPlayer.swift b/JellyfinPlayer/Views/VLCPlayer.swift index b6800599..2897333e 100644 --- a/JellyfinPlayer/Views/VLCPlayer.swift +++ b/JellyfinPlayer/Views/VLCPlayer.swift @@ -75,15 +75,38 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe var playbackItem = PlaybackItem(); @IBAction func seekSliderStart(_ sender: Any) { - print("ss start") + sendProgressReport(eventName: "pause") mediaPlayer.pause() } @IBAction func seekSliderValueChanged(_ sender: Any) { - print("ss mv " + String(seekSlider.value)) + let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.intValue))/1000 + let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration); + let scrubRemaining = videoDuration - secondsScrubbedTo; + let remainingTime = scrubRemaining; + let hours = floor(remainingTime / 3600); + let minutes = (remainingTime.truncatingRemainder(dividingBy: 3600)) / 60; + let seconds = (remainingTime.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60); + if(hours != 0) { + timeText.text = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"; + } else { + timeText.text = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"; + } } + @IBAction func seekSliderEnd(_ sender: Any) { print("ss end") + let videoPosition = Double(mediaPlayer.time.intValue) + let videoDuration = Double(mediaPlayer.time.intValue + abs(mediaPlayer.remainingTime.intValue)) + //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; mediaPlayer.play() + if(offset > 0) { + mediaPlayer.jumpForward(Int32(offset)/1000); + } else { + mediaPlayer.jumpBackward(Int32(abs(offset))/1000); + } + sendProgressReport(eventName: "unpause") } @IBAction func exitButtonPressed(_ sender: Any) { diff --git a/JellyfinPlayer/Views/VideoPlayer.storyboard b/JellyfinPlayer/Views/VideoPlayer.storyboard index 0e86e10f..7ba3f95c 100644 --- a/JellyfinPlayer/Views/VideoPlayer.storyboard +++ b/JellyfinPlayer/Views/VideoPlayer.storyboard @@ -32,20 +32,21 @@ - + + - @@ -99,17 +102,15 @@ - - + - - + @@ -149,8 +150,5 @@ - - - From 198516b8a96469ba88554626ced3535da2017125 Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Wed, 26 May 2021 22:20:06 -0400 Subject: [PATCH 4/6] Add jump buttons --- JellyfinPlayer.xcodeproj/project.pbxproj | 16 +- JellyfinPlayer/VideoPlayerView.swift | 585 ------------------ .../VideoPlayerViewRefactored.swift | 371 ----------- JellyfinPlayer/Views/VideoPlayer.storyboard | 62 +- .../{VLCPlayer.swift => VideoPlayer.swift} | 104 +--- 5 files changed, 80 insertions(+), 1058 deletions(-) delete mode 100644 JellyfinPlayer/VideoPlayerView.swift delete mode 100644 JellyfinPlayer/VideoPlayerViewRefactored.swift rename JellyfinPlayer/Views/{VLCPlayer.swift => VideoPlayer.swift} (88%) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 78e7efd9..038f4adb 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -10,14 +10,12 @@ 5302F82A2658791C00647A2E /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 5302F8292658791C00647A2E /* Sentry */; }; 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */; }; 53313B90265EEA6D00947AA3 /* VideoPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */; }; - 5335256E265E8D5A006CCA86 /* VideoPlayerViewRefactored.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5335256D265E8D5A006CCA86 /* VideoPlayerViewRefactored.swift */; }; 53352571265EA0A0006CCA86 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 53352570265EA0A0006CCA86 /* Introspect */; }; 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5338F74D263B61370014BF09 /* ConnectToServerView.swift */; }; 5338F754263B65E10014BF09 /* SwiftyRequest in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F753263B65E10014BF09 /* SwiftyRequest */; }; 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 5338F756263B7E2E0014BF09 /* KeychainSwift */; }; 535BAE9F2649E569005FA86D /* ItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAE9E2649E569005FA86D /* ItemView.swift */; }; - 535BAEA5264A151C005FA86D /* VLCPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VLCPlayer.swift */; }; - 535BAEA7264A18AA005FA86D /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA6264A18AA005FA86D /* VideoPlayerView.swift */; }; + 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535BAEA4264A151C005FA86D /* VideoPlayer.swift */; }; 5377CBF5263B596A003A4E83 /* JellyfinPlayerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */; }; 5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBF6263B596A003A4E83 /* ContentView.swift */; }; 5377CBF9263B596B003A4E83 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5377CBF8263B596B003A4E83 /* Assets.xcassets */; }; @@ -74,11 +72,9 @@ /* Begin PBXFileReference section */ 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProfileBuilder.swift; sourceTree = ""; }; 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = VideoPlayer.storyboard; sourceTree = ""; }; - 5335256D265E8D5A006CCA86 /* VideoPlayerViewRefactored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewRefactored.swift; sourceTree = ""; }; 5338F74D263B61370014BF09 /* ConnectToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerView.swift; sourceTree = ""; }; 535BAE9E2649E569005FA86D /* ItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemView.swift; sourceTree = ""; }; - 535BAEA4264A151C005FA86D /* VLCPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayer.swift; sourceTree = ""; }; - 535BAEA6264A18AA005FA86D /* VideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; }; + 535BAEA4264A151C005FA86D /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; 5377CBF1263B596A003A4E83 /* JellyfinPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JellyfinPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5377CBF4263B596A003A4E83 /* JellyfinPlayerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinPlayerApp.swift; sourceTree = ""; }; 5377CBF6263B596A003A4E83 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -171,13 +167,11 @@ 53E4E648263F725B00F67C6B /* MultiSelector.swift */, 535BAE9E2649E569005FA86D /* ItemView.swift */, 53A089CF264DA9DA00D57806 /* MovieItemView.swift */, - 535BAEA6264A18AA005FA86D /* VideoPlayerView.swift */, 53EE24E5265060780068F029 /* LibrarySearchView.swift */, 53987CA326572C1300E7EA70 /* SeasonItemView.swift */, 53987CA526572F0700E7EA70 /* SeriesItemView.swift */, 53987CA72657424A00E7EA70 /* EpisodeItemView.swift */, 53192D5C265AA78A008A4215 /* DeviceProfileBuilder.swift */, - 5335256D265E8D5A006CCA86 /* VideoPlayerViewRefactored.swift */, ); path = JellyfinPlayer; sourceTree = ""; @@ -201,7 +195,7 @@ AE8C3150265D5FE1008AA076 /* Views */ = { isa = PBXGroup; children = ( - 535BAEA4264A151C005FA86D /* VLCPlayer.swift */, + 535BAEA4264A151C005FA86D /* VideoPlayer.swift */, 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, ); @@ -332,19 +326,17 @@ 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, AE8C3154265D60BF008AA076 /* SettingsModel.swift in Sources */, 53892770263C25230035E14B /* NextUpView.swift in Sources */, - 535BAEA5264A151C005FA86D /* VLCPlayer.swift in Sources */, + 535BAEA5264A151C005FA86D /* VideoPlayer.swift in Sources */, 5377CC01263B596B003A4E83 /* Model.xcdatamodeld in Sources */, 53DF641E263D9C0600A7CD1A /* LibraryView.swift in Sources */, 53A089D0264DA9DA00D57806 /* MovieItemView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelector.swift in Sources */, - 535BAEA7264A18AA005FA86D /* VideoPlayerView.swift in Sources */, 53E4E647263F6CF100F67C6B /* LibraryFilterView.swift in Sources */, 53892777263CBB000035E14B /* JellyApiTypings.swift in Sources */, 5377CBF7263B596A003A4E83 /* ContentView.swift in Sources */, 53987CA82657424A00E7EA70 /* EpisodeItemView.swift in Sources */, 5389277C263CC3DB0035E14B /* BlurHashDecode.swift in Sources */, 53987CA626572F0700E7EA70 /* SeriesItemView.swift in Sources */, - 5335256E265E8D5A006CCA86 /* VideoPlayerViewRefactored.swift in Sources */, 539B2DA5263BA5B8007FF1A4 /* SettingsView.swift in Sources */, AE8C3156265D616A008AA076 /* SettingsViewModel.swift in Sources */, 5338F74E263B61370014BF09 /* ConnectToServerView.swift in Sources */, diff --git a/JellyfinPlayer/VideoPlayerView.swift b/JellyfinPlayer/VideoPlayerView.swift deleted file mode 100644 index aa1d4448..00000000 --- a/JellyfinPlayer/VideoPlayerView.swift +++ /dev/null @@ -1,585 +0,0 @@ -// -// VideoPlayerView.swift -// JellyfinPlayer -// -// Created by Aiden Vigue on 5/10/21. -// -/* -import SwiftUI -import SwiftyJSON -import SwiftyRequest -import AVKit -import MobileVLCKit -import Foundation -import NotificationCenter - -struct Subtitle { - var name: String; - var id: Int32; - var url: URL; - var delivery: String; - var codec: String; -} - -extension String { - public func leftPad(toWidth width: Int, withString string: String?) -> String { - let paddingString = string ?? " " - - if self.count >= width { - return self - } - - let remainingLength: Int = width - self.count - var padString = String() - for _ in 0 ..< remainingLength { - padString += paddingString - } - - return "\(padString)\(self)" - } -} - -struct VideoPlayerView: View { - @EnvironmentObject var globalData: GlobalData - @State private var pbitem: PlaybackItem = PlaybackItem(videoType: VideoType.direct, videoUrl: URL(string: "https://example.com")!, subtitles: []); - @State private var streamLoading = false; - @State private var vlcplayer: VLCMediaPlayer = VLCMediaPlayer(); - @State private var isPlaying = false; - @State private var subtitles: [Subtitle] = []; - @State private var audioTracks: [Subtitle] = []; - @State private var inactivity: Bool = true; - @State private var lastActivityTime: Double = 0; - @State private var scrub: Double = 0; - @State private var timeText: String = "-:--:--"; - @State private var playPauseButtonSystemName: String = "pause"; - @State private var playSessionId: String = ""; - @State private var lastPosition: Double = 0; - @State private var iterations: Int = 0; - @State private var startTime: Int = 0; - @State private var hasSentPlayReport: Bool = false; - @State private var selectedVideoQuality: Int = 0; - @State private var captionConfiguration: Bool = false { - didSet { - if(captionConfiguration == false) { - DispatchQueue.global(qos: .userInitiated).async { [self] in - vlcplayer.pause() - usleep(10000); - vlcplayer.play() - usleep(10000); - vlcplayer.pause() - usleep(10000); - vlcplayer.play() - } - } - } - }; - - @State private var playbackSettings: Bool = false; - @State private var selectedCaptionTrack: Int32 = -1; - @State private var selectedAudioTrack: Int32 = -1; - - var playing: Binding; - var item: DetailItem; - - init(item: DetailItem, playing: Binding) { - self.item = item; - self.playing = playing; - } - - @State var lastProgressReportSent: Double = CACurrentMediaTime() - - func keepUpWithPlayerState() { - if(!vlcplayer.isPlaying) { - while(!vlcplayer.isPlaying) {} - } - - sendProgressReport(eventName: "unpause") - - while(vlcplayer.state != VLCMediaPlayerState.stopped) { - _streamLoading.wrappedValue = false; - while(vlcplayer.isPlaying) { - vlcplayer.currentVideoSubTitleIndex = _selectedCaptionTrack.wrappedValue; - usleep(500000) - if(CACurrentMediaTime() - lastProgressReportSent > 10) { - sendProgressReport(eventName: "timeupdate") - _lastProgressReportSent.wrappedValue = CACurrentMediaTime() - } - if(vlcplayer.time.intValue != 0) { - _scrub.wrappedValue = Double(Double(vlcplayer.time.intValue) / Double(vlcplayer.time.intValue + abs(vlcplayer.remainingTime.intValue))); - - //Turn remainingTime into text - let remainingTime = abs(vlcplayer.remainingTime.intValue)/1000; - let hours = remainingTime / 3600; - let minutes = (remainingTime % 3600) / 60; - let seconds = (remainingTime % 3600) % 60; - if(hours != 0) { - timeText = "\(Int(hours)):\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))"; - } else { - timeText = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))"; - } - } - if(CACurrentMediaTime() - _lastActivityTime.wrappedValue > 5 && vlcplayer.state != VLCMediaPlayerState.paused) { - _inactivity.wrappedValue = true - } - if((lastPosition == Double(vlcplayer.position) && vlcplayer.state != VLCMediaPlayerState.paused)) { - if(iterations > 5) { - _iterations.wrappedValue = 0; - _streamLoading.wrappedValue = true; - } - _iterations.wrappedValue+=1; - } else { - _iterations.wrappedValue = 0; - _streamLoading.wrappedValue = false; - } - if(vlcplayer.state == VLCMediaPlayerState.error) { - playing.wrappedValue = false; - } - _lastPosition.wrappedValue = Double(vlcplayer.position) - } - } - } - - func sendProgressReport(eventName: String) { - var progressBody: String = ""; - if(pbitem.videoType == VideoType.direct) { - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":\(vlcplayer.state == VLCMediaPlayerState.paused ? "true" : "false"),\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(vlcplayer.position * Float(item.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":569735888.888889}],\"PlayMethod\":\"DirectStream\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"EventName\":\"\(eventName)\"}"; - } else { - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":\(vlcplayer.state == VLCMediaPlayerState.paused ? "true" : "false"),\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(vlcplayer.position * Float(item.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":569735888.888889}],\"PlayMethod\":\"Transcode\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"EventName\":\"\(eventName)\"}"; - } - - print(""); - print("Sending progress report") - print(progressBody) - - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Progress") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = progressBody.data(using: .ascii); - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let resp): - print(resp.body) - break - case .failure(let error): - debugPrint(error) - break - } - } - } - - func sendStopReport() { - var progressBody: String = ""; - if(pbitem.videoType == VideoType.direct) { - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":true,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(vlcplayer.position * Float(item.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[],\"PlayMethod\":\"DirectStream\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(item.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; - } else { - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":true,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(vlcplayer.position * Float(item.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":100000}],\"PlayMethod\":\"Transcode\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(item.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; - } - - print(""); - print("Sending stop report") - print(progressBody) - - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Stopped") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = progressBody.data(using: .ascii); - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let resp): - print(resp.body) - break - case .failure(let error): - debugPrint(error) - break - } - } - } - - func sendPlayReport() { - var progressBody: String = ""; - _startTime.wrappedValue = Int(Date().timeIntervalSince1970) * 10000000 - if(pbitem.videoType == VideoType.hls) { - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":false,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(item.Progress)),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[],\"PlayMethod\":\"Transcode\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(item.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; - } else { - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":false,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(item.Progress)),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[],\"PlayMethod\":\"DirectStream\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(item.Id)\",\"CanSeek\":true,\"ItemId\":\"\(item.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(item.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; - } - - print(""); - print("Sending play report") - print(progressBody) - - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = progressBody.data(using: .ascii); - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let resp): - print(resp.body) - break - case .failure(let error): - debugPrint(error) - break - } - } - } - - func startStream() { - - let builder = DeviceProfileBuilder() - - let defaults = UserDefaults.standard; - if(globalData.isInNetwork) { - builder.setMaxBitrate(bitrate: defaults.integer(forKey: "InNetworkBandwidth")) - } else { - builder.setMaxBitrate(bitrate: defaults.integer(forKey: "OutOfNetworkBandwidth")) - } - print(builder.bitrate) - _selectedVideoQuality.wrappedValue = builder.bitrate; - - let DeviceProfile = builder.buildProfile() - - let jsonEncoder = JSONEncoder() - let jsonData = try! jsonEncoder.encode(DeviceProfile) - let jsonString = String(data: jsonData, encoding: .ascii)! - print(jsonString) - - _streamLoading.wrappedValue = true; - let url = (globalData.server?.baseURI ?? "") + "/Items/\(item.Id)/PlaybackInfo?UserId=\(globalData.user?.user_id ?? "")&StartTimeTicks=\(Int(item.Progress))&IsPlayback=true&AutoOpenLiveStream=true&MaxStreamingBitrate=\(DeviceProfile.DeviceProfile.MaxStreamingBitrate)"; - print(url) - - let request = RestRequest(method: .post, url: url) - - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = jsonString.data(using: .ascii) - - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let response): - let body = response.body - do { - let json = try JSON(data: body) - _playSessionId.wrappedValue = json["PlaySessionId"].string ?? ""; - if(json["MediaSources"][0]["TranscodingUrl"].string != nil) { - print("Transcoding!") - let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")\((json["MediaSources"][0]["TranscodingUrl"].string ?? ""))")! - print(streamURL) - let item = PlaybackItem(videoType: VideoType.hls, videoUrl: streamURL, subtitles: []) - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "") - _subtitles.wrappedValue.append(disableSubtitleTrack); - for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] { - if(stream["Type"].string == "Subtitle") { //ignore ripped subtitles - we don't want to extract subtitles - let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")! - let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "") - _subtitles.wrappedValue.append(subtitle); - } - - if(stream["Type"].string == "Audio") { - let deliveryUrl = URL(string: "https://example.com")! - let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["IsExternal"].boolValue ? "External" : "Embed", codec: stream["Codec"].string ?? "") - if(stream["IsDefault"].boolValue) { - _selectedAudioTrack.wrappedValue = Int32(stream["Index"].int ?? 0); - } - _audioTracks.wrappedValue.append(subtitle); - } - } - - if(_selectedAudioTrack.wrappedValue == -1) { - if(_audioTracks.wrappedValue.count > 0) { - _selectedAudioTrack.wrappedValue = _audioTracks.wrappedValue[0].id; - } - } - - let streamUrl = streamURL.absoluteString; - let segmentUrl = URL(string: streamUrl.replacingOccurrences(of: "master.m3u8", with: "hls1/main/0.ts"))! - var request2 = URLRequest(url: segmentUrl) - - request2.httpMethod = "GET" - let task = URLSession.shared.dataTask(with: request2) { (data, response2, error) in - DispatchQueue.global(qos: .utility).async { [self] in - self.sendPlayReport() - pbitem = item; - pbitem.subtitles = subtitles; - _isPlaying.wrappedValue = true; - } - } - task.resume() - } else { - print("Direct playing!"); - let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")/Videos/\(item.Id)/stream?Static=true&mediaSourceId=\(item.Id)&deviceId=\(globalData.user?.device_uuid ?? "")&api_key=\(globalData.authToken)&Tag=\(json["MediaSources"][0]["ETag"])")!; - let item = PlaybackItem(videoType: VideoType.direct, videoUrl: streamURL, subtitles: []) - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "") - _subtitles.wrappedValue.append(disableSubtitleTrack); - for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] { - if(stream["Type"].string == "Subtitle") { - let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")! - let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "") - _subtitles.wrappedValue.append(subtitle); - } - - if(stream["Type"].string == "Audio") { - let deliveryUrl = URL(string: "https://example.com")! - let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["IsExternal"].boolValue ? "External" : "Embed", codec: stream["Codec"].string ?? "") - if(stream["IsDefault"].boolValue) { - _selectedAudioTrack.wrappedValue = Int32(stream["Index"].int ?? 0); - } - _audioTracks.wrappedValue.append(subtitle); - } - } - - if(_selectedAudioTrack.wrappedValue == -1) { - _selectedAudioTrack.wrappedValue = _audioTracks.wrappedValue[0].id; - } - - sendPlayReport() - pbitem = item; - pbitem.subtitles = subtitles; - _isPlaying.wrappedValue = true; - } - - DispatchQueue.global(qos: .utility).async { [self] in - self.keepUpWithPlayerState() - } - } catch { - - } - break - case .failure(let error): - debugPrint(error) - break - } - } - } - - func processScrubbingState() { - let videoDuration = Double(vlcplayer.time.intValue + abs(vlcplayer.remainingTime.intValue))/1000 - while(vlcplayer.state != VLCMediaPlayerState.paused) {} - while(vlcplayer.state == VLCMediaPlayerState.paused) { - let secondsScrubbedTo = round(_scrub.wrappedValue * videoDuration); - let scrubRemaining = videoDuration - secondsScrubbedTo; - usleep(100000) - let remainingTime = scrubRemaining; - let hours = floor(remainingTime / 3600); - let minutes = (remainingTime.truncatingRemainder(dividingBy: 3600)) / 60; - let seconds = (remainingTime.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60); - if(hours != 0) { - timeText = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"; - } else { - timeText = "\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"; - } - } - } - - func resetTimer() { - print("resetTimer ran") - if(_inactivity.wrappedValue == false) { - _inactivity.wrappedValue = true; - return; - } - _lastActivityTime.wrappedValue = CACurrentMediaTime() - _inactivity.wrappedValue = false; - } - - var body: some View { - LoadingView(isShowing: ($streamLoading)) { - VLCPlayer(url: $pbitem, player: $vlcplayer, startTime: Int(item.Progress)).onDisappear(perform: { - _isPlaying.wrappedValue = false; - vlcplayer.stop() - }).padding(EdgeInsets(top: 0, leading: UIDevice.current.hasNotch ? 30 : 0, bottom: 0, trailing: UIDevice.current.hasNotch ? 30 : 0)) - } - .overlay( - VStack() { - HStack() { - HStack() { - Button() { - sendStopReport() - self.playing.wrappedValue = false; - } label: { - HStack() { - Image(systemName: "chevron.left").font(.system(size: 20)).foregroundColor(.white) - } - }.frame(width: 20) - Spacer() - Text(item.Name).font(.headline).fontWeight(.semibold).foregroundColor(.white).offset(x:20) - Spacer() - Button() { - vlcplayer.pause() - self.playbackSettings = true; - } label: { - HStack() { - Image(systemName: "gear").font(.system(size: 20)).foregroundColor(.white) - } - }.frame(width: 20).padding(.trailing,15) - Button() { - vlcplayer.pause() - self.captionConfiguration = true; - } label: { - HStack() { - Image(systemName: "captions.bubble").font(.system(size: 20)).foregroundColor(.white) - } - }.frame(width: 20) - } - Spacer() - }.padding(EdgeInsets(top: 55, leading: 40, bottom: 0, trailing: 40)) - Spacer() - HStack() { - Spacer() - Button() { - vlcplayer.jumpBackward(15) - } label: { - Image(systemName: "gobackward.15").font(.system(size: 40)).foregroundColor(.white) - }.padding(20) - Spacer() - Button() { - if(vlcplayer.state != VLCMediaPlayerState.paused) { - vlcplayer.pause() - playPauseButtonSystemName = "play" - sendProgressReport(eventName: "pause") - } else { - vlcplayer.play() - playPauseButtonSystemName = "pause" - sendProgressReport(eventName: "unpause") - } - } label: { - Image(systemName: playPauseButtonSystemName).font(.system(size: 55)).foregroundColor(.white) - }.padding(20).frame(width: 60, height: 60) - Spacer() - Button() { - vlcplayer.jumpForward(15) - } label: { - Image(systemName: "goforward.15").font(.system(size: 40)).foregroundColor(.white) - }.padding(20) - Spacer() - }.padding(.leading, -20) - Spacer() - HStack() { - Slider(value: $scrub, onEditingChanged: { bool in - let videoPosition = Double(vlcplayer.time.intValue) - let videoDuration = Double(vlcplayer.time.intValue + abs(vlcplayer.remainingTime.intValue)) - if(bool == true) { - vlcplayer.pause() - sendProgressReport(eventName: "pause") - DispatchQueue.global(qos: .utility).async { [self] in - self.processScrubbingState() - } - } else { - //Scrub is value from 0..1 - find position in video and add / or remove. - let secondsScrubbedTo = round(_scrub.wrappedValue * videoDuration); - let offset = secondsScrubbedTo - videoPosition; - sendProgressReport(eventName: "unpause") - vlcplayer.play() - if(offset > 0) { - vlcplayer.jumpForward(Int32(offset)/1000); - } else { - vlcplayer.jumpBackward(Int32(abs(offset))/1000); - } - } - }) - .accentColor(Color(red: 172/255, green: 92/255, blue: 195/255)) - Text(timeText).fontWeight(.semibold).frame(width: 80).foregroundColor(.white) - }.padding(EdgeInsets(top: -20, leading: 44, bottom: 42, trailing: 40)) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(.black).opacity(0.4)) - , alignment: .topLeading) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .onAppear(perform: startStream) - .navigationBarHidden(true) - .overrideViewPreference(.dark) - .preferredColorScheme(.dark) - .navigationBarBackButtonHidden(true) - .edgesIgnoringSafeArea(.all) - .withHostingWindow { window in - if let vc = window?.rootViewController { - let preferenceHost = vc as! PreferenceUIHostingController - preferenceHost._viewPreference = .dark - } - } - .statusBar(hidden: true) - .onTapGesture(perform: resetTimer) - .fullScreenCover(isPresented: self.$captionConfiguration) { - NavigationView() { - VStack() { - Form() { - Picker("Closed Captions", selection: $selectedCaptionTrack) { - ForEach(subtitles, id: \.id) { caption in - Text(caption.name).tag(caption.id) - } - }.onChange(of: selectedCaptionTrack) { track in - vlcplayer.currentVideoSubTitleIndex = track; - } - Picker("Audio Track", selection: $selectedAudioTrack) { - ForEach(audioTracks, id: \.id) { caption in - Text(caption.name).tag(caption.id) - } - }.onChange(of: selectedAudioTrack) { track in - vlcplayer.currentAudioTrackIndex = track; - } - } - Text("Subtitles may take a few moments to appear once selected.") - .font(.callout) - .foregroundColor(.secondary) - Spacer() - } - .navigationBarTitle("Audio & Captions", displayMode: .inline) - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - captionConfiguration = false; - playPauseButtonSystemName = "pause"; - } label: { - HStack() { - Text("Back").font(.callout) - } - } - } - } - }.edgesIgnoringSafeArea(.bottom) - } - EmptyView() - .fullScreenCover(isPresented: $playbackSettings) { - NavigationView() { - Form() { - Picker("Quality", selection: $selectedVideoQuality) { - Group { - Text("1080p - 60 Mbps").tag(60000000) - Text("1080p - 40 Mbps").tag(40000000) - Text("1080p - 20 Mbps").tag(20000000) - Text("1080p - 15 Mbps").tag(15000000) - Text("1080p - 10 Mbps").tag(10000000) - } - Group { - Text("720p - 8 Mbps").tag(8000000) - Text("720p - 6 Mbps").tag(6000000) - Text("720p - 4 Mbps").tag(4000000) - } - Text("480p - 3 Mbps").tag(3000000) - Text("480p - 1.5 Mbps").tag(2000000) - Text("480p - 740 Kbps").tag(1000000) - }.onChange(of: selectedVideoQuality) { quality in - print(quality) - } - } - .navigationBarTitle("Playback Settings", displayMode: .inline) - .toolbar { - ToolbarItemGroup(placement: .navigationBarLeading) { - Button { - playbackSettings = false; - playPauseButtonSystemName = "pause"; - } label: { - HStack() { - Text("Back").font(.callout) - } - } - } - } - }.edgesIgnoringSafeArea(.bottom) - } - } -} -*/ diff --git a/JellyfinPlayer/VideoPlayerViewRefactored.swift b/JellyfinPlayer/VideoPlayerViewRefactored.swift deleted file mode 100644 index fd295ff1..00000000 --- a/JellyfinPlayer/VideoPlayerViewRefactored.swift +++ /dev/null @@ -1,371 +0,0 @@ -// -// VideoPlayerViewRefactored.swift -// JellyfinPlayer -// -// Created by Aiden Vigue on 5/26/21. -// - -import SwiftUI -import MobileVLCKit -import Introspect -import SwiftyJSON -import SwiftyRequest - -struct VideoPlayerViewRefactored: View { - @EnvironmentObject private var globalData: GlobalData; - - @State private var shouldShowLoadingView: Bool = true; - @State private var itemPlayback: ItemPlayback; - - @State private var VLCPlayerObj = VLCMediaPlayer() - - @State private var scrub: Double = 0; // storage value for scrubbing - @State private var timeText: String = "-:--:--"; //shows time text on play overlay - @State private var startTime: Int = 0; //ticks since 1970 - @State private var selectedAudioTrack: Int32 = 0; - @State private var selectedCaptionTrack: Int32 = 0; - @State private var playSessionId: String = ""; - @State private var shouldOverlayShow: Bool = true; - @State private var show: Bool = true; - - @State private var subtitles: [Subtitle] = []; - @State private var audioTracks: [Subtitle] = []; // can reuse the same struct - - @State private var VLCItem: PlaybackItem = PlaybackItem(); - - init(itemPlayback: ItemPlayback) { - self.itemPlayback = itemPlayback - } - - var body: some View { - if(show) { - LoadingView(isShowing: $shouldShowLoadingView) { - EmptyView() - .padding(EdgeInsets(top: 0, leading: UIDevice.current.hasNotch ? 30 : 0, bottom: 0, trailing: UIDevice.current.hasNotch ? 30 : 0)) - } - .overlay( - Group { - if(shouldOverlayShow) { - VStack() { - HStack() { - HStack() { - Button() { - sendStopReport() - VLCPlayerObj.stop() - self.itemPlayback.shouldPlay = false; - } label: { - HStack() { - Image(systemName: "chevron.left").font(.system(size: 20)).foregroundColor(.white) - } - }.frame(width: 20) - Spacer() - Text(itemPlayback.itemToPlay.Name).font(.headline).fontWeight(.semibold).foregroundColor(.white).offset(x:20) - Spacer() - Button() { - VLCPlayerObj.pause() - } label: { - HStack() { - Image(systemName: "gear").font(.system(size: 20)).foregroundColor(.white) - } - }.frame(width: 20).padding(.trailing,15) - Button() { - VLCPlayerObj.pause() - } label: { - HStack() { - Image(systemName: "captions.bubble").font(.system(size: 20)).foregroundColor(.white) - } - }.frame(width: 20) - } - Spacer() - }.padding(EdgeInsets(top: 55, leading: 40, bottom: 0, trailing: 40)) - Spacer() - HStack() { - Spacer() - Button() { - VLCPlayerObj.jumpBackward(15) - } label: { - Image(systemName: "gobackward.15").font(.system(size: 40)).foregroundColor(.white) - }.padding(20) - Spacer() - Button() { - if(VLCPlayerObj.state != .paused) { - VLCPlayerObj.pause() - sendProgressReport(eventName: "pause") - } else { - VLCPlayerObj.play() - sendProgressReport(eventName: "unpause") - } - } label: { - if(VLCPlayerObj.state == .paused) { - Image(systemName: "play").font(.system(size: 55)).foregroundColor(.white) - } else { - Image(systemName: "pause").font(.system(size: 55)).foregroundColor(.white) - } - }.padding(20).frame(width: 60, height: 60) - Spacer() - Button() { - VLCPlayerObj.jumpForward(15) - } label: { - Image(systemName: "goforward.15").font(.system(size: 40)).foregroundColor(.white) - }.padding(20) - Spacer() - }.padding(.leading, -20) - Spacer() - HStack() { - Slider(value: $scrub, onEditingChanged: { bool in - let videoPosition = Double(VLCPlayerObj.time.intValue) - let videoDuration = Double(VLCPlayerObj.time.intValue + abs(VLCPlayerObj.remainingTime.intValue)) - if(bool == true) { - VLCPlayerObj.pause() - sendProgressReport(eventName: "pause") - } else { - //Scrub is value from 0..1 - find position in video and add / or remove. - let secondsScrubbedTo = round(_scrub.wrappedValue * videoDuration); - let offset = secondsScrubbedTo - videoPosition; - sendProgressReport(eventName: "unpause") - VLCPlayerObj.play() - if(offset > 0) { - VLCPlayerObj.jumpForward(Int32(offset)/1000); - } else { - VLCPlayerObj.jumpBackward(Int32(abs(offset))/1000); - } - } - }) - .accentColor(Color(red: 172/255, green: 92/255, blue: 195/255)) - Text(timeText).fontWeight(.semibold).frame(width: 80).foregroundColor(.white) - }.padding(EdgeInsets(top: -20, leading: 44, bottom: 42, trailing: 40)) - } - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .background(Color(.black).opacity(0.4)) - } - } - , alignment: .topLeading) - .introspectTabBarController { (UITabBarController) in - UITabBarController.tabBar.isHidden = true - } - .onTapGesture(perform: resetTimer) - .navigationBarHidden(true) - .navigationBarBackButtonHidden(true) - .statusBar(hidden: true) - .prefersHomeIndicatorAutoHidden(true) - .preferredColorScheme(.dark) - .edgesIgnoringSafeArea(.all) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) - .overrideViewPreference(.unspecified) - .supportedOrientations(.landscape) - .onAppear(perform: onAppear) - } else { - Text("test").onAppear(perform: { - print("ev appear") - usleep(10000); - _show.wrappedValue = true; - }) - } - } - - func onAppear() { - shouldShowLoadingView = true; - let builder = DeviceProfileBuilder() - - let defaults = UserDefaults.standard; - if(globalData.isInNetwork) { - builder.setMaxBitrate(bitrate: defaults.integer(forKey: "InNetworkBandwidth")) - } else { - builder.setMaxBitrate(bitrate: defaults.integer(forKey: "OutOfNetworkBandwidth")) - } - - let DeviceProfile = builder.buildProfile() - - let jsonEncoder = JSONEncoder() - let jsonData = try! jsonEncoder.encode(DeviceProfile) - - let url = (globalData.server?.baseURI ?? "") + "/Items/\(itemPlayback.itemToPlay.Id)/PlaybackInfo?UserId=\(globalData.user?.user_id ?? "")&StartTimeTicks=\(Int(itemPlayback.itemToPlay.Progress))&IsPlayback=true&AutoOpenLiveStream=true&MaxStreamingBitrate=\(DeviceProfile.DeviceProfile.MaxStreamingBitrate)"; - - let request = RestRequest(method: .post, url: url) - - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = jsonData - - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let response): - let body = response.body - do { - let json = try JSON(data: body) - _playSessionId.wrappedValue = json["PlaySessionId"].string ?? ""; - if(json["MediaSources"][0]["TranscodingUrl"].string != nil) { - let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")\((json["MediaSources"][0]["TranscodingUrl"].string ?? ""))")! - let item = PlaybackItem() - item.videoType = .hls - item.videoUrl = streamURL - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "") - _subtitles.wrappedValue.append(disableSubtitleTrack); - - for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] { - if(stream["Type"].string == "Subtitle") { //ignore ripped subtitles - we don't want to extract subtitles - let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")! - let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "") - _subtitles.wrappedValue.append(subtitle); - } - - if(stream["Type"].string == "Audio") { - let deliveryUrl = URL(string: "https://example.com")! - let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["IsExternal"].boolValue ? "External" : "Embed", codec: stream["Codec"].string ?? "") - if(stream["IsDefault"].boolValue) { - _selectedAudioTrack.wrappedValue = Int32(stream["Index"].int ?? 0); - } - _audioTracks.wrappedValue.append(subtitle); - } - } - - if(_selectedAudioTrack.wrappedValue == -1) { - if(_audioTracks.wrappedValue.count > 0) { - _selectedAudioTrack.wrappedValue = _audioTracks.wrappedValue[0].id; - } - } - - self.sendPlayReport() - VLCItem = item; - VLCItem.subtitles = subtitles; - } else { - print("Direct playing!"); - let streamURL: URL = URL(string: "\(globalData.server?.baseURI ?? "")/Videos/\(itemPlayback.itemToPlay.Id)/stream?Static=true&mediaSourceId=\(itemPlayback.itemToPlay.Id)&deviceId=\(globalData.user?.device_uuid ?? "")&api_key=\(globalData.authToken)&Tag=\(json["MediaSources"][0]["ETag"])")!; - let item = PlaybackItem() - item.videoUrl = streamURL - item.videoType = .direct - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "") - _subtitles.wrappedValue.append(disableSubtitleTrack); - for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] { - if(stream["Type"].string == "Subtitle") { - let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")! - let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "") - _subtitles.wrappedValue.append(subtitle); - } - - if(stream["Type"].string == "Audio") { - let deliveryUrl = URL(string: "https://example.com")! - let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["IsExternal"].boolValue ? "External" : "Embed", codec: stream["Codec"].string ?? "") - if(stream["IsDefault"].boolValue) { - _selectedAudioTrack.wrappedValue = Int32(stream["Index"].int ?? 0); - } - _audioTracks.wrappedValue.append(subtitle); - } - } - - if(_selectedAudioTrack.wrappedValue == -1) { - _selectedAudioTrack.wrappedValue = _audioTracks.wrappedValue[0].id; - } - - sendPlayReport() - _VLCItem.wrappedValue = item; - _VLCItem.wrappedValue.subtitles = subtitles; - } - - shouldShowLoadingView = false; - - /* - DispatchQueue.global(qos: .utility).async { [self] in - self.keepUpWithPlayerState() - } - */ - } catch { - - } - break - case .failure(let error): - debugPrint(error) - break - } - } - } - - func sendProgressReport(eventName: String) { - var progressBody: String = ""; - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":\(VLCPlayerObj.state == .paused ? "true" : "false"),\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(VLCPlayerObj.position * Float(itemPlayback.itemToPlay.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":569735888.888889}],\"PlayMethod\":\"\(VLCItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(itemPlayback.itemToPlay.Id)\",\"CanSeek\":true,\"ItemId\":\"\(itemPlayback.itemToPlay.Id)\",\"EventName\":\"\(eventName)\"}"; - - print(""); - print("Sending progress report") - print(progressBody) - - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Progress") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = progressBody.data(using: .ascii); - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let resp): - print(resp.body) - break - case .failure(let error): - debugPrint(error) - break - } - } - } - - func resetTimer() { - print("rt running") - show = false; - if(shouldOverlayShow == true) { - shouldOverlayShow = false - return; - } - shouldOverlayShow = true; - } - - func sendStopReport() { - var progressBody: String = ""; - - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":true,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(VLCPlayerObj.position * Float(itemPlayback.itemToPlay.RuntimeTicks))),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[{\"start\":0,\"end\":100000}],\"PlayMethod\":\"\(VLCItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(itemPlayback.itemToPlay.Id)\",\"CanSeek\":true,\"ItemId\":\"\(itemPlayback.itemToPlay.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(itemPlayback.itemToPlay.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; - - print(""); - print("Sending stop report") - print(progressBody) - - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing/Stopped") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = progressBody.data(using: .ascii); - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let resp): - print(resp.body) - break - case .failure(let error): - debugPrint(error) - break - } - } - } - - func sendPlayReport() { - var progressBody: String = ""; - _startTime.wrappedValue = Int(Date().timeIntervalSince1970) * 10000000 - - progressBody = "{\"VolumeLevel\":100,\"IsMuted\":false,\"IsPaused\":false,\"RepeatMode\":\"RepeatNone\",\"ShuffleMode\":\"Sorted\",\"MaxStreamingBitrate\":120000000,\"PositionTicks\":\(Int(itemPlayback.itemToPlay.Progress)),\"PlaybackStartTimeTicks\":\(startTime),\"AudioStreamIndex\":\(selectedAudioTrack),\"BufferedRanges\":[],\"PlayMethod\":\"\(VLCItem.videoType == .hls ? "Transcode" : "DirectStream")\",\"PlaySessionId\":\"\(playSessionId)\",\"PlaylistItemId\":\"playlistItem0\",\"MediaSourceId\":\"\(itemPlayback.itemToPlay.Id)\",\"CanSeek\":true,\"ItemId\":\"\(itemPlayback.itemToPlay.Id)\",\"NowPlayingQueue\":[{\"Id\":\"\(itemPlayback.itemToPlay.Id)\",\"PlaylistItemId\":\"playlistItem0\"}]}"; - - print(""); - print("Sending play report") - print(progressBody) - - let request = RestRequest(method: .post, url: (globalData.server?.baseURI ?? "") + "/Sessions/Playing") - request.headerParameters["X-Emby-Authorization"] = globalData.authHeader - request.contentType = "application/json" - request.acceptType = "application/json" - request.messageBody = progressBody.data(using: .ascii); - request.responseData() { (result: Result, RestError>) in - switch result { - case .success(let resp): - print(resp.body) - break - case .failure(let error): - debugPrint(error) - break - } - } - } -} diff --git a/JellyfinPlayer/Views/VideoPlayer.storyboard b/JellyfinPlayer/Views/VideoPlayer.storyboard index 7ba3f95c..acea156d 100644 --- a/JellyfinPlayer/Views/VideoPlayer.storyboard +++ b/JellyfinPlayer/Views/VideoPlayer.storyboard @@ -51,7 +51,7 @@ - + + + + @@ -109,6 +138,7 @@ + @@ -121,6 +151,8 @@ + + @@ -146,6 +178,8 @@ + + diff --git a/JellyfinPlayer/Views/VLCPlayer.swift b/JellyfinPlayer/Views/VideoPlayer.swift similarity index 88% rename from JellyfinPlayer/Views/VLCPlayer.swift rename to JellyfinPlayer/Views/VideoPlayer.swift index 2897333e..de21ae0b 100644 --- a/JellyfinPlayer/Views/VLCPlayer.swift +++ b/JellyfinPlayer/Views/VideoPlayer.swift @@ -1,13 +1,10 @@ // -// VLCPlayer.swift +// VideoPlayer.swift // JellyfinPlayer // -// Created by Aiden Vigue on 5/10/21. +// Created by Aiden Vigue on 5/26/21. // -//me realizing i shouldve just written the whole app in the mvvm system bc it makes so much more sense -//Please don't touch this ifle - import SwiftUI import MobileVLCKit import SwiftyJSON @@ -55,7 +52,9 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe @IBOutlet weak var videoControlsView: UIView! @IBOutlet weak var seekSlider: UISlider! @IBOutlet weak var titleLabel: UILabel! - + @IBOutlet weak var jumpBackButton: UIButton! + @IBOutlet weak var jumpForwardButton: UIButton! + var shouldShowLoadingScreen: Bool = false; var ssTargetValueOffset: Int = 0; var ssStartValue: Int = 0; @@ -63,6 +62,8 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe var paused: Bool = true; var lastTime: Float = 0.0; var startTime: Int = 0; + var controlsAppearTime: Double = 0; + var selectedAudioTrack: Int32 = 0; var selectedCaptionTrack: Int32 = 0; var playSessionId: String = ""; @@ -115,13 +116,27 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe } @IBAction func controlViewTapped(_ sender: Any) { - videoControlsView.isHidden = !videoControlsView.isHidden + videoControlsView.isHidden = true } @IBAction func contentViewTapped(_ sender: Any) { - videoControlsView.isHidden = !videoControlsView.isHidden + videoControlsView.isHidden = false + controlsAppearTime = CACurrentMediaTime() } + @IBAction func jumpBackTapped(_ sender: Any) { + if(paused == false) { + mediaPlayer.jumpBackward(15) + } + } + + @IBAction func jumpForwardTapped(_ sender: Any) { + if(paused == false) { + mediaPlayer.jumpForward(15) + } + } + + @IBOutlet weak var mainActionButton: UIButton! @IBAction func mainActionButtonPressed(_ sender: Any) { @@ -335,6 +350,11 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))"; } timeText.text = timeTextStr + + if(CACurrentMediaTime() - controlsAppearTime > 5) { + videoControlsView.isHidden = true; + controlsAppearTime = 10000000000000000000000; + } } else { paused = true; } @@ -474,71 +494,3 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable { func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType, context: UIViewControllerRepresentableContext) { } } - -/* -struct VLCPlayer: UIViewRepresentable{ - var url: Binding; - var player: Binding; - var startTime: Int; - - func updateUIView(_ uiView: PlayerUIView, context: UIViewRepresentableContext) { - uiView.url = self.url - if(self.url.wrappedValue.videoUrl.absoluteString != "https://example.com") { - uiView.videoSetup() - } - } - - func makeUIView(context: Context) -> PlayerUIView { - return PlayerUIView(frame: .zero, url: url, player: self.player, startTime: self.startTime); - } -} - -class PlayerUIView: UIView, VLCMediaPlayerDelegate { - - private var mediaPlayer: Binding; - var url:Binding - var lastUrl: PlaybackItem? - var startTime: Int - - init(frame: CGRect, url: Binding, player: Binding, startTime: Int) { - self.mediaPlayer = player; - self.url = url; - self.startTime = startTime; - super.init(frame: frame) - mediaPlayer.wrappedValue.delegate = self - mediaPlayer.wrappedValue.drawable = self - } - - func videoSetup() { - if(lastUrl == nil || lastUrl?.videoUrl != url.wrappedValue.videoUrl) { - lastUrl = url.wrappedValue - mediaPlayer.wrappedValue.stop() - mediaPlayer.wrappedValue.media = VLCMedia(url: url.wrappedValue.videoUrl) - self.url.wrappedValue.subtitles.forEach() { sub in - if(sub.id != -1 && sub.delivery == "External" && sub.codec != "subrip") { - mediaPlayer.wrappedValue.addPlaybackSlave(sub.url, type: .subtitle, enforce: false) - } - } - - mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFontSize:")), with: 14) - //mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate") - - DispatchQueue.global(qos: .utility).async { [weak self] in - self?.mediaPlayer.wrappedValue.play() - if(self?.startTime != 0) { - print(self?.startTime ?? "") - self?.mediaPlayer.wrappedValue.jumpForward(Int32(self!.startTime/10000000)) - } - } - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layoutSubviews() { - super.layoutSubviews() - } -} - */ From 9e9b5874409ada5b8bdb957e897bc956f6012851 Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Wed, 26 May 2021 22:23:56 -0400 Subject: [PATCH 5/6] Fix captions being displayed by default if not wanted. --- JellyfinPlayer/Views/VideoPlayer.storyboard | 12 +++++++----- JellyfinPlayer/Views/VideoPlayer.swift | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/JellyfinPlayer/Views/VideoPlayer.storyboard b/JellyfinPlayer/Views/VideoPlayer.storyboard index acea156d..947c947d 100644 --- a/JellyfinPlayer/Views/VideoPlayer.storyboard +++ b/JellyfinPlayer/Views/VideoPlayer.storyboard @@ -51,7 +51,7 @@ -