Merge pull request #267 from LePips/final-work-for-winter-break-2022

Some polishing
This commit is contained in:
aiden 3 2022-01-08 14:15:26 -05:00 committed by GitHub
commit fa28b4019b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 292 additions and 100 deletions

View File

@ -0,0 +1,22 @@
//
/*
* 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
extension MediaStream {
func externalURL(base: String) -> URL? {
guard let deliveryURL = deliveryUrl else { return nil }
var baseComponents = URLComponents(string: base)
baseComponents?.path += deliveryURL
return baseComponents?.url
}
}

View File

@ -67,6 +67,8 @@ final class VideoPlayerViewModel: ViewModel {
}
@Published var autoplayEnabled: Bool {
willSet {
previousItemVideoPlayerViewModel?.autoplayEnabled = newValue
nextItemVideoPlayerViewModel?.autoplayEnabled = newValue
Defaults[.autoplayEnabled] = newValue
}
}
@ -115,6 +117,16 @@ final class VideoPlayerViewModel: ViewModel {
return Int64(currentSeconds) * 10_000_000
}
// MARK: Helpers
var currentAudioStream: MediaStream? {
return audioStreams.first(where: { $0.index == selectedAudioStreamIndex })
}
var currentSubtitleStream: MediaStream? {
return subtitleStreams.first(where: { $0.index == selectedSubtitleStreamIndex })
}
// Necessary PassthroughSubject to capture manual scrubbing from sliders
let sliderScrubbingSubject = PassthroughSubject<VideoPlayerViewModel, Never>()

View File

@ -7,6 +7,7 @@
* Copyright 2021 Aiden Vigue & Jellyfin Contributors
*/
import CachedAsyncImage
import SwiftUI
struct ImageView: View {
@ -31,7 +32,7 @@ struct ImageView: View {
private var failureImage: some View {
ZStack {
Rectangle()
.foregroundColor(Color.systemFill)
.foregroundColor(Color(UIColor.darkGray))
Text(failureInitials)
.font(.largeTitle)
@ -40,21 +41,36 @@ struct ImageView: View {
}
var body: some View {
AsyncImage(url: source) { phase in
if let image = phase.image {
CachedAsyncImage(url: source, urlCache: .imageCache, transaction: Transaction(animation: .easeInOut)) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fill)
} else if phase.error != nil {
case .failure(_):
failureImage
} else {
default:
// TODO: remove once placeholder hash image fixed
#if os(tvOS)
ZStack {
Color.gray.ignoresSafeArea()
Color.black.ignoresSafeArea()
ProgressView()
}
#else
ZStack {
Color.gray.ignoresSafeArea()
ProgressView()
}
#endif
}
}
}
}
extension URLCache {
static let imageCache = URLCache(memoryCapacity: 512*1000*1000, diskCapacity: 10*1000*1000*1000)
}

View File

@ -84,7 +84,7 @@ struct EpisodesRowView: View {
HStack(alignment: .top) {
VStack(alignment: .leading) {
ImageView(src: episode.getBackdropImage(maxWidth: 445),
ImageView(src: episode.getBackdropImage(maxWidth: 500),
bh: episode.getBackdropImageBlurHash())
.mask(Rectangle().frame(width: 500, height: 280))
.frame(width: 500, height: 280)

View File

@ -22,8 +22,13 @@ struct ContinueWatchingCard: View {
} label: {
ZStack(alignment: .bottom) {
ImageView(src: item.getBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
if item.itemType == .episode {
ImageView(src: item.getSeriesBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
} else {
ImageView(src: item.getBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
}
VStack(alignment: .leading, spacing: 0) {
Text(item.getItemProgressString() ?? "")
@ -57,6 +62,7 @@ struct ContinueWatchingCard: View {
.fontWeight(.semibold)
.foregroundColor(.primary)
.lineLimit(1)
.frame(width: 500, alignment: .leading)
if item.itemType == .episode {
Text(item.getEpisodeLocator() ?? "")
@ -64,8 +70,11 @@ struct ContinueWatchingCard: View {
.fontWeight(.medium)
.foregroundColor(.secondary)
.lineLimit(1)
} else {
Text("")
}
}
}
.padding(.vertical)
}
}

View File

@ -25,7 +25,7 @@ struct ContinueWatchingView: View {
.padding(.leading, 50)
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
LazyHStack(alignment: .top) {
ForEach(items, id: \.self) { item in
ContinueWatchingCard(item: item)
}

View File

@ -31,6 +31,7 @@ struct CinematicEpisodeItemView: View {
ImageView(src: viewModel.item.getBackdropImage(maxWidth: 1920),
bh: viewModel.item.getBackdropImageBlurHash())
.frame(height: UIScreen.main.bounds.height - 10)
.ignoresSafeArea()
ScrollView {

View File

@ -16,10 +16,60 @@ struct LatestMediaView: View {
@Default(.showPosterLabels) var showPosterLabels
var body: some View {
PortraitItemsRowView(rowTitle: L10n.latestWithString(viewModel.library.name ?? ""),
items: viewModel.items,
showItemTitles: showPosterLabels) { item in
homeRouter.route(to: \.modalItem, item)
VStack(alignment: .leading) {
L10n.latestWithString(viewModel.library.name ?? "").text
.font(.title3)
.padding(.horizontal, 50)
ScrollView(.horizontal) {
HStack(alignment: .top) {
ForEach(viewModel.items, id: \.self) { item in
VStack(spacing: 15) {
Button {
homeRouter.route(to: \.modalItem, item)
} label: {
ImageView(src: item.portraitHeaderViewURL(maxWidth: 257))
.frame(width: 257, height: 380)
}
.frame(height: 380)
.buttonStyle(PlainButtonStyle())
if showPosterLabels {
Text(item.title)
.lineLimit(2)
.frame(width: 257)
}
}
}
Button {
homeRouter.route(to: \.library, (viewModel: .init(parentID: viewModel.library.id!,
filters: LibraryFilters(filters: [], sortOrder: [.descending], sortBy: [.dateAdded])),
title: viewModel.library.name ?? ""))
} label: {
ZStack {
Color(UIColor.darkGray)
.opacity(0.5)
VStack(spacing: 20) {
Image(systemName: "chevron.right")
.font(.title)
L10n.seeAll.text
.font(.title3)
}
}
}
.frame(width: 257, height: 380)
.buttonStyle(PlainButtonStyle())
}
.padding(.horizontal, 50)
.padding(.vertical)
}
.edgesIgnoringSafeArea(.horizontal)
}
.focusSection()
}
}

View File

@ -20,8 +20,13 @@ struct NextUpCard: View {
Button {
homeRouter.route(to: \.modalItem, item)
} label: {
ImageView(src: item.getBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
if item.itemType == .episode {
ImageView(src: item.getSeriesBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
} else {
ImageView(src: item.getBackdropImage(maxWidth: 500))
.frame(width: 500, height: 281.25)
}
}
.buttonStyle(CardButtonStyle())
.padding(.top)

View File

@ -24,7 +24,7 @@ class VLCPlayerViewController: UIViewController {
// MARK: variables
private var viewModel: VideoPlayerViewModel
private var vlcMediaPlayer = VLCMediaPlayer()
private var vlcMediaPlayer: VLCMediaPlayer
private var lastPlayerTicks: Int64 = 0
private var lastProgressReportTicks: Int64 = 0
private var viewModelListeners = Set<AnyCancellable>()
@ -59,6 +59,7 @@ class VLCPlayerViewController: UIViewController {
init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel
self.vlcMediaPlayer = VLCMediaPlayer()
super.init(nibName: nil, bundle: nil)
@ -118,14 +119,6 @@ class VLCPlayerViewController: UIViewController {
view.backgroundColor = .black
// Outside of 'setupMediaPlayer' such that they
// aren't unnecessarily set more than once
vlcMediaPlayer.delegate = self
vlcMediaPlayer.drawable = videoContentView
// TODO: custom font sizes
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16)
setupMediaPlayer(newViewModel: viewModel)
setupPanGestureRecognizer()
@ -211,20 +204,20 @@ class VLCPlayerViewController: UIViewController {
hideConfirmCloseOverlay()
if Defaults[.downActionShowsMenu] {
if !displayingContentOverlay {
if !displayingContentOverlay && !displayingOverlay {
didSelectMenu()
}
}
case .leftArrow:
hideConfirmCloseOverlay()
if !displayingContentOverlay {
if !displayingContentOverlay && !displayingOverlay {
didSelectBackward()
}
case .rightArrow:
hideConfirmCloseOverlay()
if !displayingContentOverlay {
if !displayingContentOverlay && !displayingOverlay {
didSelectForward()
}
case .pageUp: ()
@ -246,9 +239,6 @@ class VLCPlayerViewController: UIViewController {
hideOverlay()
} else if displayingContentOverlay {
hideOverlayContent()
showOverlay()
restartOverlayDismissTimer()
} else if viewModel.confirmClose && !displayingConfirmClose {
showConfirmCloseOverlay()
@ -387,6 +377,28 @@ extension VLCPlayerViewController {
/// Use case for this is setting new media within the same VLCPlayerViewController
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
// remove old player
if vlcMediaPlayer.media != nil {
viewModelListeners.forEach({ $0.cancel() })
vlcMediaPlayer.stop()
viewModel.sendStopReport()
viewModel.playerOverlayDelegate = nil
}
vlcMediaPlayer = VLCMediaPlayer()
// setup with new player and view model
vlcMediaPlayer = VLCMediaPlayer()
vlcMediaPlayer.delegate = self
vlcMediaPlayer.drawable = videoContentView
// TODO: Custom subtitle sizes
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16)
stopOverlayDismissTimer()
// Stop current media if there is one
@ -436,6 +448,13 @@ extension VLCPlayerViewController {
func startPlayback() {
vlcMediaPlayer.play()
// Setup external subtitles
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) {
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
}
}
setMediaPlayerTimeAtCurrentSlider()
viewModel.sendPlayReport()
@ -672,7 +691,8 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
}
// If needing to fix subtitle streams during playback
if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled {
if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex &&
viewModel.subtitlesEnabled {
didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex)
}

View File

@ -47,7 +47,7 @@ struct tvOSVLCOverlay: View {
if let subtitle = viewModel.subtitle {
Text(subtitle)
.font(.subheadline)
.foregroundColor(.lightGray)
.foregroundColor(.white)
}
Text(viewModel.title)

View File

@ -527,12 +527,14 @@ public final class TvOSSlider: UIControl {
@objc
private func leftTapWasTriggered() {
setValue(value-stepValue, animated: true)
// setValue(value-stepValue, animated: true)
viewModel.playerOverlayDelegate?.didSelectBackward()
}
@objc
private func rightTapWasTriggered() {
setValue(value+stepValue, animated: true)
// setValue(value+stepValue, animated: true)
viewModel.playerOverlayDelegate?.didSelectForward()
}
public override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {

View File

@ -270,10 +270,9 @@
E11D224227378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; };
E11D224327378428003F9CB3 /* ServerDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */; };
E12186DE2718F1C50010884C /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = E12186DD2718F1C50010884C /* Defaults */; };
E1218C9A271A26BA00EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C99271A26BA00EA0737 /* Nuke */; };
E1218C9C271A26C400EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9B271A26C400EA0737 /* Nuke */; };
E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9D271A2CD600EA0737 /* CombineExt */; };
E1218CA0271A2CF200EA0737 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1218C9F271A2CF200EA0737 /* Nuke */; };
E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */; };
E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */; };
E1267D3E271A1F46003C492E /* PreferenceUIHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */; };
E131691726C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
E131691826C583BC0074BFEE /* LogConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E131691626C583BC0074BFEE /* LogConstructor.swift */; };
@ -372,6 +371,8 @@
E1AD105C26D9ABDD003E4A08 /* PillHStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105B26D9ABDD003E4A08 /* PillHStackView.swift */; };
E1AD105F26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */; };
E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1AD106126D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift */; };
E1AE8E7C2789135A00FBDDAA /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1AE8E7B2789135A00FBDDAA /* Nuke */; };
E1AE8E7E2789136D00FBDDAA /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1AE8E7D2789136D00FBDDAA /* Nuke */; };
E1B59FD52786ADE500A5287E /* ContinueWatchingCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD42786ADE500A5287E /* ContinueWatchingCard.swift */; };
E1B59FD92786AE4600A5287E /* NextUpCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B59FD82786AE4600A5287E /* NextUpCard.swift */; };
E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = E1B6DCE7271A23780015B715 /* CombineExt */; };
@ -403,9 +404,12 @@
E1D4BF8C2719F39F00A11E64 /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF802719D22800A11E64 /* AppAppearance.swift */; };
E1D4BF8D2719F3A300A11E64 /* VideoPlayerJumpLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F0204D26CCCA74001C1C3B /* VideoPlayerJumpLength.swift */; };
E1D4BF8F271A079A00A11E64 /* BasicAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1D4BF8E271A079A00A11E64 /* BasicAppSettingsView.swift */; };
E1D7E5A827892566009D0EF7 /* Nuke in Frameworks */ = {isa = PBXBuildFile; productRef = E1D7E5A727892566009D0EF7 /* Nuke */; };
E1E00A35278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; };
E1E00A36278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; };
E1E00A37278628A40022235B /* DoubleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E00A34278628A40022235B /* DoubleExtensions.swift */; };
E1E0F4D8278911680084F701 /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = E1E0F4D7278911680084F701 /* CachedAsyncImage */; };
E1E0F4DA278911A30084F701 /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = E1E0F4D9278911A30084F701 /* CachedAsyncImage */; };
E1E48CC9271E6D410021A2F9 /* RefreshHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E48CC8271E6D410021A2F9 /* RefreshHelper.swift */; };
E1E5D5372783A52C00692DFE /* CinematicEpisodeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5362783A52C00692DFE /* CinematicEpisodeItemView.swift */; };
E1E5D5392783A56B00692DFE /* EpisodesRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E5D5382783A56B00692DFE /* EpisodesRowView.swift */; };
@ -645,6 +649,7 @@
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseItemDto+VideoPlayerViewModel.swift"; sourceTree = "<group>"; };
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinAPIError.swift; sourceTree = "<group>"; };
E11D224127378428003F9CB3 /* ServerDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDetailCoordinator.swift; sourceTree = "<group>"; };
E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaStreamExtension.swift; sourceTree = "<group>"; };
E1267D3D271A1F46003C492E /* PreferenceUIHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferenceUIHostingController.swift; sourceTree = "<group>"; };
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = "<group>"; };
E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
@ -748,13 +753,14 @@
files = (
53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */,
E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */,
E1218CA0271A2CF200EA0737 /* Nuke in Frameworks */,
6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */,
535870912669D7A800D05A09 /* Introspect in Frameworks */,
536D3D84267BEA550004248C /* ParallaxView in Frameworks */,
53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */,
E1A9999B271A343C008E78C0 /* SwiftUICollection in Frameworks */,
E13DD3CD27164CA7009D4DAF /* CoreStore in Frameworks */,
E1E0F4DA278911A30084F701 /* CachedAsyncImage in Frameworks */,
E1AE8E7E2789136D00FBDDAA /* Nuke in Frameworks */,
E178857D278037FD0094FBCF /* JellyfinAPI in Frameworks */,
E12186DE2718F1C50010884C /* Defaults in Frameworks */,
53ABFDED26799D7700886593 /* ActivityIndicator in Frameworks */,
@ -771,10 +777,11 @@
E10EAA4D277BB716000269ED /* Sliders in Frameworks */,
62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */,
E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */,
E1218C9A271A26BA00EA0737 /* Nuke in Frameworks */,
E1B6DCEA271A23880015B715 /* SwiftyJSON in Frameworks */,
53352571265EA0A0006CCA86 /* Introspect in Frameworks */,
E13DD3C62716499E009D4DAF /* CoreStore in Frameworks */,
E1E0F4D8278911680084F701 /* CachedAsyncImage in Frameworks */,
E1AE8E7C2789135A00FBDDAA /* Nuke in Frameworks */,
625CB57A2678C4A400530A6E /* ActivityIndicator in Frameworks */,
E1B6DCE8271A23780015B715 /* CombineExt in Frameworks */,
E10EAA45277BB646000269ED /* JellyfinAPI in Frameworks */,
@ -786,13 +793,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E1D7E5A827892566009D0EF7 /* Nuke in Frameworks */,
628B95242670CABD0091AF3B /* SwiftUI.framework in Frameworks */,
531ABF6C2671F5CC00C0FE20 /* WidgetKit.framework in Frameworks */,
E13DD3DD27175CE3009D4DAF /* Defaults in Frameworks */,
53649AB5269D423A00A2D8B7 /* Puppy in Frameworks */,
536D3D7D267BD5F90004248C /* ActivityIndicator in Frameworks */,
E13DD3CF27164E1F009D4DAF /* CoreStore in Frameworks */,
E1218C9C271A26C400EA0737 /* Nuke in Frameworks */,
E10EAA47277BB670000269ED /* JellyfinAPI in Frameworks */,
3B8BA25B211CA261017ABA16 /* Pods_Swiftfin_Widget.framework in Frameworks */,
);
@ -1502,10 +1509,11 @@
isa = PBXGroup;
children = (
E18845F426DD631E00B0C5B7 /* BaseItemDto+Stackable.swift */,
E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */,
E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */,
E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */,
5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */,
E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */,
E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */,
E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */,
);
path = JellyfinAPIExtensions;
@ -1634,9 +1642,10 @@
E13DD3CC27164CA7009D4DAF /* CoreStore */,
E12186DD2718F1C50010884C /* Defaults */,
E1218C9D271A2CD600EA0737 /* CombineExt */,
E1218C9F271A2CF200EA0737 /* Nuke */,
E1A9999A271A343C008E78C0 /* SwiftUICollection */,
E178857C278037FD0094FBCF /* JellyfinAPI */,
E1E0F4D9278911A30084F701 /* CachedAsyncImage */,
E1AE8E7D2789136D00FBDDAA /* Nuke */,
);
productName = "JellyfinPlayer tvOS";
productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */;
@ -1671,10 +1680,11 @@
E13DD3D227168E65009D4DAF /* Defaults */,
E1B6DCE7271A23780015B715 /* CombineExt */,
E1B6DCE9271A23880015B715 /* SwiftyJSON */,
E1218C99271A26BA00EA0737 /* Nuke */,
E1A99998271A3429008E78C0 /* SwiftUICollection */,
E10EAA44277BB646000269ED /* JellyfinAPI */,
E10EAA4C277BB716000269ED /* Sliders */,
E1E0F4D7278911680084F701 /* CachedAsyncImage */,
E1AE8E7B2789135A00FBDDAA /* Nuke */,
);
productName = JellyfinPlayer;
productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */;
@ -1699,8 +1709,8 @@
53649AB4269D423A00A2D8B7 /* Puppy */,
E13DD3CE27164E1F009D4DAF /* CoreStore */,
E13DD3DC27175CE3009D4DAF /* Defaults */,
E1218C9B271A26C400EA0737 /* Nuke */,
E10EAA46277BB670000269ED /* JellyfinAPI */,
E1D7E5A727892566009D0EF7 /* Nuke */,
);
productName = WidgetExtensionExtension;
productReference = 628B95202670CABD0091AF3B /* Swiftfin Widget.appex */;
@ -1759,14 +1769,15 @@
536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */,
53649AAB269CFAEA00A2D8B7 /* XCRemoteSwiftPackageReference "Puppy" */,
62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */,
E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore.git" */,
E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */,
E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */,
E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */,
E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */,
C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */,
E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */,
E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */,
E1E0F4D6278911680084F701 /* XCRemoteSwiftPackageReference "SwiftUI-CachedAsyncImage" */,
E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */,
);
productRefGroup = 5377CBF2263B596A003A4E83 /* Products */;
projectDirPath = "";
@ -2051,6 +2062,7 @@
E1C812CC277AE40A00918266 /* VideoPlayerView.swift in Sources */,
53CD2A40268A49C2002ABD4E /* ItemView.swift in Sources */,
53CD2A42268A4B38002ABD4E /* MovieItemView.swift in Sources */,
E122A9142788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
E178859E2780F53B0094FBCF /* SliderView.swift in Sources */,
536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */,
E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */,
@ -2191,6 +2203,7 @@
E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */,
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */,
62E632EC267D410B0063E547 /* SeriesItemViewModel.swift in Sources */,
625CB5732678C32A00530A6E /* HomeViewModel.swift in Sources */,
@ -2920,14 +2933,6 @@
kind = branch;
};
};
E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kean/Nuke";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 9.0.0;
};
};
E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/CombineCommunity/CombineExt";
@ -2936,7 +2941,7 @@
minimumVersion = 1.0.0;
};
};
E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore.git" */ = {
E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/JohnEstropia/CoreStore.git";
requirement = {
@ -2952,6 +2957,14 @@
minimumVersion = 6.0.0;
};
};
E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kean/Nuke";
requirement = {
kind = upToNextMinorVersion;
minimumVersion = 9.6.0;
};
};
E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON";
@ -2960,6 +2973,14 @@
minimumVersion = 5.0.0;
};
};
E1E0F4D6278911680084F701 /* XCRemoteSwiftPackageReference "SwiftUI-CachedAsyncImage" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/lorenzofiamingo/SwiftUI-CachedAsyncImage";
requirement = {
branch = main;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@ -3038,39 +3059,24 @@
package = E13DD3D127168E65009D4DAF /* XCRemoteSwiftPackageReference "Defaults" */;
productName = Defaults;
};
E1218C99271A26BA00EA0737 /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E1218C9B271A26C400EA0737 /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E1218C9D271A2CD600EA0737 /* CombineExt */ = {
isa = XCSwiftPackageProductDependency;
package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */;
productName = CombineExt;
};
E1218C9F271A2CF200EA0737 /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1218C98271A26BA00EA0737 /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E13DD3C52716499E009D4DAF /* CoreStore */ = {
isa = XCSwiftPackageProductDependency;
package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore.git" */;
package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */;
productName = CoreStore;
};
E13DD3CC27164CA7009D4DAF /* CoreStore */ = {
isa = XCSwiftPackageProductDependency;
package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore.git" */;
package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */;
productName = CoreStore;
};
E13DD3CE27164E1F009D4DAF /* CoreStore */ = {
isa = XCSwiftPackageProductDependency;
package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore.git" */;
package = E13DD3C42716499E009D4DAF /* XCRemoteSwiftPackageReference "CoreStore" */;
productName = CoreStore;
};
E13DD3D227168E65009D4DAF /* Defaults */ = {
@ -3098,6 +3104,16 @@
package = C4BFD4E327167B63007739E3 /* XCRemoteSwiftPackageReference "SwiftUICollection" */;
productName = SwiftUICollection;
};
E1AE8E7B2789135A00FBDDAA /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E1AE8E7D2789136D00FBDDAA /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E1B6DCE7271A23780015B715 /* CombineExt */ = {
isa = XCSwiftPackageProductDependency;
package = E1267D42271A212C003C492E /* XCRemoteSwiftPackageReference "CombineExt" */;
@ -3108,6 +3124,21 @@
package = E1C16B89271A2180009A5D25 /* XCRemoteSwiftPackageReference "SwiftyJSON" */;
productName = SwiftyJSON;
};
E1D7E5A727892566009D0EF7 /* Nuke */ = {
isa = XCSwiftPackageProductDependency;
package = E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */;
productName = Nuke;
};
E1E0F4D7278911680084F701 /* CachedAsyncImage */ = {
isa = XCSwiftPackageProductDependency;
package = E1E0F4D6278911680084F701 /* XCRemoteSwiftPackageReference "SwiftUI-CachedAsyncImage" */;
productName = CachedAsyncImage;
};
E1E0F4D9278911A30084F701 /* CachedAsyncImage */ = {
isa = XCSwiftPackageProductDependency;
package = E1E0F4D6278911680084F701 /* XCRemoteSwiftPackageReference "SwiftUI-CachedAsyncImage" */;
productName = CachedAsyncImage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 5377CBE9263B596A003A4E83 /* Project object */;

View File

@ -100,6 +100,15 @@
"version": "1.4.2"
}
},
{
"package": "CachedAsyncImage",
"repositoryURL": "https://github.com/lorenzofiamingo/SwiftUI-CachedAsyncImage",
"state": {
"branch": "main",
"revision": "eb489a699be1f6e6c1a19fecdd6bfdc556474fd6",
"version": null
}
},
{
"package": "Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect",

View File

@ -63,19 +63,20 @@ struct SettingsView: View {
}
}
Section(header: Text("Networking")) {
Picker("Default local quality", selection: $inNetworkStreamBitrate) {
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
Text(bitrate.name).tag(bitrate.value)
}
}
Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
Text(bitrate.name).tag(bitrate.value)
}
}
}
// TODO: Implement these for playback
// Section(header: Text("Networking")) {
// Picker("Default local quality", selection: $inNetworkStreamBitrate) {
// ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
// Text(bitrate.name).tag(bitrate.value)
// }
// }
//
// Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) {
// ForEach(self.viewModel.bitrates, id: \.self) { bitrate in
// Text(bitrate.name).tag(bitrate.value)
// }
// }
// }
Section(header: Text("Video Player")) {
Picker("Jump Forward Length", selection: $jumpForwardLength) {

View File

@ -24,7 +24,7 @@ class VLCPlayerViewController: UIViewController {
// MARK: variables
private var viewModel: VideoPlayerViewModel
private var vlcMediaPlayer = VLCMediaPlayer()
private var vlcMediaPlayer: VLCMediaPlayer
private var lastPlayerTicks: Int64 = 0
private var lastProgressReportTicks: Int64 = 0
private var viewModelListeners = Set<AnyCancellable>()
@ -49,6 +49,7 @@ class VLCPlayerViewController: UIViewController {
init(viewModel: VideoPlayerViewModel) {
self.viewModel = viewModel
self.vlcMediaPlayer = VLCMediaPlayer()
super.init(nibName: nil, bundle: nil)
@ -97,14 +98,6 @@ class VLCPlayerViewController: UIViewController {
view.backgroundColor = .black
// These are kept outside of 'setupMediaPlayer' such that
// they aren't unnecessarily set more than once
vlcMediaPlayer.delegate = self
vlcMediaPlayer.drawable = videoContentView
// TODO: Custom subtitle sizes
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
setupMediaPlayer(newViewModel: viewModel)
refreshJumpBackwardOverlayView(with: viewModel.jumpBackwardLength)
@ -287,9 +280,8 @@ extension VLCPlayerViewController {
/// Use case for this is setting new media within the same VLCPlayerViewController
func setupMediaPlayer(newViewModel: VideoPlayerViewModel) {
stopOverlayDismissTimer()
// remove old player
// Stop current media if there is one
if vlcMediaPlayer.media != nil {
viewModelListeners.forEach({ $0.cancel() })
@ -298,6 +290,20 @@ extension VLCPlayerViewController {
viewModel.playerOverlayDelegate = nil
}
vlcMediaPlayer = VLCMediaPlayer()
// setup with new player and view model
vlcMediaPlayer = VLCMediaPlayer()
vlcMediaPlayer.delegate = self
vlcMediaPlayer.drawable = videoContentView
// TODO: Custom subtitle sizes
vlcMediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 14)
stopOverlayDismissTimer()
lastPlayerTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
lastProgressReportTicks = newViewModel.item.userData?.playbackPositionTicks ?? 0
@ -334,6 +340,13 @@ extension VLCPlayerViewController {
func startPlayback() {
vlcMediaPlayer.play()
// Setup external subtitles
for externalSubtitle in viewModel.subtitleStreams.filter({ $0.deliveryMethod == .external }) {
if let deliveryURL = externalSubtitle.externalURL(base: SessionManager.main.currentLogin.server.currentURI) {
vlcMediaPlayer.addPlaybackSlave(deliveryURL, type: .subtitle, enforce: false)
}
}
setMediaPlayerTimeAtCurrentSlider()
viewModel.sendPlayReport()
@ -526,7 +539,8 @@ extension VLCPlayerViewController: VLCMediaPlayerDelegate {
}
// If needing to fix subtitle streams during playback
if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex && viewModel.subtitlesEnabled {
if vlcMediaPlayer.currentVideoSubTitleIndex != viewModel.selectedSubtitleStreamIndex &&
viewModel.subtitlesEnabled {
didSelectSubtitleStream(index: viewModel.selectedSubtitleStreamIndex)
}