From 208bec783a4728c8fd81d208f96e2fbfbeb5a12d Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Wed, 26 May 2021 21:24:01 -0400 Subject: [PATCH] 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +