Add VideoPlayerCoordinator

rename router
This commit is contained in:
PangMo5 2021-08-25 17:37:42 +09:00
parent 74a9302021
commit 2aab9df5df
20 changed files with 314 additions and 215 deletions

View File

@ -146,6 +146,8 @@
6220D0BD26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; };
6220D0BE26D60D6600B8E046 /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */; };
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */; };
6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; };
6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */; };
6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; };
6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; };
624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; };
@ -370,6 +372,7 @@
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterCoordinator.swift; sourceTree = "<group>"; };
6220D0BC26D60D6600B8E046 /* ItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = "<group>"; };
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCoordinator.swift; sourceTree = "<group>"; };
6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerCoordinator.swift; sourceTree = "<group>"; };
6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = "<group>"; };
624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = "<group>"; };
625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = "<group>"; };
@ -788,6 +791,7 @@
6220D0B626D5EE1100B8E046 /* SearchCoordinator.swift */,
6220D0B926D6092100B8E046 /* FilterCoordinator.swift */,
6220D0BF26D61C5000B8E046 /* ItemCoordinator.swift */,
6220D0C526D62D8700B8E046 /* VideoPlayerCoordinator.swift */,
);
path = Coordinators;
sourceTree = "<group>";
@ -1185,6 +1189,7 @@
535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */,
53ABFDE5267974EF00886593 /* ViewModel.swift in Sources */,
531069582684E7EE00CFFDBA /* MediaInfoView.swift in Sources */,
6220D0C726D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */,
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
535870A62669D8AE00D05A09 /* LazyView.swift in Sources */,
@ -1261,6 +1266,7 @@
62E632F3267D54030063E547 /* DetailItemViewModel.swift in Sources */,
53DE4BD2267098F300739748 /* SearchBarView.swift in Sources */,
53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */,
6220D0C626D62D8700B8E046 /* VideoPlayerCoordinator.swift in Sources */,
621338B32660A07800A81A2A /* LazyView.swift in Sources */,
6220D0B126D5EC9900B8E046 /* SettingsCoordinator.swift in Sources */,
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */,

View File

