Merge pull request #320 from LePips/reimplement-native-player
This commit is contained in:
commit
0a8d2a0112
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -861,6 +865,7 @@
|
|||
E17885A7278130690094FBCF /* Overlays */,
|
||||
E1C812C8277AE40900918266 /* VideoPlayerView.swift */,
|
||||
E1384943278036C70024FB48 /* VLCPlayerViewController.swift */,
|
||||
E13AD72F2798C60F00FDCEE8 /* NativePlayerViewController.swift */,
|
||||
);
|
||||
path = VideoPlayer;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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,12 +422,12 @@ struct VLCPlayerOverlayView: View {
|
|||
}
|
||||
|
||||
struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {
|
||||
|
||||
static let videoPlayerViewModel = VideoPlayerViewModel(item: BaseItemDto(),
|
||||
title: "Glorious Purpose",
|
||||
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)],
|
||||
|
@ -468,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]
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue