178 lines
5.4 KiB
Swift
178 lines
5.4 KiB
Swift
//
|
|
// 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) 2024 Jellyfin & Jellyfin Contributors
|
|
//
|
|
|
|
import AVKit
|
|
import Combine
|
|
import Defaults
|
|
import JellyfinAPI
|
|
import SwiftUI
|
|
|
|
struct LiveNativeVideoPlayer: View {
|
|
|
|
@EnvironmentObject
|
|
private var router: LiveVideoPlayerCoordinator.Router
|
|
|
|
@ObservedObject
|
|
private var videoPlayerManager: LiveVideoPlayerManager
|
|
|
|
init(manager: LiveVideoPlayerManager) {
|
|
self.videoPlayerManager = manager
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var playerView: some View {
|
|
LiveNativeVideoPlayerView(videoPlayerManager: videoPlayerManager)
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let _ = videoPlayerManager.currentViewModel {
|
|
playerView
|
|
} else {
|
|
VideoPlayer.LoadingView()
|
|
}
|
|
}
|
|
.navigationBarHidden()
|
|
.statusBarHidden()
|
|
.ignoresSafeArea()
|
|
.onDisappear {
|
|
NotificationCenter.default.post(name: .livePlayerDismissed, object: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct LiveNativeVideoPlayerView: UIViewControllerRepresentable {
|
|
|
|
let videoPlayerManager: LiveVideoPlayerManager
|
|
|
|
func makeUIViewController(context: Context) -> UILiveNativeVideoPlayerViewController {
|
|
UILiveNativeVideoPlayerViewController(manager: videoPlayerManager)
|
|
}
|
|
|
|
func updateUIViewController(_ uiViewController: UILiveNativeVideoPlayerViewController, context: Context) {}
|
|
}
|
|
|
|
class UILiveNativeVideoPlayerViewController: AVPlayerViewController {
|
|
|
|
let videoPlayerManager: LiveVideoPlayerManager
|
|
|
|
private var rateObserver: NSKeyValueObservation!
|
|
private var timeObserverToken: Any!
|
|
|
|
init(manager: LiveVideoPlayerManager) {
|
|
|
|
self.videoPlayerManager = manager
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
let newPlayer: AVPlayer = .init(url: manager.currentViewModel.hlsPlaybackURL)
|
|
|
|
newPlayer.allowsExternalPlayback = true
|
|
newPlayer.appliesMediaSelectionCriteriaAutomatically = false
|
|
newPlayer.currentItem?.externalMetadata = createMetadata()
|
|
|
|
// enable pip
|
|
allowsPictureInPicturePlayback = true
|
|
|
|
rateObserver = newPlayer.observe(\.rate, options: .new) { _, change in
|
|
guard let newValue = change.newValue else { return }
|
|
|
|
if newValue == 0 {
|
|
self.videoPlayerManager.onStateUpdated(newState: .paused)
|
|
} else {
|
|
self.videoPlayerManager.onStateUpdated(newState: .playing)
|
|
}
|
|
}
|
|
|
|
let time = CMTime(seconds: 0.1, preferredTimescale: 1000)
|
|
|
|
timeObserverToken = newPlayer.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in
|
|
|
|
guard let self else { return }
|
|
|
|
if time.seconds >= 0 {
|
|
let newSeconds = Int(time.seconds)
|
|
let progress = CGFloat(newSeconds) / CGFloat(self.videoPlayerManager.currentViewModel.item.runTimeSeconds)
|
|
self.videoPlayerManager.currentProgressHandler.progress = progress
|
|
self.videoPlayerManager.currentProgressHandler.scrubbedProgress = progress
|
|
self.videoPlayerManager.currentProgressHandler.seconds = newSeconds
|
|
self.videoPlayerManager.currentProgressHandler.scrubbedSeconds = newSeconds
|
|
}
|
|
}
|
|
|
|
player = newPlayer
|
|
}
|
|
|
|
@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()
|
|
guard let timeObserverToken else { return }
|
|
player?.removeTimeObserver(timeObserverToken)
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
player?.seek(
|
|
to: CMTimeMake(
|
|
value: Int64(videoPlayerManager.currentViewModel.item.startTimeSeconds - Defaults[.VideoPlayer.resumeOffset]),
|
|
timescale: 1
|
|
),
|
|
toleranceBefore: .zero,
|
|
toleranceAfter: .zero,
|
|
completionHandler: { _ in
|
|
self.play()
|
|
}
|
|
)
|
|
}
|
|
|
|
private func createMetadata() -> [AVMetadataItem] {
|
|
let allMetadata: [AVMetadataIdentifier: Any?] = [
|
|
.commonIdentifierTitle: videoPlayerManager.currentViewModel.item.displayTitle,
|
|
.iTunesMetadataTrackSubTitle: videoPlayerManager.currentViewModel.item.subtitle,
|
|
]
|
|
|
|
return allMetadata.compactMap { createMetadataItem(for: $0, value: $1) }
|
|
}
|
|
|
|
private func createMetadataItem(
|
|
for identifier: AVMetadataIdentifier,
|
|
value: Any?
|
|
) -> AVMetadataItem? {
|
|
guard let value else { return nil }
|
|
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
|
|
}
|
|
|
|
private func play() {
|
|
player?.play()
|
|
|
|
videoPlayerManager.sendStartReport()
|
|
}
|
|
|
|
private func stop() {
|
|
player?.pause()
|
|
|
|
videoPlayerManager.sendStopReport()
|
|
}
|
|
}
|