@ -9,7 +9,7 @@ import SwiftUI
import Stinsen
struct ConnectToServerView: View {
@EnvironmentObject var main: ViewRouter<MainCoordinator.Route>
@EnvironmentObject var mainRouter: ViewRouter<MainCoordinator.Route>
@StateObject var viewModel = ConnectToServerViewModel()
@State var username = ""
@State var password = ""
@ -61,7 +61,7 @@ struct ConnectToServerView: View {
if SessionManager.current.doesUserHaveSavedSession(userID: publicUser.id!) {
let user = SessionManager.current.getSavedSession(userID: publicUser.id!)
SessionManager.current.loginWithSavedSession(user: user)
main.route(to: .mainTab)
mainRouter.route(to: .mainTab)
} else {
username = publicUser.name ?? ""
viewModel.selectedPublicUser = publicUser

View File

@ -32,7 +32,7 @@ struct ProgressBar: Shape {
}
struct ContinueWatchingView: View {
@EnvironmentObject var home: NavigationRouter<HomeCoordinator.Route>
@EnvironmentObject var homeRouter: NavigationRouter<HomeCoordinator.Route>
var items: [BaseItemDto]
@ -41,7 +41,7 @@ struct ContinueWatchingView: View {
LazyHStack {
ForEach(items, id: \.id) { item in
Button {
home.route(to: .item(viewModel: .init(id: item.id!)))
homeRouter.route(to: .item(viewModel: .init(id: item.id!)))
} label: {
VStack(alignment: .leading) {
ImageView(src: item.getBackdropImage(maxWidth: 320), bh: item.getBackdropImageBlurHash())

View File

@ -8,6 +8,7 @@
*/
import Foundation
import JellyfinAPI
import Stinsen
import SwiftUI
@ -22,6 +23,7 @@ final class ItemCoordinator: NavigationCoordinatable {
enum Route: NavigationRoute {
case item(viewModel: ItemViewModel)
case library(viewModel: LibraryViewModel, title: String)
case videoPlayer(item: BaseItemDto)
}
func resolveRoute(route: Route) -> Transition {
@ -30,6 +32,8 @@ final class ItemCoordinator: NavigationCoordinatable {
return .push(ItemCoordinator(viewModel: viewModel).eraseToAnyCoordinatable())
case let .library(viewModel, title):
return .push(LibraryCoordinator(viewModel: viewModel, title: title).eraseToAnyCoordinatable())
case let .videoPlayer(item):
return .fullScreen(NavigationViewCoordinator(VideoPlayerCoordinator(item: item)).eraseToAnyCoordinatable())
}
}

View File

@ -0,0 +1,31 @@
//
/*
* SwiftFin is subject to the terms of the Mozilla Public
* License, v2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import Foundation
import JellyfinAPI
import Stinsen
import SwiftUI
final class VideoPlayerCoordinator: NavigationCoordinatable {
var navigationStack = NavigationStack()
var item: BaseItemDto
init(item: BaseItemDto) {
self.item = item
}
enum Route: NavigationRoute {}
func resolveRoute(route: Route) -> Transition {}
@ViewBuilder
func start() -> some View {
VideoPlayerView(item: item)
}
}

View File

@ -9,7 +9,7 @@ import Stinsen
import SwiftUI
struct EpisodeItemView: View {
@EnvironmentObject var item: NavigationRouter<ItemCoordinator.Route>
@EnvironmentObject var itemRouter: NavigationRouter<ItemCoordinator.Route>
@StateObject var viewModel: EpisodeItemViewModel
@State private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass) var hSizeClass
@ -63,7 +63,6 @@ struct EpisodeItemView: View {
HStack {
// Play button
Button {
self.playbackInfo.itemToPlay = viewModel.item
self.playbackInfo.shouldShowPlayer = true
} label: {
HStack {
@ -134,7 +133,7 @@ struct EpisodeItemView: View {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
Button {
item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
} label: {
Text(genre.name ?? "").font(.footnote)
}
@ -151,7 +150,7 @@ struct EpisodeItemView: View {
ForEach(viewModel.item.people!, id: \.self) { person in
if person.type! == "Actor" {
Button {
item.route(to: .library(viewModel: .init(person: person), title: person.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(person: person), title: person.name ?? ""))
} label: {
VStack {
ImageView(src: person
@ -181,7 +180,7 @@ struct EpisodeItemView: View {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(viewModel.item.studios!, id: \.id) { studio in
Button {
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
} label: {
Text(studio.name ?? "").font(.footnote)
}
@ -199,7 +198,7 @@ struct EpisodeItemView: View {
Spacer().frame(width: 16)
ForEach(viewModel.similarItems, id: \.self) { similarItem in
Button {
item.route(to: .item(viewModel: .init(id: similarItem.id!)))
itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!)))
} label: {
PortraitItemView(item: similarItem)
}
@ -230,7 +229,6 @@ struct EpisodeItemView: View {
.cornerRadius(10)
Spacer().frame(height: 15)
Button {
self.playbackInfo.itemToPlay = viewModel.item
self.playbackInfo.shouldShowPlayer = true
} label: {
HStack {
@ -331,7 +329,7 @@ struct EpisodeItemView: View {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
Button {
item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
} label: {
Text(genre.name ?? "").font(.footnote)
}
@ -350,7 +348,7 @@ struct EpisodeItemView: View {
ForEach(viewModel.item.people!, id: \.self) { person in
if person.type! == "Actor" {
Button {
item
itemRouter
.route(to: .library(viewModel: .init(person: person),
title: person.name ?? ""))
} label: {
@ -384,7 +382,7 @@ struct EpisodeItemView: View {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(viewModel.item.studios!, id: \.id) { studio in
Button {
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
} label: {
Text(studio.name ?? "").font(.footnote)
}
@ -404,7 +402,7 @@ struct EpisodeItemView: View {
Spacer().frame(width: 16)
ForEach(viewModel.similarItems, id: \.self) { similarItem in
Button {
item.route(to: .item(viewModel: .init(id: similarItem.id!)))
itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!)))
} label: {
PortraitItemView(item: similarItem)
}

View File

@ -12,7 +12,7 @@ import SwiftUI
import Stinsen
struct HomeView: View {
@EnvironmentObject var home: NavigationRouter<HomeCoordinator.Route>
@EnvironmentObject var homeRouter: NavigationRouter<HomeCoordinator.Route>
@StateObject var viewModel = HomeViewModel()
@ViewBuilder
@ -37,7 +37,7 @@ struct HomeView: View {
.fontWeight(.bold)
Spacer()
Button {
home.route(to: .library(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? ""))
homeRouter.route(to: .library(viewModel: .init(parentID: libraryID, filters: viewModel.recentFilterSet), title: library?.name ?? ""))
} label: {
HStack {
Text("See All").font(.subheadline).fontWeight(.bold)
@ -61,7 +61,7 @@ struct HomeView: View {
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
home.route(to: .settings)
homeRouter.route(to: .settings)
} label: {
Image(systemName: "gear")
}

View File

@ -7,14 +7,15 @@
import Introspect
import JellyfinAPI
import Stinsen
import SwiftUI
class VideoPlayerItem: ObservableObject {
@Published var shouldShowPlayer: Bool = false
@Published var itemToPlay = BaseItemDto()
}
struct ItemView: View {
@EnvironmentObject var itemRouter: NavigationRouter<ItemCoordinator.Route>
@StateObject var viewModel: ItemViewModel
@StateObject private var videoPlayerItem = VideoPlayerItem()
@State private var videoIsLoading: Bool = false // This variable is only changed by the underlying VLC view.
@ -23,20 +24,6 @@ struct ItemView: View {
var body: some View {
ZStack {
NavigationLink(destination: LoadingViewNoBlur(isShowing: $videoIsLoading) {
VLCPlayerWithControls(item: videoPlayerItem.itemToPlay,
loadBinding: $videoIsLoading,
pBinding: _videoPlayerItem
.projectedValue
.shouldShowPlayer)
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.statusBar(hidden: true)
.edgesIgnoringSafeArea(.all)
.prefersHomeIndicatorAutoHidden(true)
}, isActive: $videoPlayerItem.shouldShowPlayer) {
EmptyView()
}
Group {
if let item = viewModel.item {
if item.type == "Movie" {
@ -59,6 +46,11 @@ struct ItemView: View {
.navigationBarBackButtonHidden(false)
.environmentObject(videoPlayerItem)
}
.onReceive(videoPlayerItem.$shouldShowPlayer) { flag in
guard flag,
let item = viewModel.item else { return }
self.itemRouter.route(to: .videoPlayer(item: item))
}
if viewModel.isLoading {
ProgressView()
}

View File

@ -9,7 +9,7 @@ import Stinsen
import SwiftUI
struct LatestMediaView: View {
@EnvironmentObject var home: NavigationRouter<HomeCoordinator.Route>
@EnvironmentObject var homeRouter: NavigationRouter<HomeCoordinator.Route>
@StateObject var viewModel: LatestMediaViewModel
var body: some View {
@ -18,7 +18,7 @@ struct LatestMediaView: View {
ForEach(viewModel.items, id: \.id) { item in
if item.type == "Series" || item.type == "Movie" {
Button {
home.route(to: .item(viewModel: .init(id: item.id!)))
homeRouter.route(to: .item(viewModel: .init(id: item.id!)))
} label: {
PortraitItemView(item: item)
}

View File

@ -7,8 +7,10 @@
import JellyfinAPI
import SwiftUI
import Stinsen
struct LibraryFilterView: View {
@EnvironmentObject var filterRouter: NavigationRouter<FilterCoordinator.Route>
@Environment(\.presentationMode) var presentationMode
@Binding var filters: LibraryFilters
var parentId: String = ""
@ -64,7 +66,7 @@ struct LibraryFilterView: View {
Button {
viewModel.resetFilters()
self.filters = viewModel.modifiedFilters
presentationMode.wrappedValue.dismiss()
filterRouter.dismiss()
} label: {
Text("Reset")
}
@ -74,7 +76,7 @@ struct LibraryFilterView: View {
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
Button {
presentationMode.wrappedValue.dismiss()
filterRouter.dismiss()
} label: {
Image(systemName: "xmark")
}
@ -83,7 +85,7 @@ struct LibraryFilterView: View {
Button {
viewModel.updateModifiedFilter()
self.filters = viewModel.modifiedFilters
presentationMode.wrappedValue.dismiss()
filterRouter.dismiss()
} label: {
Text("Apply")
}

View File

@ -10,14 +10,14 @@ import Stinsen
import SwiftUI
struct LibraryListView: View {
@EnvironmentObject var libraryList: NavigationRouter<LibraryListCoordinator.Route>
@EnvironmentObject var libraryListRouter: NavigationRouter<LibraryListCoordinator.Route>
@StateObject var viewModel = LibraryListViewModel()
var body: some View {
ScrollView {
LazyVStack {
Button {
libraryList.route(to: .library(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites"))
libraryListRouter.route(to: .library(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites"))
} label: {
ZStack {
HStack {
@ -62,7 +62,7 @@ struct LibraryListView: View {
ForEach(viewModel.libraries, id: \.id) { library in
if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" {
Button {
libraryList.route(to: .library(viewModel: .init(parentID: library.id), title: library.name ?? ""))
libraryListRouter.route(to: .library(viewModel: .init(parentID: library.id), title: library.name ?? ""))
} label: {
ZStack {
ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash())
@ -99,7 +99,7 @@ struct LibraryListView: View {
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
libraryList.route(to: .search(viewModel: .init(parentID: nil)))
libraryListRouter.route(to: .search(viewModel: .init(parentID: nil)))
} label: {
Image(systemName: "magnifyingglass")
}

View File

@ -11,7 +11,7 @@ import Stinsen
import SwiftUI
struct LibrarySearchView: View {
@EnvironmentObject var search: NavigationRouter<SearchCoordinator.Route>
@EnvironmentObject var searchRouter: NavigationRouter<SearchCoordinator.Route>
@StateObject var viewModel: LibrarySearchViewModel
@State private var searchQuery = ""
@ -81,7 +81,7 @@ struct LibrarySearchView: View {
LazyVGrid(columns: tracks) {
ForEach(items, id: \.id) { item in
Button {
search.route(to: .item(viewModel: .init(id: item.id!)))
searchRouter.route(to: .item(viewModel: .init(id: item.id!)))
} label: {
PortraitItemView(item: item)
}

View File

@ -10,7 +10,7 @@ import Stinsen
import SwiftUI
struct LibraryView: View {
@EnvironmentObject var library: NavigationRouter<LibraryCoordinator.Route>
@EnvironmentObject var libraryRouter: NavigationRouter<LibraryCoordinator.Route>
@StateObject var viewModel: LibraryViewModel
var title: String
@ -36,7 +36,7 @@ struct LibraryView: View {
ForEach(viewModel.items, id: \.id) { item in
if item.type != "Folder" {
Button {
library.route(to: .item(viewModel: .init(id: item.id!)))
libraryRouter.route(to: .item(viewModel: .init(id: item.id!)))
} label: {
PortraitItemView(item: item)
}
@ -95,12 +95,12 @@ struct LibraryView: View {
Label("Icon One", systemImage: "line.horizontal.3.decrease.circle")
.foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange))
.onTapGesture {
library
libraryRouter
.route(to: .filter(filters: $viewModel.filters, enabledFilterType: viewModel.enabledFilterType,
parentId: viewModel.parentID ?? ""))
}
Button {
library.route(to: .search(viewModel: .init(parentID: viewModel.parentID)))
libraryRouter.route(to: .search(viewModel: .init(parentID: viewModel.parentID)))
} label: {
Image(systemName: "magnifyingglass")
}

View File

@ -9,7 +9,7 @@ import Stinsen
import SwiftUI
struct MovieItemView: View {
@EnvironmentObject var item: NavigationRouter<ItemCoordinator.Route>
@EnvironmentObject var itemRouter: NavigationRouter<ItemCoordinator.Route>
@StateObject var viewModel: MovieItemViewModel
@State private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass)
@ -68,7 +68,6 @@ struct MovieItemView: View {
HStack {
// Play button
Button {
self.playbackInfo.itemToPlay = viewModel.item
self.playbackInfo.shouldShowPlayer = true
} label: {
HStack {
@ -139,7 +138,7 @@ struct MovieItemView: View {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
Button {
item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
} label: {
Text(genre.name ?? "").font(.footnote)
}
@ -156,7 +155,7 @@ struct MovieItemView: View {
ForEach(viewModel.item.people!, id: \.self) { person in
if person.type ?? "" == "Actor" {
Button {
item.route(to: .library(viewModel: .init(person: person), title: person.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(person: person), title: person.name ?? ""))
} label: {
VStack {
ImageView(src: person
@ -186,7 +185,7 @@ struct MovieItemView: View {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(viewModel.item.studios!, id: \.id) { studio in
Button {
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
} label: {
Text(studio.name ?? "").font(.footnote)
}
@ -204,7 +203,7 @@ struct MovieItemView: View {
Spacer().frame(width: 16)
ForEach(viewModel.similarItems, id: \.self) { similarItem in
Button {
item.route(to: .item(viewModel: .init(id: similarItem.id!)))
itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!)))
} label: {
PortraitItemView(item: similarItem)
}
@ -236,7 +235,6 @@ struct MovieItemView: View {
.cornerRadius(10)
Spacer().frame(height: 15)
Button {
self.playbackInfo.itemToPlay = viewModel.item
self.playbackInfo.shouldShowPlayer = true
} label: {
HStack {
@ -339,7 +337,7 @@ struct MovieItemView: View {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(viewModel.item.genreItems!, id: \.id) { genre in
Button {
item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
} label: {
Text(genre.name ?? "").font(.footnote)
}
@ -358,7 +356,7 @@ struct MovieItemView: View {
ForEach(viewModel.item.people!, id: \.self) { person in
if person.type! == "Actor" {
Button {
item
itemRouter
.route(to: .library(viewModel: .init(person: person),
title: person.name ?? ""))
} label: {
@ -392,7 +390,7 @@ struct MovieItemView: View {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(viewModel.item.studios!, id: \.id) { studio in
Button {
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
} label: {
Text(studio.name ?? "").font(.footnote)
}
@ -412,7 +410,7 @@ struct MovieItemView: View {
Spacer().frame(width: 16)
ForEach(viewModel.similarItems, id: \.self) { similarItem in
Button {
item.route(to: .item(viewModel: .init(id: similarItem.id!)))
itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!)))
} label: {
PortraitItemView(item: similarItem)
}

View File

@ -11,7 +11,7 @@ import Stinsen
import SwiftUI
struct NextUpView: View {
@EnvironmentObject var home: NavigationRouter<HomeCoordinator.Route>
@EnvironmentObject var homeRouter: NavigationRouter<HomeCoordinator.Route>
var items: [BaseItemDto]
@ -25,7 +25,7 @@ struct NextUpView: View {
LazyHStack {
ForEach(items, id: \.id) { item in
Button {
home.route(to: .item(viewModel: .init(id: item.id!)))
homeRouter.route(to: .item(viewModel: .init(id: item.id!)))
} label: {
PortraitItemView(item: item)
}

View File

@ -9,7 +9,7 @@ import Stinsen
import SwiftUI
struct SeasonItemView: View {
@EnvironmentObject var item: NavigationRouter<ItemCoordinator.Route>
@EnvironmentObject var itemRouter: NavigationRouter<ItemCoordinator.Route>
@StateObject var viewModel: SeasonItemViewModel
@State private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass) var hSizeClass
@ -68,7 +68,7 @@ struct SeasonItemView: View {
.padding(.trailing, 16)
ForEach(viewModel.episodes, id: \.id) { episode in
Button {
item.route(to: .item(viewModel: .init(id: episode.id!)))
itemRouter.route(to: .item(viewModel: .init(id: episode.id!)))
} label: {
HStack {
ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
@ -134,7 +134,7 @@ struct SeasonItemView: View {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(viewModel.item.studios!, id: \.id) { studio in
Button {
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
} label: {
Text(studio.name ?? "").font(.footnote)
}
@ -184,7 +184,7 @@ struct SeasonItemView: View {
.padding(.trailing, 16)
ForEach(viewModel.episodes, id: \.id) { episode in
Button {
item.route(to: .item(viewModel: .init(id: episode.id!)))
itemRouter.route(to: .item(viewModel: .init(id: episode.id!)))
} label: {
HStack {
ImageView(src: episode.getPrimaryImage(maxWidth: 150), bh: episode.getPrimaryImageBlurHash())
@ -229,7 +229,7 @@ struct SeasonItemView: View {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(viewModel.item.studios!, id: \.id) { studio in
Button {
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
} label: {
Text(studio.name ?? "").font(.footnote)
}

View File

@ -9,7 +9,7 @@ import Stinsen
import SwiftUI
struct SeriesItemView: View {
@EnvironmentObject var item: NavigationRouter<ItemCoordinator.Route>
@EnvironmentObject var itemRouter: NavigationRouter<ItemCoordinator.Route>
@StateObject var viewModel: SeriesItemViewModel
@State private var orientation = UIDeviceOrientation.unknown
@Environment(\.horizontalSizeClass) var hSizeClass
@ -78,7 +78,7 @@ struct SeriesItemView: View {
Text("Genres:").font(.callout).fontWeight(.semibold)
ForEach(genreItems, id: \.id) { genre in
Button {
item.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(genre: genre), title: genre.name ?? ""))
} label: {
Text(genre.name ?? "").font(.footnote)
}
@ -101,7 +101,7 @@ struct SeriesItemView: View {
Text("Studios:").font(.callout).fontWeight(.semibold)
ForEach(studios, id: \.id) { studio in
Button {
item.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
itemRouter.route(to: .library(viewModel: .init(studio: studio), title: studio.name ?? ""))
} label: {
Text(studio.name ?? "").font(.footnote)
}
@ -119,7 +119,7 @@ struct SeriesItemView: View {
LazyVGrid(columns: tracks) {
ForEach(viewModel.seasons, id: \.id) { season in
Button {
item.route(to: .item(viewModel: .init(id: season.id!)))
itemRouter.route(to: .item(viewModel: .init(id: season.id!)))
} label: {
PortraitItemView(item: season)
}
@ -140,7 +140,7 @@ struct SeriesItemView: View {
ForEach(people, id: \.self) { person in
if person.type == "Actor" {
Button {
item
itemRouter
.route(to: .library(viewModel: .init(person: person),
title: person.name ?? ""))
} label: {
@ -176,7 +176,7 @@ struct SeriesItemView: View {
LazyHStack(spacing: 16) {
ForEach(viewModel.similarItems, id: \.self) { similarItem in
Button {
item.route(to: .item(viewModel: .init(id: similarItem.id!)))
itemRouter.route(to: .item(viewModel: .init(id: similarItem.id!)))
} label: {
PortraitItemView(item: similarItem)
}

View File

@ -11,8 +11,8 @@ import Stinsen
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var main: ViewRouter<MainCoordinator.Route>
@EnvironmentObject var settings: NavigationRouter<SettingsCoordinator.Route>
@EnvironmentObject var mainRouter: ViewRouter<MainCoordinator.Route>
@EnvironmentObject var settingsRouter: NavigationRouter<SettingsCoordinator.Route>
@Environment(\.managedObjectContext) private var viewContext
@ObservedObject var viewModel: SettingsViewModel
@ -97,16 +97,16 @@ struct SettingsView: View {
Spacer()
Button {
print("logging out")
main.route(to: .connectToServer)
settings.dismiss()
mainRouter.route(to: .connectToServer)
settingsRouter.dismiss()
} label: {
Text("Switch user").font(.callout)
}
}
Button {
SessionManager.current.logout()
main.route(to: .connectToServer)
settings.dismiss()
mainRouter.route(to: .connectToServer)
settingsRouter.dismiss()
} label: {
Text("Sign out").font(.callout)
}
@ -116,7 +116,7 @@ struct SettingsView: View {
.toolbar {
ToolbarItemGroup(placement: .navigationBarLeading) {
Button {
settings.dismiss()
settingsRouter.dismiss()
} label: {
Image(systemName: "xmark")
}

View File

@ -11,16 +11,16 @@ import Stinsen
import SwiftUI
struct SplashView: View {
@EnvironmentObject var main: ViewRouter<MainCoordinator.Route>
@EnvironmentObject var mainRouter: ViewRouter<MainCoordinator.Route>
@StateObject var viewModel = SplashViewModel()
var body: some View {
ProgressView()
.onReceive(viewModel.$isLoggedIn) { flag in
if flag {
main.route(to: .mainTab)
mainRouter.route(to: .mainTab)
} else {
main.route(to: .connectToServer)
mainRouter.route(to: .connectToServer)
}
}
}

View File

@ -5,15 +5,15 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import SwiftUI
import MobileVLCKit
import Combine
import Defaults
import GoogleCast
import JellyfinAPI
import MediaPlayer
import Combine
import GoogleCast
import SwiftyJSON
import Defaults
import MobileVLCKit
import Stinsen
import SwiftUI
import SwiftyJSON
enum PlayerDestination {
case remote
@ -27,10 +27,9 @@ protocol PlayerViewControllerDelegate: AnyObject {
}
class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRemoteMediaClientListener {
@RouterObject
var main: ViewRouter<MainCoordinator.Route>?
weak var delegate: PlayerViewControllerDelegate?
var cancellables = Set<AnyCancellable>()
@ -68,9 +67,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
private var castDiscoveryManager: GCKDiscoveryManager {
return GCKCastContext.sharedInstance().discoveryManager
}
private var castSessionManager: GCKSessionManager {
return GCKCastContext.sharedInstance().sessionManager
}
var hasSentRemoteSeek: Bool = false
var selectedPlaybackSpeedIndex: Int = 3
@ -84,17 +85,19 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
var jumpForwardLength: VideoPlayerJumpLength {
return Defaults[.videoPlayerJumpForward]
}
var jumpBackwardLength: VideoPlayerJumpLength {
return Defaults[.videoPlayerJumpBackward]
}
var manifest: BaseItemDto = BaseItemDto()
var manifest = BaseItemDto()
var playbackItem = PlaybackItem()
var remoteTimeUpdateTimer: Timer?
var upNextViewModel: UpNextViewModel = UpNextViewModel()
var lastOri: UIInterfaceOrientation? = nil
var upNextViewModel = UpNextViewModel()
var lastOri: UIInterfaceOrientation?
// MARK: IBActions
@IBAction func seekSliderStart(_ sender: Any) {
if playerDestination == .local {
sendProgressReport(eventName: "pause")
@ -105,33 +108,36 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
@IBAction func seekSliderValueChanged(_ sender: Any) {
let videoDuration: Double = Double(manifest.runTimeTicks! / Int64(10_000_000))
let videoDuration = Double(manifest.runTimeTicks! / Int64(10_000_000))
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
let secondsScrubbedRemaining = videoDuration - secondsScrubbedTo
timeText.text = calculateTimeText(from: secondsScrubbedTo)
timeLeftText.text = calculateTimeText(from: secondsScrubbedRemaining)
}
private func calculateTimeText(from duration: Double) -> String {
let hours = floor(duration / 3600)
let minutes = (duration.truncatingRemainder(dividingBy: 3600)) / 60
let seconds = (duration.truncatingRemainder(dividingBy: 3600)).truncatingRemainder(dividingBy: 60)
let minutes = duration.truncatingRemainder(dividingBy: 3600) / 60
let seconds = duration.truncatingRemainder(dividingBy: 3600).truncatingRemainder(dividingBy: 60)
let timeText: String
if hours != 0 {
timeText = "\(Int(hours)):\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "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"))"
timeText =
"\(String(Int(floor(minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int(floor(seconds))).leftPad(toWidth: 2, withString: "0"))"
}
return timeText
}
@IBAction func seekSliderEnd(_ sender: Any) {
isSeeking = false
let videoPosition = playerDestination == .local ? Double(mediaPlayer.time.intValue / 1000) : Double(remotePositionTicks / Int(10_000_000))
let videoPosition = playerDestination == .local ? Double(mediaPlayer.time.intValue / 1000) :
Double(remotePositionTicks / Int(10_000_000))
let videoDuration = Double(manifest.runTimeTicks! / Int64(10_000_000))
// Scrub is value from 0..1 - find position in video and add / or remove.
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
@ -147,7 +153,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
sendProgressReport(eventName: "unpause")
} else {
sendJellyfinCommand(command: "Seek", options: [
"position": Int(secondsScrubbedTo)
"position": Int(secondsScrubbedTo),
])
}
}
@ -184,7 +190,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
if playerDestination == .local {
mediaPlayer.jumpBackward(jumpBackwardLength.rawValue)
} else {
self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000) - Int(jumpBackwardLength.rawValue)])
sendJellyfinCommand(command: "Seek",
options: ["position": (remotePositionTicks / 10_000_000) - Int(jumpBackwardLength.rawValue)])
}
}
}
@ -194,7 +201,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
if playerDestination == .local {
mediaPlayer.jumpForward(jumpForwardLength.rawValue)
} else {
self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000) + Int(jumpForwardLength.rawValue)])
sendJellyfinCommand(command: "Seek",
options: ["position": (remotePositionTicks / 10_000_000) + Int(jumpForwardLength.rawValue)])
}
}
}
@ -232,7 +240,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
optionsVC?.popoverPresentationController?.sourceView = playerSettingsButton
// Present the view controller (in a popover).
self.present(optionsVC!, animated: true) {
present(optionsVC!, animated: true) {
print("popover visible, pause playback")
self.mediaPlayer.pause()
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
@ -240,6 +248,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
// MARK: Cast methods
@IBAction func castButtonPressed(_ sender: Any) {
if selectedCastDevice == nil {
LogManager.shared.log.debug("Presenting Cast modal")
@ -250,7 +259,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
castDeviceVC?.popoverPresentationController?.sourceView = castButton
// Present the view controller (in a popover).
self.present(castDeviceVC!, animated: true) {
present(castDeviceVC!, animated: true) {
self.mediaPlayer.pause()
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
}
@ -258,8 +267,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
LogManager.shared.log.info("Stopping casting session: button was pressed.")
castSessionManager.endSessionAndStopCasting(true)
selectedCastDevice = nil
self.castButton.isEnabled = true
self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
castButton.isEnabled = true
castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
playerDestination = .local
}
}
@ -268,9 +277,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
LogManager.shared.log.debug("Cast modal dismissed")
castDeviceVC?.dismiss(animated: true, completion: nil)
if playerDestination == .local {
self.mediaPlayer.play()
mediaPlayer.play()
}
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
}
func castDeviceChanged() {
@ -284,11 +293,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
// MARK: Cast End
func settingsPopoverDismissed() {
optionsVC?.dismiss(animated: true, completion: nil)
if playerDestination == .local {
self.mediaPlayer.play()
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
mediaPlayer.play()
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
}
}
@ -330,7 +340,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
self.mediaPlayer.jumpForward(30)
self.sendProgressReport(eventName: "timeupdate")
} else {
self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks/10_000_000)+30])
self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks / 10_000_000) + 30])
}
return .success
}
@ -341,14 +351,14 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
self.mediaPlayer.jumpBackward(15)
self.sendProgressReport(eventName: "timeupdate")
} else {
self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks/10_000_000)-15])
self.sendJellyfinCommand(command: "Seek", options: ["position": (self.remotePositionTicks / 10_000_000) - 15])
}
return .success
}
// Scrubber
commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in
guard let self = self else {return .commandFailed}
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] (remoteEvent) -> MPRemoteCommandHandlerStatus in
guard let self = self else { return .commandFailed }
if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent {
let targetSeconds = event.positionTime
@ -358,14 +368,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
if self.playerDestination == .local {
if offset > 0 {
self.mediaPlayer.jumpForward(Int32(offset)/1000)
self.mediaPlayer.jumpForward(Int32(offset) / 1000)
} else {
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
self.mediaPlayer.jumpBackward(Int32(abs(offset)) / 1000)
}
self.sendProgressReport(eventName: "unpause")
} else {
}
} else {}
return .success
} else {
@ -387,17 +395,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video"
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks
if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) {
if let artworkImage = UIImage(data: imageData as Data) {
let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in
return artworkImage
let artwork = MPMediaItemArtwork(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in
artworkImage
})
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
}
}
@ -407,6 +415,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
// MARK: viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
if manifest.type == "Movie" {
@ -421,8 +430,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
DispatchQueue.main.async {
self.lastOri = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? nil
AppDelegate.orientationLock = .landscape
if(self.lastOri != nil) {
if self.lastOri != nil {
if !self.lastOri!.isLandscape {
UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
@ -430,7 +439,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
}
NotificationCenter.default.addObserver(self, selector: #selector(didChangedOrientation), name: UIDevice.orientationDidChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didChangedOrientation),
name: UIDevice.orientationDidChangeNotification, object: nil)
}
@objc func didChangedOrientation() {
@ -451,7 +461,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
let totalDevices = castDiscoveryManager.deviceCount
discoveredCastDevices = []
if totalDevices > 0 {
for i in 0...totalDevices-1 {
for i in 0 ... totalDevices - 1 {
let device = castDiscoveryManager.device(at: i)
discoveredCastDevices.append(device)
}
@ -470,11 +480,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.tabBarController?.tabBar.isHidden = false
self.navigationController?.isNavigationBarHidden = false
tabBarController?.tabBar.isHidden = false
navigationController?.isNavigationBarHidden = false
overrideUserInterfaceStyle = .unspecified
DispatchQueue.main.async {
if(self.lastOri != nil) {
if self.lastOri != nil {
AppDelegate.orientationLock = .all
UIDevice.current.setValue(self.lastOri!.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
@ -483,11 +493,12 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
// MARK: viewDidAppear
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
overrideUserInterfaceStyle = .dark
self.tabBarController?.tabBar.isHidden = true
self.navigationController?.isNavigationBarHidden = true
tabBarController?.tabBar.isHidden = true
navigationController?.isNavigationBarHidden = true
mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
// mediaPlayer.wrappedValue.perform(Selector(("setTextRendererFont:")), with: "Copperplate")
@ -500,7 +511,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
func setupMediaPlayer() {
// Fetch max bitrate from UserDefaults depending on current connection mode
let maxBitrate = Defaults[.inNetworkBandwidth]
print(maxBitrate)
@ -508,16 +518,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
let builder = DeviceProfileBuilder()
builder.setMaxBitrate(bitrate: maxBitrate)
let profile = builder.buildProfile()
let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true)
let playbackInfo = PlaybackInfoDto(userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate),
startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile,
autoOpenLiveStream: true)
DispatchQueue.global(qos: .userInitiated).async { [self] in
delegate?.showLoadingView(self)
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!, maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo)
MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: SessionManager.current.user.user_id!,
maxStreamingBitrate: Int(maxBitrate),
startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true,
playbackInfoDto: playbackInfo)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
case let .failure(error):
if let err = error as? ErrorResponse {
switch err {
case .error(401, _, _, _):
@ -528,7 +543,6 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
self.delegate?.exitPlayer(self)
}
}
break
}
}, receiveValue: { [self] response in
dump(response)
@ -541,7 +555,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
item.videoType = .transcode
item.videoUrl = streamURL!
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "")
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "",
languageCode: "")
subtitleTrackArray.append(disableSubtitleTrack)
// Loop through media streams and add to array
@ -553,7 +568,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} else {
deliveryUrl = nil
}
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "")
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl,
delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt",
languageCode: stream.language ?? "")
if subtitle.delivery != .encode {
subtitleTrackArray.append(subtitle)
@ -561,7 +578,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
if stream.type == .audio {
let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!))
let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "",
id: Int32(stream.index!))
if stream.isDefault! == true {
selectedAudioTrack = Int32(stream.index!)
}
@ -570,7 +588,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
if selectedAudioTrack == -1 {
if audioTrackArray.count > 0 {
if !audioTrackArray.isEmpty {
selectedAudioTrack = audioTrackArray[0].id
}
}
@ -579,13 +597,15 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
playbackItem = item
} else {
// Item will be directly played by the client.
let streamURL: URL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")!
let streamURL =
URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag ?? "")")!
let item = PlaybackItem()
item.videoUrl = streamURL
item.videoType = .directPlay
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "")
let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "",
languageCode: "")
subtitleTrackArray.append(disableSubtitleTrack)
// Loop through media streams and add to array
@ -597,7 +617,9 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
} else {
deliveryUrl = nil
}
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!, languageCode: stream.language ?? "")
let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl,
delivery: stream.deliveryMethod!, codec: stream.codec!,
languageCode: stream.language ?? "")
if subtitle.delivery != .encode {
subtitleTrackArray.append(subtitle)
@ -605,7 +627,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
if stream.type == .audio {
let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!))
let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "",
id: Int32(stream.index!))
if stream.isDefault! == true {
selectedAudioTrack = Int32(stream.index!)
}
@ -614,7 +637,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
if selectedAudioTrack == -1 {
if audioTrackArray.count > 0 {
if !audioTrackArray.isEmpty {
selectedAudioTrack = audioTrackArray[0].id
}
}
@ -630,7 +653,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
.store(in: &cancellables)
}
}
private func setupJumpLengthButtons() {
let buttonFont = UIFont.systemFont(ofSize: 35, weight: .regular)
jumpForwardButton.setImage(jumpForwardLength.generateForwardImage(with: buttonFont), for: .normal)
@ -641,7 +664,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
subtitleTrackArray.forEach { subtitle in
if Defaults[.isAutoSelectSubtitles] {
if Defaults[.autoSelectSubtitlesLangCode] == "Auto",
subtitle.languageCode.contains(Locale.current.languageCode ?? "") {
subtitle.languageCode.contains(Locale.current.languageCode ?? "")
{
selectedCaptionTrack = subtitle.id
mediaPlayer.currentVideoSubTitleIndex = subtitle.id
} else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) {
@ -688,21 +712,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
subtitleTrackArray.forEach { sub in
// stupid fxcking jeff decides to re-encode these when added.
// only add playback streams when codec not supported by VLC.
if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" {
if sub.id != -1, sub.delivery == .external, sub.codec != "subrip" {
mediaPlayer.addPlaybackSlave(sub.url!, type: .subtitle, enforce: false)
}
}
}
self.mediaHasStartedPlaying()
mediaHasStartedPlaying()
delegate?.hideLoadingView(self)
videoContentView.setNeedsLayout()
videoContentView.setNeedsDisplay()
self.view.setNeedsLayout()
self.view.setNeedsDisplay()
self.videoControlsView.setNeedsLayout()
self.videoControlsView.setNeedsDisplay()
view.setNeedsLayout()
view.setNeedsDisplay()
videoControlsView.setNeedsLayout()
videoControlsView.setNeedsDisplay()
mediaPlayer.pause()
mediaPlayer.play()
@ -710,6 +734,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
// MARK: VideoPlayerSettings Delegate
func subtitleTrackChanged(newTrackID: Int32) {
selectedCaptionTrack = newTrackID
mediaPlayer.currentVideoSubTitleIndex = newTrackID
@ -736,7 +761,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
// Create the swiftUI view
let contentView = UIHostingController(rootView: VideoUpNextView(viewModel: upNextViewModel))
self.upNextView.addSubview(contentView.view)
upNextView.addSubview(contentView.view)
contentView.view.backgroundColor = .clear
contentView.view.translatesAutoresizingMaskIntoConstraints = false
contentView.view.topAnchor.constraint(equalTo: upNextView.topAnchor).isActive = true
@ -746,7 +771,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
}
func getNextEpisode() {
TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id, limit: 2)
TvShowsAPI.getEpisodes(seriesId: manifest.seriesId!, userId: SessionManager.current.user.user_id!, startItemId: manifest.id,
limit: 2)
.sink(receiveCompletion: { completion in
print(completion)
}, receiveValue: { [self] response in
@ -795,21 +821,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe
setupMediaPlayer()
getNextEpisode()
}
}
// MARK: - GCKGenericChannelDelegate
extension PlayerViewController: GCKGenericChannelDelegate {
@objc func updateRemoteTime() {
castButton.setImage(UIImage(named: "CastConnected"), for: .normal)
if !paused {
remotePositionTicks = remotePositionTicks + 2_000_000; // add 0.2 secs every timer evt.
remotePositionTicks = remotePositionTicks + 2_000_000 // add 0.2 secs every timer evt.
}
if isSeeking == false {
let positiveSeconds = Double(remotePositionTicks/10_000_000)
let remainingSeconds = Double((manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000)
let positiveSeconds = Double(remotePositionTicks / 10_000_000)
let remainingSeconds = Double((manifest.runTimeTicks! - Int64(remotePositionTicks)) / 10_000_000)
timeText.text = calculateTimeText(from: positiveSeconds)
timeLeftText.text = calculateTimeText(from: remainingSeconds)
@ -828,14 +854,15 @@ extension PlayerViewController: GCKGenericChannelDelegate {
if hasSentRemoteSeek == false {
hasSentRemoteSeek = true
sendJellyfinCommand(command: "Seek", options: [
"position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position)
"position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position),
])
}
}
paused = json["data"]["PlayState"]["IsPaused"].boolValue
self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0
remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0
if remoteTimeUpdateTimer == nil {
remoteTimeUpdateTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateRemoteTime), userInfo: nil, repeats: true)
remoteTimeUpdateTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateRemoteTime),
userInfo: nil, repeats: true)
}
}
}
@ -853,7 +880,7 @@ extension PlayerViewController: GCKGenericChannelDelegate {
"serverId": ServerEnvironment.current.server.server_id!,
"serverVersion": "10.8.0",
"receiverName": castSessionManager.currentCastSession!.device.friendlyName!,
"subtitleBurnIn": false
"subtitleBurnIn": false,
]
let jsonData = JSON(payload)
@ -862,7 +889,13 @@ extension PlayerViewController: GCKGenericChannelDelegate {
if command == "Seek" {
remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000)
// Send playback report as Jellyfin Chromecast isn't smarter than a rock.
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false, positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId,
mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack),
subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false,
positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime),
volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType,
liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone,
nowPlayingQueue: [], playlistItemId: "playlistItem0")
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
.sink(receiveCompletion: { result in
@ -876,9 +909,10 @@ extension PlayerViewController: GCKGenericChannelDelegate {
}
// MARK: - GCKSessionManagerListener
extension PlayerViewController: GCKSessionManagerListener {
func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) {
self.sendStopReport()
sendStopReport()
mediaPlayer.stop()
playerDestination = .remote
@ -896,25 +930,25 @@ extension PlayerViewController: GCKSessionManagerListener {
let playNowOptions: [String: Any] = [
"items": [[
"Id": self.manifest.id!,
"Id": manifest.id!,
"ServerId": ServerEnvironment.current.server.server_id!,
"Name": self.manifest.name!,
"Type": self.manifest.type!,
"MediaType": self.manifest.mediaType!,
"IsFolder": self.manifest.isFolder!
]]
"Name": manifest.name!,
"Type": manifest.type!,
"MediaType": manifest.mediaType!,
"IsFolder": manifest.isFolder!,
]],
]
sendJellyfinCommand(command: "PlayNow", options: playNowOptions)
}
func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) {
self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
self.sessionDidStart(manager: sessionManager, didStart: session)
jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
sessionDidStart(manager: sessionManager, didStart: session)
}
func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) {
self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
self.sessionDidStart(manager: sessionManager, didStart: session)
jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk")
sessionDidStart(manager: sessionManager, didStart: session)
}
func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) {
@ -943,30 +977,29 @@ extension PlayerViewController: GCKSessionManagerListener {
}
// MARK: - VLCMediaPlayer Delegates
extension PlayerViewController: VLCMediaPlayerDelegate {
func mediaPlayerStateChanged(_ aNotification: Notification!) {
let currentState: VLCMediaPlayerState = mediaPlayer.state
switch currentState {
case .stopped :
case .stopped:
LogManager.shared.log.debug("Player state changed: STOPPED")
break
case .ended :
case .ended:
LogManager.shared.log.debug("Player state changed: ENDED")
break
case .playing :
case .playing:
LogManager.shared.log.debug("Player state changed: PLAYING")
sendProgressReport(eventName: "unpause")
delegate?.hideLoadingView(self)
paused = false
case .paused :
case .paused:
LogManager.shared.log.debug("Player state changed: PAUSED")
paused = true
case .opening :
case .opening:
LogManager.shared.log.debug("Player state changed: OPENING")
case .buffering :
case .buffering:
LogManager.shared.log.debug("Player state changed: BUFFERING")
delegate?.showLoadingView(self)
case .error :
case .error:
LogManager.shared.log.error("Video had error.")
sendStopReport()
case .esAdded:
@ -978,19 +1011,19 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
func mediaPlayerTimeChanged(_ aNotification: Notification!) {
let time = mediaPlayer.position
if abs(time-lastTime) > 0.00005 {
if abs(time - lastTime) > 0.00005 {
paused = false
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
seekSlider.setValue(mediaPlayer.position, animated: true)
delegate?.hideLoadingView(self)
if manifest.type == "Episode" && upNextViewModel.item != nil {
if manifest.type == "Episode", upNextViewModel.item != nil {
if time > 0.96 {
upNextView.isHidden = false
self.jumpForwardButton.isHidden = true
jumpForwardButton.isHidden = true
} else {
upNextView.isHidden = true
self.jumpForwardButton.isHidden = false
jumpForwardButton.isHidden = false
}
}
@ -998,7 +1031,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
timeLeftText.text = String(mediaPlayer.remainingTime.stringValue.dropFirst())
if CACurrentMediaTime() - controlsAppearTime > 5 {
self.smallNextUpView()
smallNextUpView()
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseOut, animations: {
self.videoControlsView.alpha = 0.0
}, completion: { (_: Bool) in
@ -1018,42 +1051,61 @@ extension PlayerViewController: VLCMediaPlayerDelegate {
}
}
struct VideoPlayerView: View {
var item: BaseItemDto
@State private var isLoading = false
var body: some View {
// Loading UI needs to be moved into ViewController later
LoadingViewNoBlur(isShowing: $isLoading) {
VLCPlayerWithControls(item: item, loadBinding: $isLoading)
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
.statusBar(hidden: true)
.edgesIgnoringSafeArea(.all)
.prefersHomeIndicatorAutoHidden(true)
}
}
}
// MARK: End VideoPlayerVC
struct VLCPlayerWithControls: UIViewControllerRepresentable {
var item: BaseItemDto
@Environment(\.presentationMode) var presentationMode
@RouterObject var playerRouter: NavigationRouter<VideoPlayerCoordinator.Route>?
var loadBinding: Binding<Bool>
var pBinding: Binding<Bool>
let loadBinding: Binding<Bool>
class Coordinator: NSObject, PlayerViewControllerDelegate {
let parent: VLCPlayerWithControls
let loadBinding: Binding<Bool>
let pBinding: Binding<Bool>
init(loadBinding: Binding<Bool>, pBinding: Binding<Bool>) {
init(parent: VLCPlayerWithControls, loadBinding: Binding<Bool>) {
self.parent = parent
self.loadBinding = loadBinding
self.pBinding = pBinding
}
func hideLoadingView(_ viewController: PlayerViewController) {
self.loadBinding.wrappedValue = false
loadBinding.wrappedValue = false
}
func showLoadingView(_ viewController: PlayerViewController) {
self.loadBinding.wrappedValue = true
loadBinding.wrappedValue = true
}
func exitPlayer(_ viewController: PlayerViewController) {
self.pBinding.wrappedValue = false
parent.playerRouter?.dismiss()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(loadBinding: self.loadBinding, pBinding: self.pBinding)
Coordinator(parent: self, loadBinding: loadBinding)
}
typealias UIViewControllerType = PlayerViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls.UIViewControllerType {
func makeUIViewController(context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) -> VLCPlayerWithControls
.UIViewControllerType
{
let storyboard = UIStoryboard(name: "VideoPlayer", bundle: nil)
let customViewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! PlayerViewController
customViewController.manifest = item
@ -1061,20 +1113,27 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable {
return customViewController
}
func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType, context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) {
}
func updateUIViewController(_ uiViewController: VLCPlayerWithControls.UIViewControllerType,
context: UIViewControllerRepresentableContext<VLCPlayerWithControls>) {}
}
// MARK: - Play State Update Methods
extension PlayerViewController {
func sendProgressReport(eventName: String) {
if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" {
var ticks: Int64 = Int64(mediaPlayer.position * Float(manifest.runTimeTicks!))
var ticks = Int64(mediaPlayer.position * Float(manifest.runTimeTicks!))
if ticks == 0 {
ticks = manifest.userData?.playbackPositionTicks ?? 0
}
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (mediaPlayer.state == .paused), isMuted: false, positionTicks: ticks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId,
mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack),
subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: mediaPlayer.state == .paused,
isMuted: false, positionTicks: ticks, playbackStartTimeTicks: Int64(startTime),
volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType,
liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone,
nowPlayingQueue: [], playlistItemId: "playlistItem0")
PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo)
.sink(receiveCompletion: { result in
@ -1087,7 +1146,10 @@ extension PlayerViewController {
}
func sendStopReport() {
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: [])
let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id,
positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil,
playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0",
nowPlayingQueue: [])
PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo)
.sink(receiveCompletion: { result in
@ -1103,7 +1165,13 @@ extension PlayerViewController {
print("sending play report!")
let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0")
let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId,
mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack),
subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false,
positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime),
volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType,
liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [],
playlistItemId: "playlistItem0")
PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo)
.sink(receiveCompletion: { result in
@ -1116,7 +1184,7 @@ extension PlayerViewController {
}
extension UINavigationController {
open override var childForHomeIndicatorAutoHidden: UIViewController? {
override open var childForHomeIndicatorAutoHidden: UIViewController? {
return nil
}
}