Merge pull request #320 from LePips/reimplement-native-player

This commit is contained in:
Ethan Pippin 2022-01-20 20:02:16 -07:00 committed by GitHub
commit 0a8d2a0112
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 370 additions and 34 deletions

View File

@ -28,12 +28,21 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {
PreferenceUIHostingControllerView { PreferenceUIHostingControllerView {
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) VLCPlayerView(viewModel: self.viewModel)
.navigationBarHidden(true) .navigationBarHidden(true)
.statusBar(hidden: true) .statusBar(hidden: true)
.ignoresSafeArea() .ignoresSafeArea()
.prefersHomeIndicatorAutoHidden(true) .prefersHomeIndicatorAutoHidden(true)
.supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape) .supportedOrientations(UIDevice.current.userInterfaceIdiom == .pad ? .all : .landscape)
}
}.ignoresSafeArea() }.ignoresSafeArea()
} }
} }

View File

@ -27,8 +27,14 @@ final class VideoPlayerCoordinator: NavigationCoordinatable {
@ViewBuilder @ViewBuilder
func makeStart() -> some View { func makeStart() -> some View {
if Defaults[.Experimental.nativePlayer] {
NativePlayerView(viewModel: viewModel)
.navigationBarHidden(true)
.ignoresSafeArea()
} else {
VLCPlayerView(viewModel: viewModel) VLCPlayerView(viewModel: viewModel)
.navigationBarHidden(true) .navigationBarHidden(true)
.ignoresSafeArea() .ignoresSafeArea()
} }
} }
}

View File

