start rewriting videoplayerview
|
@ -102,7 +102,6 @@
|
|||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "Icon.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
|
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 6.4 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 232 KiB |
|
@ -206,17 +206,17 @@ struct ContentView: View {
|
|||
}
|
||||
Spacer().frame(height: 7)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Home")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showSettingsPopover = true;
|
||||
} label: {
|
||||
Image(systemName: "gear")
|
||||
.navigationTitle("Home")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
showSettingsPopover = true;
|
||||
} label: {
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.fullScreenCover( isPresented: $showSettingsPopover) { SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover) }
|
||||
}.fullScreenCover( isPresented: $showSettingsPopover) { SettingsView(viewModel: SettingsViewModel(), close: $showSettingsPopover) }
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.tabItem({
|
||||
|
|
|
@ -11,13 +11,13 @@ import SwiftyJSON
|
|||
import SDWebImageSwiftUI
|
||||
|
||||
struct EpisodeItemView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@State private var isLoading: Bool = true;
|
||||
@EnvironmentObject private var globalData: GlobalData
|
||||
@EnvironmentObject private var orientationInfo: OrientationInfo
|
||||
@EnvironmentObject private var playbackInfo: ItemPlayback
|
||||
var item: ResumeItem;
|
||||
@EnvironmentObject var orientationInfo: OrientationInfo
|
||||
var fullItem: DetailItem;
|
||||
@State private var playing: Bool = false;
|
||||
|
||||
@State private var isLoading: Bool = true;
|
||||
@State private var progressString: String = "";
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
|
||||
|
@ -195,7 +195,7 @@ struct EpisodeItemView: View {
|
|||
if(orientationInfo.orientation == .portrait) {
|
||||
GeometryReader { geometry in
|
||||
VStack() {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=450&quality=90&tag=\(fullItem.Backdrop)")!)
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.ParentBackdropItemId)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (fullItem.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.BackdropBlurHash), size: CGSize(width: 32, height: 32))!)
|
||||
|
@ -263,7 +263,10 @@ struct EpisodeItemView: View {
|
|||
VStack(alignment: .leading) {
|
||||
HStack() {
|
||||
//Play button
|
||||
NavigationLink(destination: VideoPlayerViewRefactored()) {
|
||||
Button {
|
||||
self.playbackInfo.itemToPlay = fullItem;
|
||||
self.playbackInfo.shouldPlay = true;
|
||||
} label: {
|
||||
HStack() {
|
||||
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
|
||||
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
|
||||
|
@ -271,7 +274,8 @@ struct EpisodeItemView: View {
|
|||
.frame(width: 120, height: 35)
|
||||
.background(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}.buttonStyle(PlainButtonStyle())
|
||||
.frame(width: 120, height: 35)
|
||||
Spacer()
|
||||
HStack() {
|
||||
Button() {
|
||||
|
@ -324,15 +328,15 @@ struct EpisodeItemView: View {
|
|||
WebImage(url: cast.Image)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 4, height: 4))!)
|
||||
Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 70, height: 70)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10).shadow(radius: 6)
|
||||
.cornerRadius(10)
|
||||
Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary)
|
||||
if(cast.Role != "") {
|
||||
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100)
|
||||
|
@ -399,7 +403,10 @@ struct EpisodeItemView: View {
|
|||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
Spacer().frame(height: 15)
|
||||
NavigationLink(destination: VideoPlayerViewRefactored()) {
|
||||
Button {
|
||||
self.playbackInfo.itemToPlay = fullItem;
|
||||
self.playbackInfo.shouldPlay = true;
|
||||
} label: {
|
||||
HStack() {
|
||||
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
|
||||
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
|
||||
|
@ -407,7 +414,8 @@ struct EpisodeItemView: View {
|
|||
.frame(width: 120, height: 35)
|
||||
.background(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}.buttonStyle(PlainButtonStyle())
|
||||
.frame(width: 120, height: 35)
|
||||
Spacer()
|
||||
}
|
||||
ScrollView() {
|
||||
|
@ -504,7 +512,7 @@ struct EpisodeItemView: View {
|
|||
WebImage(url: cast.Image)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 32, height: 32))!)
|
||||
Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
|
@ -512,7 +520,7 @@ struct EpisodeItemView: View {
|
|||
}
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10).shadow(radius: 6)
|
||||
.cornerRadius(10)
|
||||
Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary)
|
||||
if(cast.Role != "") {
|
||||
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100)
|
||||
|
|
|
@ -8,39 +8,48 @@
|
|||
import SwiftUI
|
||||
import Introspect
|
||||
|
||||
class ItemPlayback: ObservableObject {
|
||||
@Published var shouldPlay: Bool = false;
|
||||
@Published var itemToPlay: DetailItem = DetailItem();
|
||||
}
|
||||
|
||||
struct ItemView: View {
|
||||
var item: ResumeItem;
|
||||
@StateObject private var playback: ItemPlayback = ItemPlayback()
|
||||
|
||||
init(item: ResumeItem) {
|
||||
self.item = item;
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
NavigationLink(destination: EmptyView(), label: {})
|
||||
NavigationLink(destination: EmptyView(), label: {})
|
||||
if(item.Type == "Movie") {
|
||||
MovieItemView(item: self.item)
|
||||
} else if(item.Type == "Season") {
|
||||
SeasonItemView(item: self.item)
|
||||
} else if(item.Type == "Series") {
|
||||
SeriesItemView(item: self.item)
|
||||
} else if(item.Type == "Episode") {
|
||||
EpisodeItemView(item: self.item)
|
||||
} else {
|
||||
Text("Type: \(item.Type) not implemented yet :(")
|
||||
if(playback.shouldPlay) {
|
||||
VideoPlayerViewRefactored(itemPlayback: playback)
|
||||
} else {
|
||||
Group {
|
||||
if(item.Type == "Movie") {
|
||||
MovieItemView(item: self.item)
|
||||
} else if(item.Type == "Season") {
|
||||
SeasonItemView(item: self.item)
|
||||
} else if(item.Type == "Series") {
|
||||
SeriesItemView(item: self.item)
|
||||
} else if(item.Type == "Episode") {
|
||||
EpisodeItemView(item: self.item)
|
||||
} else {
|
||||
Text("Type: \(item.Type) not implemented yet :(")
|
||||
}
|
||||
}
|
||||
.introspectTabBarController { (UITabBarController) in
|
||||
UITabBarController.tabBar.isHidden = false
|
||||
}
|
||||
.navigationBarHidden(false)
|
||||
.navigationBarBackButtonHidden(false)
|
||||
.statusBar(hidden: false)
|
||||
.prefersHomeIndicatorAutoHidden(false)
|
||||
.preferredColorScheme(.none)
|
||||
.edgesIgnoringSafeArea([])
|
||||
.overrideViewPreference(.unspecified)
|
||||
.supportedOrientations(.allButUpsideDown)
|
||||
.environmentObject(playback)
|
||||
}
|
||||
.introspectTabBarController { (UITabBarController) in
|
||||
UITabBarController.tabBar.isHidden = false
|
||||
}
|
||||
.navigationBarHidden(false)
|
||||
.navigationBarBackButtonHidden(false)
|
||||
.statusBar(hidden: false)
|
||||
.prefersHomeIndicatorAutoHidden(false)
|
||||
.preferredColorScheme(.none)
|
||||
.edgesIgnoringSafeArea([])
|
||||
.overrideViewPreference(.unspecified)
|
||||
.supportedOrientations(.allButUpsideDown)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -181,6 +181,24 @@ extension View {
|
|||
}
|
||||
}
|
||||
|
||||
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)"
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
struct JellyfinPlayerApp: App {
|
||||
let persistenceController = PersistenceController.shared
|
||||
|
|
|
@ -60,16 +60,17 @@ class CastMember: ObservableObject {
|
|||
}
|
||||
|
||||
struct MovieItemView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
@EnvironmentObject var orientationInfo: OrientationInfo
|
||||
@EnvironmentObject private var globalData: GlobalData
|
||||
@EnvironmentObject private var orientationInfo: OrientationInfo
|
||||
@EnvironmentObject private var playbackInfo: ItemPlayback
|
||||
|
||||
@State private var isLoading: Bool = true;
|
||||
|
||||
var item: ResumeItem;
|
||||
var fullItem: DetailItem;
|
||||
@State private var playing: Bool = false;
|
||||
|
||||
@State private var progressString: String = "";
|
||||
@State private var viewDidLoad: Bool = false;
|
||||
|
||||
@State private var watched: Bool = false {
|
||||
didSet {
|
||||
if(watched == true) {
|
||||
|
@ -242,7 +243,7 @@ struct MovieItemView: View {
|
|||
if(orientationInfo.orientation == .portrait) {
|
||||
GeometryReader { geometry in
|
||||
VStack() {
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=450&quality=90&tag=\(fullItem.Backdrop)")!)
|
||||
WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(fullItem.Id)/Images/Backdrop?maxWidth=550&quality=90&tag=\(fullItem.Backdrop)")!)
|
||||
.resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size
|
||||
.placeholder {
|
||||
Image(uiImage: UIImage(blurHash: (fullItem.BackdropBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : fullItem.BackdropBlurHash), size: CGSize(width: 32, height: 32))!)
|
||||
|
@ -310,7 +311,10 @@ struct MovieItemView: View {
|
|||
VStack(alignment: .leading) {
|
||||
HStack() {
|
||||
//Play button
|
||||
NavigationLink(destination: VideoPlayerViewRefactored()) {
|
||||
Button {
|
||||
self.playbackInfo.itemToPlay = fullItem;
|
||||
self.playbackInfo.shouldPlay = true;
|
||||
} label: {
|
||||
HStack() {
|
||||
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
|
||||
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
|
||||
|
@ -318,7 +322,8 @@ struct MovieItemView: View {
|
|||
.frame(width: 120, height: 35)
|
||||
.background(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}.buttonStyle(PlainButtonStyle())
|
||||
.frame(width: 120, height: 35)
|
||||
Spacer()
|
||||
HStack() {
|
||||
Button() {
|
||||
|
@ -379,7 +384,7 @@ struct MovieItemView: View {
|
|||
}
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10).shadow(radius: 6)
|
||||
.cornerRadius(10)
|
||||
Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary)
|
||||
if(cast.Role != "") {
|
||||
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100)
|
||||
|
@ -445,7 +450,10 @@ struct MovieItemView: View {
|
|||
.cornerRadius(10)
|
||||
.shadow(radius: 5)
|
||||
Spacer().frame(height: 15)
|
||||
NavigationLink(destination: VideoPlayerViewRefactored()) {
|
||||
Button {
|
||||
self.playbackInfo.itemToPlay = fullItem;
|
||||
self.playbackInfo.shouldPlay = true;
|
||||
} label: {
|
||||
HStack() {
|
||||
Text(fullItem.Progress == 0 ? "Play" : "\(progressString) left").foregroundColor(Color.white).font(.callout).fontWeight(.semibold)
|
||||
Image(systemName: "play.fill").foregroundColor(Color.white).font(.system(size: 20))
|
||||
|
@ -453,7 +461,8 @@ struct MovieItemView: View {
|
|||
.frame(width: 120, height: 35)
|
||||
.background(Color(red: 172/255, green: 92/255, blue: 195/255))
|
||||
.cornerRadius(10)
|
||||
}
|
||||
}.buttonStyle(PlainButtonStyle())
|
||||
.frame(width: 120, height: 35)
|
||||
Spacer()
|
||||
}
|
||||
ScrollView() {
|
||||
|
@ -558,7 +567,7 @@ struct MovieItemView: View {
|
|||
}
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(10).shadow(radius: 6)
|
||||
.cornerRadius(10)
|
||||
Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary)
|
||||
if(cast.Role != "") {
|
||||
Text(cast.Role).font(.caption).fontWeight(.medium).lineLimit(1).foregroundColor(Color.secondary).frame(width: 100)
|
||||
|
|
|
@ -17,10 +17,18 @@ enum VideoType {
|
|||
case direct;
|
||||
}
|
||||
|
||||
struct PlaybackItem {
|
||||
var videoType: VideoType;
|
||||
var videoUrl: URL;
|
||||
var subtitles: [Subtitle];
|
||||
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{
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
//
|
||||
// Created by Aiden Vigue on 5/10/21.
|
||||
//
|
||||
|
||||
/*
|
||||
import SwiftUI
|
||||
import SwiftyJSON
|
||||
import SwiftyRequest
|
||||
|
@ -41,7 +41,6 @@ extension String {
|
|||
|
||||
struct VideoPlayerView: View {
|
||||
@EnvironmentObject var globalData: GlobalData
|
||||
var item: DetailItem;
|
||||
@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();
|
||||
|
@ -76,10 +75,11 @@ struct VideoPlayerView: View {
|
|||
};
|
||||
|
||||
@State private var playbackSettings: Bool = false;
|
||||
|
||||
@State private var selectedCaptionTrack: Int32 = -1;
|
||||
@State private var selectedAudioTrack: Int32 = -1;
|
||||
|
||||
var playing: Binding<Bool>;
|
||||
var item: DetailItem;
|
||||
|
||||
init(item: DetailItem, playing: Binding<Bool>) {
|
||||
self.item = item;
|
||||
|
@ -582,3 +582,4 @@ struct VideoPlayerView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
@ -8,17 +8,137 @@
|
|||
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 = false;
|
||||
|
||||
@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 {
|
||||
LoadingView(isShowing: $shouldShowLoadingView) {
|
||||
Text("content")
|
||||
.introspectTabBarController { (UITabBarController) in
|
||||
UITabBarController.tabBar.isHidden = true
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
, alignment: .topLeading)
|
||||
.introspectTabBarController { (UITabBarController) in
|
||||
UITabBarController.tabBar.isHidden = true
|
||||
}
|
||||
.onTapGesture(perform: resetTimer)
|
||||
.navigationBarHidden(true)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.statusBar(hidden: true)
|
||||
|
@ -28,5 +148,211 @@ struct VideoPlayerViewRefactored: View {
|
|||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
|
||||
.overrideViewPreference(.unspecified)
|
||||
.supportedOrientations(.landscape)
|
||||
.onAppear(perform: onAppear)
|
||||
}
|
||||
|
||||
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<RestResponse<Data>, 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<RestResponse<Data>, RestError>) in
|
||||
switch result {
|
||||
case .success(let resp):
|
||||
print(resp.body)
|
||||
break
|
||||
case .failure(let error):
|
||||
debugPrint(error)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resetTimer() {
|
||||
print("rt running")
|
||||
if(_shouldOverlayShow.wrappedValue == true) {
|
||||
_shouldOverlayShow.wrappedValue = false
|
||||
return;
|
||||
}
|
||||
_shouldOverlayShow.wrappedValue = 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<RestResponse<Data>, 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<RestResponse<Data>, RestError>) in
|
||||
switch result {
|
||||
case .success(let resp):
|
||||
print(resp.body)
|
||||
break
|
||||
case .failure(let error):
|
||||
debugPrint(error)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|