From 961f87d3c72dec1f5c45762c0d04194a6fa85639 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 19 Jan 2022 16:11:06 -0700 Subject: [PATCH 1/6] add back experimental native player --- .../iOSVideoPlayerCoordinator.swift | 21 ++- .../tvOSVideoPlayerCoordinator.swift | 12 +- .../BaseItemDto+VideoPlayerViewModel.swift | 55 ++++++-- .../SwiftfinStore/SwiftfinStoreDefaults.swift | 1 + .../VideoPlayerViewModel.swift | 10 ++ .../ExperimentalSettingsView.swift | 4 + .../NativePlayerViewController.swift | 124 ++++++++++++++++++ .../VideoPlayer/Overlays/tvOSVLCOverlay.swift | 1 + .../Views/VideoPlayer/VideoPlayerView.swift | 14 ++ Swiftfin.xcodeproj/project.pbxproj | 12 +- .../ExperimentalSettingsView.swift | 4 + .../NativePlayerViewController.swift | 108 +++++++++++++++ .../Overlays/VLCPlayerOverlayView.swift | 1 + .../Views/VideoPlayer/VLCPlayerView.swift | 14 ++ 14 files changed, 354 insertions(+), 27 deletions(-) create mode 100644 Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift create mode 100644 Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift diff --git a/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift index e193b581..94efc82b 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator/iOSVideoPlayerCoordinator.swift @@ -28,12 +28,21 @@ final class VideoPlayerCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { PreferenceUIHostingControllerView { - VLCPlayerView(viewModel: self.viewModel) - .navigationBarHidden(true) - .statusBar(hidden: true) - .ignoresSafeArea() - .prefersHomeIndicatorAutoHidden(true) - .supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape) + if Defaults[.Experimental.nativePlayer] { + NativePlayerView(viewModel: self.viewModel) + .navigationBarHidden(true) + .statusBar(hidden: true) + .ignoresSafeArea() + .prefersHomeIndicatorAutoHidden(true) + .supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape) + } else { + VLCPlayerView(viewModel: self.viewModel) + .navigationBarHidden(true) + .statusBar(hidden: true) + .ignoresSafeArea() + .prefersHomeIndicatorAutoHidden(true) + .supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape) + } }.ignoresSafeArea() } } diff --git a/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift b/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift index e01e279f..97136ed7 100644 --- a/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift +++ b/Shared/Coordinators/VideoPlayerCoordinator/tvOSVideoPlayerCoordinator.swift @@ -27,8 +27,14 @@ final class VideoPlayerCoordinator: NavigationCoordinatable { @ViewBuilder func makeStart() -> some View { - VLCPlayerView(viewModel: viewModel) - .navigationBarHidden(true) - .ignoresSafeArea() + if Defaults[.Experimental.nativePlayer] { + NativePlayerView(viewModel: viewModel) + .navigationBarHidden(true) + .ignoresSafeArea() + } else { + VLCPlayerView(viewModel: viewModel) + .navigationBarHidden(true) + .ignoresSafeArea() + } } } diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift index 8b743846..c658e9e1 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift @@ -40,6 +40,7 @@ extension BaseItemDto { var viewModels: [VideoPlayerViewModel] = [] for currentMediaSource in mediaSources { + let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? [] let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? [] @@ -48,11 +49,28 @@ extension BaseItemDto { let defaultSubtitleStream = subtitleStreams .first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 }) - var directStreamURL: URL + // MARK: Build Streams + + let directStreamURL: URL let transcodedStreamURL: URLComponents? + var hlsStreamURL: URL let mediaSourceID: String let streamType: ServerStreamType + if mediaSources.count > 1 { + mediaSourceID = currentMediaSource.id! + } else { + mediaSourceID = self.id! + } + + let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(itemId: self.id!, + _static: true, + tag: self.etag, + playSessionId: response.playSessionId, + minSegments: 6, + mediaSourceId: mediaSourceID) + directStreamURL = URL(string: directStreamBuilder.URLString)! + if let transcodeURL = currentMediaSource.transcodingUrl { streamType = .transcode transcodedStreamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI @@ -62,18 +80,30 @@ extension BaseItemDto { transcodedStreamURL = nil } - if mediaSources.count > 1 { - mediaSourceID = currentMediaSource.id! - } else { - mediaSourceID = self.id! - } + let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(itemId: id ?? "", + mediaSourceId: id ?? "", + _static: true, + tag: currentMediaSource.eTag, + deviceProfileId: nil, + playSessionId: response.playSessionId, + segmentContainer: "ts", + segmentLength: nil, + minSegments: 2, + deviceId: UIDevice.vendorUUIDString, + audioCodec: audioStreams + .compactMap(\.codec) + .joined(separator: ","), + breakOnNonKeyFrames: true, + requireAvc: true, + transcodingMaxAudioChannels: 6, + videoCodec: videoStream?.codec, + videoStreamIndex: videoStream?.index, + enableAdaptiveBitrateStreaming: true) - let requestBuilder = VideosAPI.getVideoStreamWithRequestBuilder(itemId: self.id!, - _static: true, - tag: self.etag, - minSegments: 6, - mediaSourceId: mediaSourceID) - directStreamURL = URL(string: requestBuilder.URLString)! + var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)! + hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken) + + hlsStreamURL = hlsStreamComponents.url! // MARK: VidoPlayerViewModel Creation @@ -111,6 +141,7 @@ extension BaseItemDto { subtitle: subtitle, directStreamURL: directStreamURL, transcodedStreamURL: transcodedStreamURL?.url, + hlsStreamURL: hlsStreamURL, streamType: streamType, response: response, audioStreams: audioStreams, diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index 35bfa228..3cf1480c 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -72,6 +72,7 @@ extension Defaults.Keys { suite: SwiftfinStore.Defaults.generalSuite) static let forceDirectPlay = Key("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite) static let liveTVAlphaEnabled = Key("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let nativePlayer = Key("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite) } // tvos specific diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index 1556f745..6ce29063 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -109,6 +109,7 @@ final class VideoPlayerViewModel: ViewModel { let subtitle: String? let directStreamURL: URL let transcodedStreamURL: URL? + let hlsStreamURL: URL let audioStreams: [MediaStream] let subtitleStreams: [MediaStream] let chapters: [ChapterInfo] @@ -147,6 +148,13 @@ final class VideoPlayerViewModel: ViewModel { Int64(currentSeconds) * 10_000_000 } + func setSeconds(_ seconds: Int64) { + let videoDuration = item.runTimeTicks! + let percentage = Double(seconds * 10_000_000) / Double(videoDuration) + + sliderPercentage = percentage + } + // MARK: Helpers var currentAudioStream: MediaStream? { @@ -188,6 +196,7 @@ final class VideoPlayerViewModel: ViewModel { subtitle: String?, directStreamURL: URL, transcodedStreamURL: URL?, + hlsStreamURL: URL, streamType: ServerStreamType, response: PlaybackInfoResponse, audioStreams: [MediaStream], @@ -210,6 +219,7 @@ final class VideoPlayerViewModel: ViewModel { self.subtitle = subtitle self.directStreamURL = directStreamURL self.transcodedStreamURL = transcodedStreamURL + self.hlsStreamURL = hlsStreamURL self.streamType = streamType self.response = response self.audioStreams = audioStreams diff --git a/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift b/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift index b53b52aa..ab5c1b39 100644 --- a/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift +++ b/Swiftfin tvOS/Views/SettingsView/ExperimentalSettingsView.swift @@ -17,6 +17,8 @@ struct ExperimentalSettingsView: View { var syncSubtitleStateWithAdjacent @Default(.Experimental.liveTVAlphaEnabled) var liveTVAlphaEnabled + @Default(.Experimental.nativePlayer) + var nativePlayer var body: some View { Form { @@ -28,6 +30,8 @@ struct ExperimentalSettingsView: View { Toggle("Live TV (Alpha)", isOn: $liveTVAlphaEnabled) + Toggle("Native Player", isOn: $nativePlayer) + } header: { L10n.experimental.text } diff --git a/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift b/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift new file mode 100644 index 00000000..424c0399 --- /dev/null +++ b/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift @@ -0,0 +1,124 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import AVKit +import Combine +import JellyfinAPI +import UIKit + +class NativePlayerViewController: AVPlayerViewController { + + let viewModel: VideoPlayerViewModel + + var timeObserverToken: Any? + + var lastProgressTicks: Int64 = 0 + + private var cancellables = Set() + + init(viewModel: VideoPlayerViewModel) { + + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + + let player = AVPlayer(url: viewModel.hlsStreamURL) + + player.appliesMediaSelectionCriteriaAutomatically = false + player.currentItem?.externalMetadata = createMetadata() + + let timeScale = CMTimeScale(NSEC_PER_SEC) + let time = CMTime(seconds: 5, preferredTimescale: timeScale) + + timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in + if time.seconds != 0 { + self?.sendProgressReport(seconds: time.seconds) + } + } + + self.player = player + + self.allowsPictureInPicturePlayback = true + self.player?.allowsExternalPlayback = true + } + + private func createMetadata() -> [AVMetadataItem] { + let allMetadata: [AVMetadataIdentifier: Any] = [ + .commonIdentifierTitle: viewModel.title, + .iTunesMetadataTrackSubTitle: viewModel.subtitle ?? "", + // Need to fix against an image that doesn't exist +// .commonIdentifierArtwork: UIImage(data: try! Data(contentsOf: viewModel.item.getBackdropImage(maxWidth: 200)))? +// .pngData() as Any, +// .commonIdentifierDescription: viewModel.item.overview ?? "", +// .iTunesMetadataContentRating: viewModel.item.officialRating ?? "", +// .quickTimeMetadataGenre: viewModel.item.genres?.first ?? "", + ] + + return allMetadata.compactMap { createMetadataItem(for: $0, value: $1) } + } + + private func createMetadataItem(for identifier: AVMetadataIdentifier, + value: Any) -> AVMetadataItem + { + let item = AVMutableMetadataItem() + item.identifier = identifier + item.value = value as? NSCopying & NSObjectProtocol + // Specify "und" to indicate an undefined language. + item.extendedLanguageTag = "und" + return item.copy() as! AVMetadataItem + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + stop() + removePeriodicTimeObserver() + } + + func removePeriodicTimeObserver() { + if let timeObserverToken = timeObserverToken { + player?.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + player?.seek(to: CMTimeMake(value: viewModel.currentSecondTicks, timescale: 10_000_000), + toleranceBefore: CMTimeMake(value: 1, timescale: 1), toleranceAfter: CMTimeMake(value: 1, timescale: 1), + completionHandler: { _ in + self.play() + }) + } + + private func play() { + player?.play() + + viewModel.sendPlayReport() + } + + private func sendProgressReport(seconds: Double) { + viewModel.setSeconds(Int64(seconds)) + viewModel.sendProgressReport() + } + + private func stop() { + self.player?.pause() + viewModel.sendStopReport() + } +} diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift index 12d83836..20db21e6 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift @@ -143,6 +143,7 @@ struct tvOSVLCOverlay_Previews: PreviewProvider { subtitle: "Loki - S1E1", directStreamURL: URL(string: "www.apple.com")!, transcodedStreamURL: nil, + hlsStreamURL: URL(string: "www.apple.com")!, streamType: .direct, response: PlaybackInfoResponse(), audioStreams: [MediaStream(displayTitle: "English", index: -1)], diff --git a/Swiftfin tvOS/Views/VideoPlayer/VideoPlayerView.swift b/Swiftfin tvOS/Views/VideoPlayer/VideoPlayerView.swift index a9684d8f..9a2908db 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/VideoPlayerView.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/VideoPlayerView.swift @@ -9,6 +9,20 @@ import SwiftUI import UIKit +struct NativePlayerView: UIViewControllerRepresentable { + + let viewModel: VideoPlayerViewModel + + typealias UIViewControllerType = NativePlayerViewController + + func makeUIViewController(context: Context) -> NativePlayerViewController { + + NativePlayerViewController(viewModel: viewModel) + } + + func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {} +} + struct VLCPlayerView: UIViewControllerRepresentable { let viewModel: VideoPlayerViewModel diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 9bbbe00c..b9226523 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2643,7 +2643,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"Swiftfin tvOS/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "Swiftfin tvOS/Info.plist"; @@ -2652,7 +2652,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -2673,7 +2673,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"Swiftfin tvOS/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "Swiftfin tvOS/Info.plist"; @@ -2682,7 +2682,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_EMIT_LOC_STRINGS = YES; @@ -2837,7 +2837,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -2874,7 +2874,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; diff --git a/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift b/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift index 271cb887..57c5ca7b 100644 --- a/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift +++ b/Swiftfin/Views/SettingsView/ExperimentalSettingsView.swift @@ -15,6 +15,8 @@ struct ExperimentalSettingsView: View { var forceDirectPlay @Default(.Experimental.syncSubtitleStateWithAdjacent) var syncSubtitleStateWithAdjacent + @Default(.Experimental.nativePlayer) + var nativePlayer var body: some View { Form { @@ -24,6 +26,8 @@ struct ExperimentalSettingsView: View { Toggle("Sync Subtitles with Adjacent Episodes", isOn: $syncSubtitleStateWithAdjacent) + Toggle("Native Player", isOn: $nativePlayer) + } header: { L10n.experimental.text } diff --git a/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift b/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift new file mode 100644 index 00000000..2c090fd3 --- /dev/null +++ b/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift @@ -0,0 +1,108 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import AVKit +import Combine +import JellyfinAPI +import UIKit + +class NativePlayerViewController: AVPlayerViewController { + + let viewModel: VideoPlayerViewModel + + var timeObserverToken: Any? + + var lastProgressTicks: Int64 = 0 + + private var cancellables = Set() + + init(viewModel: VideoPlayerViewModel) { + + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + + let player = AVPlayer(url: viewModel.hlsStreamURL) + + player.appliesMediaSelectionCriteriaAutomatically = false + + let timeScale = CMTimeScale(NSEC_PER_SEC) + let time = CMTime(seconds: 5, preferredTimescale: timeScale) + + timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in + if time.seconds != 0 { + self?.sendProgressReport(seconds: time.seconds) + } + } + + self.player = player + + self.allowsPictureInPicturePlayback = true + self.player?.allowsExternalPlayback = true + } + + private func createMetadataItem(for identifier: AVMetadataIdentifier, + value: Any) -> AVMetadataItem + { + let item = AVMutableMetadataItem() + item.identifier = identifier + item.value = value as? NSCopying & NSObjectProtocol + // Specify "und" to indicate an undefined language. + item.extendedLanguageTag = "und" + return item.copy() as! AVMetadataItem + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + stop() + removePeriodicTimeObserver() + } + + func removePeriodicTimeObserver() { + if let timeObserverToken = timeObserverToken { + player?.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + player?.seek(to: CMTimeMake(value: viewModel.currentSecondTicks, timescale: 10_000_000), + toleranceBefore: CMTimeMake(value: 1, timescale: 1), toleranceAfter: CMTimeMake(value: 1, timescale: 1), + completionHandler: { _ in + self.play() + }) + } + + private func play() { + player?.play() + + viewModel.sendPlayReport() + } + + private func sendProgressReport(seconds: Double) { + viewModel.setSeconds(Int64(seconds)) + viewModel.sendProgressReport() + } + + private func stop() { + self.player?.pause() + viewModel.sendStopReport() + } +} diff --git a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift index 08133616..c85777b6 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -437,6 +437,7 @@ struct VLCPlayerCompactOverlayView_Previews: PreviewProvider { subtitle: "Loki - S1E1", directStreamURL: URL(string: "www.apple.com")!, transcodedStreamURL: nil, + hlsStreamURL: URL(string: "www.apple.com")!, streamType: .direct, response: PlaybackInfoResponse(), audioStreams: [MediaStream(displayTitle: "English", index: -1)], diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerView.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerView.swift index a9684d8f..9a2908db 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerView.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerView.swift @@ -9,6 +9,20 @@ import SwiftUI import UIKit +struct NativePlayerView: UIViewControllerRepresentable { + + let viewModel: VideoPlayerViewModel + + typealias UIViewControllerType = NativePlayerViewController + + func makeUIViewController(context: Context) -> NativePlayerViewController { + + NativePlayerViewController(viewModel: viewModel) + } + + func updateUIViewController(_ uiViewController: NativePlayerViewController, context: Context) {} +} + struct VLCPlayerView: UIViewControllerRepresentable { let viewModel: VideoPlayerViewModel From 5969a42326acbb656f7d466c21b88e96afc3cb32 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 19 Jan 2022 16:11:19 -0700 Subject: [PATCH 2/6] Update project.pbxproj --- Swiftfin.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index b9226523..3c7bd429 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2902,7 +2902,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -2929,7 +2929,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; From da75ae3f9b1400c53ee01b4a7c0dee1d2a44ae3b Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 19 Jan 2022 16:11:27 -0700 Subject: [PATCH 3/6] Update project.pbxproj --- Swiftfin.xcodeproj/project.pbxproj | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 3c7bd429..942df9cc 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -290,6 +290,8 @@ E1361DA7278FA7A300BEC523 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E1361DA6278FA7A300BEC523 /* NukeUI */; }; E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; }; E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.swift */; }; + E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */; }; + E13AD7302798C60F00FDCEE8 /* NativePlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */; }; E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */; }; E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; E13DD3C327164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; @@ -681,6 +683,8 @@ E126F740278A656C00A522BF /* ServerStreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerStreamType.swift; sourceTree = ""; }; E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.swift; sourceTree = ""; }; E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = ""; }; + E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = ""; }; + E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = ""; }; E13D02842788B634000FCB04 /* Swiftfin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swiftfin.entitlements; sourceTree = ""; }; E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; E13DD3C127164941009D4DAF /* SwiftfinStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = ""; }; @@ -861,6 +865,7 @@ E17885A7278130690094FBCF /* Overlays */, E1C812C8277AE40900918266 /* VideoPlayerView.swift */, E1384943278036C70024FB48 /* VLCPlayerViewController.swift */, + E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */, ); path = VideoPlayer; sourceTree = ""; @@ -1565,8 +1570,9 @@ E193D5452719418B00900D82 /* VideoPlayer */ = { isa = PBXGroup; children = ( - E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */, + E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */, E1002B692793E12E00E47059 /* Overlays */, + E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */, E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */, E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */, ); @@ -2249,6 +2255,7 @@ C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */, E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */, C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */, + E13AD7302798C60F00FDCEE8 /* NativePlayerViewController.swift in Sources */, E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */, 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, @@ -2339,6 +2346,7 @@ 5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, + E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */, E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */, E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */, From 179a92cbde92cec0e959b7e0337ca8a4706ec9e7 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 19 Jan 2022 16:11:49 -0700 Subject: [PATCH 4/6] Update project.pbxproj --- Swiftfin.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 942df9cc..c061ace2 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2651,7 +2651,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"Swiftfin tvOS/Preview Content\""; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "Swiftfin tvOS/Info.plist"; @@ -2660,7 +2660,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -2681,7 +2681,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = "\"Swiftfin tvOS/Preview Content\""; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "Swiftfin tvOS/Info.plist"; @@ -2690,7 +2690,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = pips.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_EMIT_LOC_STRINGS = YES; @@ -2845,7 +2845,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -2882,7 +2882,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -2910,7 +2910,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; @@ -2937,7 +2937,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.swiftfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = org.jellyfin.swiftfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; From d483e362a1b2fc66d5d7a99ef794ee4e85238266 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Wed, 19 Jan 2022 16:14:44 -0700 Subject: [PATCH 5/6] add transcoded url check --- .../Views/VideoPlayer/NativePlayerViewController.swift | 8 +++++++- .../Views/VideoPlayer/NativePlayerViewController.swift | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift b/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift index 424c0399..d0ffba8c 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/NativePlayerViewController.swift @@ -27,7 +27,13 @@ class NativePlayerViewController: AVPlayerViewController { super.init(nibName: nil, bundle: nil) - let player = AVPlayer(url: viewModel.hlsStreamURL) + let player: AVPlayer + + if let transcodedStreamURL = viewModel.transcodedStreamURL { + player = AVPlayer(url: transcodedStreamURL) + } else { + player = AVPlayer(url: viewModel.hlsStreamURL) + } player.appliesMediaSelectionCriteriaAutomatically = false player.currentItem?.externalMetadata = createMetadata() diff --git a/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift b/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift index 2c090fd3..07e31d5c 100644 --- a/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/NativePlayerViewController.swift @@ -27,7 +27,13 @@ class NativePlayerViewController: AVPlayerViewController { super.init(nibName: nil, bundle: nil) - let player = AVPlayer(url: viewModel.hlsStreamURL) + let player: AVPlayer + + if let transcodedStreamURL = viewModel.transcodedStreamURL { + player = AVPlayer(url: transcodedStreamURL) + } else { + player = AVPlayer(url: viewModel.hlsStreamURL) + } player.appliesMediaSelectionCriteriaAutomatically = false From ae959dde6cce7b33299e40cca8b2abf0c0b937ac Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Thu, 20 Jan 2022 10:59:30 -0700 Subject: [PATCH 6/6] fix top compact overlay gradient --- .../VideoPlayer/Overlays/VLCPlayerOverlayView.swift | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift index c85777b6..9bb57530 100644 --- a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -14,7 +14,6 @@ import Sliders import SwiftUI struct VLCPlayerOverlayView: View { - @ObservedObject var viewModel: VideoPlayerViewModel @@ -49,11 +48,9 @@ struct VLCPlayerOverlayView: View { @ViewBuilder private var mainBody: some View { VStack { - // MARK: Top Bar - ZStack(alignment: .center) { - + ZStack(alignment: .top) { if viewModel.overlayType == .compact { LinearGradient(gradient: Gradient(colors: [.black.opacity(0.8), .clear]), startPoint: .top, @@ -63,9 +60,7 @@ struct VLCPlayerOverlayView: View { } VStack(alignment: .EpisodeSeriesAlignmentGuide) { - HStack(alignment: .center) { - HStack { Button { viewModel.playerOverlayDelegate?.didSelectClose() @@ -87,7 +82,6 @@ struct VLCPlayerOverlayView: View { Spacer() HStack(spacing: 20) { - // MARK: Previous Item if viewModel.shouldShowPlayPreviousItem { @@ -165,7 +159,6 @@ struct VLCPlayerOverlayView: View { // MARK: Settings Menu Menu { - // MARK: Audio Streams Menu { @@ -337,7 +330,6 @@ struct VLCPlayerOverlayView: View { // MARK: Bottom Bar ZStack(alignment: .center) { - if viewModel.overlayType == .compact { LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8)]), startPoint: .top, @@ -347,7 +339,6 @@ struct VLCPlayerOverlayView: View { } HStack { - if viewModel.overlayType == .compact { HStack { Button { @@ -431,7 +422,6 @@ struct VLCPlayerOverlayView: View { } struct VLCPlayerCompactOverlayView_Previews: PreviewProvider { - static let videoPlayerViewModel = VideoPlayerViewModel(item: BaseItemDto(), title: "Glorious Purpose", subtitle: "Loki - S1E1", @@ -469,7 +459,6 @@ struct VLCPlayerCompactOverlayView_Previews: PreviewProvider { // MARK: TitleSubtitleAlignment extension HorizontalAlignment { - private struct TitleSubtitleAlignment: AlignmentID { static func defaultValue(in context: ViewDimensions) -> CGFloat { context[HorizontalAlignment.leading]