add back experimental native player

This commit is contained in:
Ethan Pippin 2022-01-19 16:11:06 -07:00
parent 433d4a97be
commit 961f87d3c7
14 changed files with 354 additions and 27 deletions

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -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,

View File

@ -72,6 +72,7 @@ extension Defaults.Keys {
suite: SwiftfinStore.Defaults.generalSuite)
static let forceDirectPlay = Key<Bool>("forceDirectPlay", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let liveTVAlphaEnabled = Key<Bool>("liveTVAlphaEnabled", default: false, suite: SwiftfinStore.Defaults.generalSuite)
static let nativePlayer = Key<Bool>("nativePlayer", default: false, suite: SwiftfinStore.Defaults.generalSuite)
}
// tvos specific

View File

@ -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

View File

@ -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
}

View File

@ -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<AnyCancellable>()
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()
}
}

View File

@ -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)],

View File

@ -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

View File

@ -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;

View File

@ -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
}

View File

@ -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<AnyCancellable>()
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()
}
}

View File

@ -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)],

View File

@ -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