@ -40,6 +40,7 @@ extension BaseItemDto {
var viewModels: [VideoPlayerViewModel] = [] var viewModels: [VideoPlayerViewModel] = []
for currentMediaSource in mediaSources { for currentMediaSource in mediaSources {
let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first
let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? [] let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? [] let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
@ -48,11 +49,28 @@ extension BaseItemDto {
let defaultSubtitleStream = subtitleStreams let defaultSubtitleStream = subtitleStreams
.first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 }) .first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 })
var directStreamURL: URL // MARK: Build Streams
let directStreamURL: URL
let transcodedStreamURL: URLComponents? let transcodedStreamURL: URLComponents?
var hlsStreamURL: URL
let mediaSourceID: String let mediaSourceID: String
let streamType: ServerStreamType 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 { if let transcodeURL = currentMediaSource.transcodingUrl {
streamType = .transcode streamType = .transcode
transcodedStreamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI transcodedStreamURL = URLComponents(string: SessionManager.main.currentLogin.server.currentURI
@ -62,18 +80,30 @@ extension BaseItemDto {
transcodedStreamURL = nil transcodedStreamURL = nil
} }
if mediaSources.count > 1 { let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(itemId: id ?? "",
mediaSourceID = currentMediaSource.id! mediaSourceId: id ?? "",
} else {
mediaSourceID = self.id!
}
let requestBuilder = VideosAPI.getVideoStreamWithRequestBuilder(itemId: self.id!,
_static: true, _static: true,
tag: self.etag, tag: currentMediaSource.eTag,
minSegments: 6, deviceProfileId: nil,
mediaSourceId: mediaSourceID) playSessionId: response.playSessionId,
directStreamURL = URL(string: requestBuilder.URLString)! 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)
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
hlsStreamURL = hlsStreamComponents.url!
// MARK: VidoPlayerViewModel Creation // MARK: VidoPlayerViewModel Creation
@ -111,6 +141,7 @@ extension BaseItemDto {
subtitle: subtitle, subtitle: subtitle,
directStreamURL: directStreamURL, directStreamURL: directStreamURL,
transcodedStreamURL: transcodedStreamURL?.url, transcodedStreamURL: transcodedStreamURL?.url,
hlsStreamURL: hlsStreamURL,
streamType: streamType, streamType: streamType,
response: response, response: response,
audioStreams: audioStreams, audioStreams: audioStreams,

View File

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

View File

@ -109,6 +109,7 @@ final class VideoPlayerViewModel: ViewModel {
let subtitle: String? let subtitle: String?
let directStreamURL: URL let directStreamURL: URL
let transcodedStreamURL: URL? let transcodedStreamURL: URL?
let hlsStreamURL: URL
let audioStreams: [MediaStream] let audioStreams: [MediaStream]
let subtitleStreams: [MediaStream] let subtitleStreams: [MediaStream]
let chapters: [ChapterInfo] let chapters: [ChapterInfo]
@ -147,6 +148,13 @@ final class VideoPlayerViewModel: ViewModel {
Int64(currentSeconds) * 10_000_000 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 // MARK: Helpers
var currentAudioStream: MediaStream? { var currentAudioStream: MediaStream? {
@ -188,6 +196,7 @@ final class VideoPlayerViewModel: ViewModel {
subtitle: String?, subtitle: String?,
directStreamURL: URL, directStreamURL: URL,
transcodedStreamURL: URL?, transcodedStreamURL: URL?,
hlsStreamURL: URL,
streamType: ServerStreamType, streamType: ServerStreamType,
response: PlaybackInfoResponse, response: PlaybackInfoResponse,
audioStreams: [MediaStream], audioStreams: [MediaStream],
@ -210,6 +219,7 @@ final class VideoPlayerViewModel: ViewModel {
self.subtitle = subtitle self.subtitle = subtitle
self.directStreamURL = directStreamURL self.directStreamURL = directStreamURL
self.transcodedStreamURL = transcodedStreamURL self.transcodedStreamURL = transcodedStreamURL
self.hlsStreamURL = hlsStreamURL
self.streamType = streamType self.streamType = streamType
self.response = response self.response = response
self.audioStreams = audioStreams self.audioStreams = audioStreams

View File

@ -17,6 +17,8 @@ struct ExperimentalSettingsView: View {
var syncSubtitleStateWithAdjacent var syncSubtitleStateWithAdjacent
@Default(.Experimental.liveTVAlphaEnabled) @Default(.Experimental.liveTVAlphaEnabled)
var liveTVAlphaEnabled var liveTVAlphaEnabled
@Default(.Experimental.nativePlayer)
var nativePlayer
var body: some View { var body: some View {
Form { Form {
@ -28,6 +30,8 @@ struct ExperimentalSettingsView: View {
Toggle("Live TV (Alpha)", isOn: $liveTVAlphaEnabled) Toggle("Live TV (Alpha)", isOn: $liveTVAlphaEnabled)
Toggle("Native Player", isOn: $nativePlayer)
} header: { } header: {
L10n.experimental.text L10n.experimental.text
} }

View File

@ -0,0 +1,130 @@
//
// 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
if let transcodedStreamURL = viewModel.transcodedStreamURL {
player = AVPlayer(url: transcodedStreamURL)
} else {
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", subtitle: "Loki - S1E1",
directStreamURL: URL(string: "www.apple.com")!, directStreamURL: URL(string: "www.apple.com")!,
transcodedStreamURL: nil, transcodedStreamURL: nil,
hlsStreamURL: URL(string: "www.apple.com")!,
streamType: .direct, streamType: .direct,
response: PlaybackInfoResponse(), response: PlaybackInfoResponse(),
audioStreams: [MediaStream(displayTitle: "English", index: -1)], audioStreams: [MediaStream(displayTitle: "English", index: -1)],

View File

@ -9,6 +9,20 @@
import SwiftUI import SwiftUI
import UIKit 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 { struct VLCPlayerView: UIViewControllerRepresentable {
let viewModel: VideoPlayerViewModel let viewModel: VideoPlayerViewModel

View File

@ -290,6 +290,8 @@
E1361DA7278FA7A300BEC523 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E1361DA6278FA7A300BEC523 /* NukeUI */; }; E1361DA7278FA7A300BEC523 /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = E1361DA6278FA7A300BEC523 /* NukeUI */; };
E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; }; E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1384943278036C70024FB48 /* VLCPlayerViewController.swift */; };
E13849452780370B0024FB48 /* PlaybackSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C812B4277A8E5D00918266 /* PlaybackSpeed.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 */; }; E13DD3BF27163DD7009D4DAF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */; };
E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; }; E13DD3C227164941009D4DAF /* SwiftfinStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13DD3C127164941009D4DAF /* SwiftfinStore.swift */; };
E13DD3C327164941009D4DAF /* 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 = "<group>"; }; E126F740278A656C00A522BF /* ServerStreamType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerStreamType.swift; sourceTree = "<group>"; };
E131691626C583BC0074BFEE /* LogConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogConstructor.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>"; }; E1384943278036C70024FB48 /* VLCPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerViewController.swift; sourceTree = "<group>"; };
E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; };
E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePlayerViewController.swift; sourceTree = "<group>"; };
E13D02842788B634000FCB04 /* Swiftfin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swiftfin.entitlements; sourceTree = "<group>"; }; E13D02842788B634000FCB04 /* Swiftfin.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Swiftfin.entitlements; sourceTree = "<group>"; };
E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; E13DD3BE27163DD7009D4DAF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
E13DD3C127164941009D4DAF /* SwiftfinStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = "<group>"; }; E13DD3C127164941009D4DAF /* SwiftfinStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftfinStore.swift; sourceTree = "<group>"; };
@ -861,6 +865,7 @@
E17885A7278130690094FBCF /* Overlays */, E17885A7278130690094FBCF /* Overlays */,
E1C812C8277AE40900918266 /* VideoPlayerView.swift */, E1C812C8277AE40900918266 /* VideoPlayerView.swift */,
E1384943278036C70024FB48 /* VLCPlayerViewController.swift */, E1384943278036C70024FB48 /* VLCPlayerViewController.swift */,
E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */,
); );
path = VideoPlayer; path = VideoPlayer;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1565,8 +1570,9 @@
E193D5452719418B00900D82 /* VideoPlayer */ = { E193D5452719418B00900D82 /* VideoPlayer */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */, E13AD72D2798BC8D00FDCEE8 /* NativePlayerViewController.swift */,
E1002B692793E12E00E47059 /* Overlays */, E1002B692793E12E00E47059 /* Overlays */,
E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */,
E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */, E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */,
E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */, E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */,
); );
@ -2249,6 +2255,7 @@
C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */, C45B29BB26FAC5B600CEF5E0 /* ColorExtension.swift in Sources */,
E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */, E1D4BF822719D22800A11E64 /* AppAppearance.swift in Sources */,
C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */, C40CD923271F8CD8000FB198 /* MoviesLibrariesCoordinator.swift in Sources */,
E13AD7302798C60F00FDCEE8 /* NativePlayerViewController.swift in Sources */,
E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */, E1E5D54F2783E67100692DFE /* OverlaySettingsView.swift in Sources */,
53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */, 53272537268C1DBB0035FBF1 /* SeasonItemView.swift in Sources */,
09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */, 09389CC526814E4500AE350E /* DeviceProfileBuilder.swift in Sources */,
@ -2339,6 +2346,7 @@
5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */, 5D160403278A41FD00D22B99 /* VLCPlayer+subtitles.swift in Sources */,
536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */,
E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */, E1FCD08826C35A0D007C8DCF /* NetworkError.swift in Sources */,
E13AD72E2798BC8D00FDCEE8 /* NativePlayerViewController.swift in Sources */,
E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */, E13DD3E527177D15009D4DAF /* ServerListView.swift in Sources */,
E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */, E18845F826DEA9C900B0C5B7 /* ItemViewBody.swift in Sources */,
E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */, E173DA5426D050F500CC4EB7 /* ServerDetailViewModel.swift in Sources */,

View File

@ -15,6 +15,8 @@ struct ExperimentalSettingsView: View {
var forceDirectPlay var forceDirectPlay
@Default(.Experimental.syncSubtitleStateWithAdjacent) @Default(.Experimental.syncSubtitleStateWithAdjacent)
var syncSubtitleStateWithAdjacent var syncSubtitleStateWithAdjacent
@Default(.Experimental.nativePlayer)
var nativePlayer
var body: some View { var body: some View {
Form { Form {
@ -24,6 +26,8 @@ struct ExperimentalSettingsView: View {
Toggle("Sync Subtitles with Adjacent Episodes", isOn: $syncSubtitleStateWithAdjacent) Toggle("Sync Subtitles with Adjacent Episodes", isOn: $syncSubtitleStateWithAdjacent)
Toggle("Native Player", isOn: $nativePlayer)
} header: { } header: {
L10n.experimental.text L10n.experimental.text
} }

View File

@ -0,0 +1,114 @@
//
// 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
if let transcodedStreamURL = viewModel.transcodedStreamURL {
player = AVPlayer(url: transcodedStreamURL)
} else {
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

@ -14,7 +14,6 @@ import Sliders
import SwiftUI import SwiftUI
struct VLCPlayerOverlayView: View { struct VLCPlayerOverlayView: View {
@ObservedObject @ObservedObject
var viewModel: VideoPlayerViewModel var viewModel: VideoPlayerViewModel
@ -49,11 +48,9 @@ struct VLCPlayerOverlayView: View {
@ViewBuilder @ViewBuilder
private var mainBody: some View { private var mainBody: some View {
VStack { VStack {
// MARK: Top Bar // MARK: Top Bar
ZStack(alignment: .center) { ZStack(alignment: .top) {
if viewModel.overlayType == .compact { if viewModel.overlayType == .compact {
LinearGradient(gradient: Gradient(colors: [.black.opacity(0.8), .clear]), LinearGradient(gradient: Gradient(colors: [.black.opacity(0.8), .clear]),
startPoint: .top, startPoint: .top,
@ -63,9 +60,7 @@ struct VLCPlayerOverlayView: View {
} }
VStack(alignment: .EpisodeSeriesAlignmentGuide) { VStack(alignment: .EpisodeSeriesAlignmentGuide) {
HStack(alignment: .center) { HStack(alignment: .center) {
HStack { HStack {
Button { Button {
viewModel.playerOverlayDelegate?.didSelectClose() viewModel.playerOverlayDelegate?.didSelectClose()
@ -87,7 +82,6 @@ struct VLCPlayerOverlayView: View {
Spacer() Spacer()
HStack(spacing: 20) { HStack(spacing: 20) {
// MARK: Previous Item // MARK: Previous Item
if viewModel.shouldShowPlayPreviousItem { if viewModel.shouldShowPlayPreviousItem {
@ -165,7 +159,6 @@ struct VLCPlayerOverlayView: View {
// MARK: Settings Menu // MARK: Settings Menu
Menu { Menu {
// MARK: Audio Streams // MARK: Audio Streams
Menu { Menu {
@ -337,7 +330,6 @@ struct VLCPlayerOverlayView: View {
// MARK: Bottom Bar // MARK: Bottom Bar
ZStack(alignment: .center) { ZStack(alignment: .center) {
if viewModel.overlayType == .compact { if viewModel.overlayType == .compact {
LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8)]), LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8)]),
startPoint: .top, startPoint: .top,
@ -347,7 +339,6 @@ struct VLCPlayerOverlayView: View {
} }
HStack { HStack {
if viewModel.overlayType == .compact { if viewModel.overlayType == .compact {
HStack { HStack {
Button { Button {
@ -431,12 +422,12 @@ struct VLCPlayerOverlayView: View {
} }
struct VLCPlayerCompactOverlayView_Previews: PreviewProvider { struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
static let videoPlayerViewModel = VideoPlayerViewModel(item: BaseItemDto(), static let videoPlayerViewModel = VideoPlayerViewModel(item: BaseItemDto(),
title: "Glorious Purpose", title: "Glorious Purpose",
subtitle: "Loki - S1E1", subtitle: "Loki - S1E1",
directStreamURL: URL(string: "www.apple.com")!, directStreamURL: URL(string: "www.apple.com")!,
transcodedStreamURL: nil, transcodedStreamURL: nil,
hlsStreamURL: URL(string: "www.apple.com")!,
streamType: .direct, streamType: .direct,
response: PlaybackInfoResponse(), response: PlaybackInfoResponse(),
audioStreams: [MediaStream(displayTitle: "English", index: -1)], audioStreams: [MediaStream(displayTitle: "English", index: -1)],
@ -468,7 +459,6 @@ struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
// MARK: TitleSubtitleAlignment // MARK: TitleSubtitleAlignment
extension HorizontalAlignment { extension HorizontalAlignment {
private struct TitleSubtitleAlignment: AlignmentID { private struct TitleSubtitleAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat { static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[HorizontalAlignment.leading] context[HorizontalAlignment.leading]

View File

@ -9,6 +9,20 @@
import SwiftUI import SwiftUI
import UIKit 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 { struct VLCPlayerView: UIViewControllerRepresentable {
let viewModel: VideoPlayerViewModel let viewModel: VideoPlayerViewModel