mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-12-11 16:06:09 +00:00
Add VideoPlayerCoordinator
rename router
This commit is contained in:
parent
74a9302021
commit
2aab9df5df
@ -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 */,
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
31
JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift
Normal file
31
JellyfinPlayer/Coordinators/VideoPlayerCoordinator.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user