229 lines
7.7 KiB
Swift
229 lines
7.7 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) 2025 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
|
|
|
|
@State
|
|
private var isPresentingOverlay: Bool = false
|
|
|
|
init(manager: LiveVideoPlayerManager) {
|
|
self.videoPlayerManager = manager
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var playerView: some View {
|
|
LiveNativeVideoPlayerView(videoPlayerManager: videoPlayerManager)
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
ZStack {
|
|
if let _ = videoPlayerManager.currentViewModel {
|
|
playerView
|
|
} else {
|
|
VideoPlayer.LoadingView()
|
|
}
|
|
}
|
|
}
|
|
.navigationBarHidden(true)
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|
|
|
|
struct LiveNativeVideoPlayerView: UIViewControllerRepresentable {
|
|
|
|
let videoPlayerManager: VideoPlayerManager
|
|
|
|
func makeUIViewController(context: Context) -> UILiveNativeVideoPlayerViewController {
|
|
UILiveNativeVideoPlayerViewController(manager: videoPlayerManager)
|
|
}
|
|
|
|
func updateUIViewController(_ uiViewController: UILiveNativeVideoPlayerViewController, context: Context) {}
|
|
}
|
|
|
|
class UILiveNativeVideoPlayerViewController: AVPlayerViewController {
|
|
|
|
let videoPlayerManager: VideoPlayerManager
|
|
|
|
private var rateObserver: NSKeyValueObservation!
|
|
private var timeObserverToken: Any!
|
|
|
|
init(manager: VideoPlayerManager) {
|
|
|
|
self.videoPlayerManager = manager
|
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
|
print("🎬 [LiveNativeVideoPlayer] Creating player with URL: \(manager.currentViewModel.playbackURL)")
|
|
print("🎬 [LiveNativeVideoPlayer] URL absoluteString: \(manager.currentViewModel.playbackURL.absoluteString)")
|
|
|
|
// Create AVURLAsset with options for live streaming
|
|
let asset = AVURLAsset(url: manager.currentViewModel.playbackURL, options: [
|
|
AVURLAssetPreferPreciseDurationAndTimingKey: false,
|
|
])
|
|
|
|
// Create player item from asset
|
|
let playerItem = AVPlayerItem(asset: asset)
|
|
|
|
// Configure for live streaming
|
|
playerItem.canUseNetworkResourcesForLiveStreamingWhilePaused = true
|
|
playerItem.preferredForwardBufferDuration = 1.0 // Minimal buffer for live streams
|
|
|
|
let newPlayer = AVPlayer(playerItem: playerItem)
|
|
|
|
newPlayer.allowsExternalPlayback = true
|
|
newPlayer.appliesMediaSelectionCriteriaAutomatically = false
|
|
newPlayer.automaticallyWaitsToMinimizeStalling = false // Don't wait for buffer, start immediately
|
|
playerItem.externalMetadata = createMetadata()
|
|
|
|
// Observe player item status to detect errors
|
|
if let playerItem = newPlayer.currentItem {
|
|
playerItem.addObserver(self, forKeyPath: "status", options: [.new, .old], context: nil)
|
|
print("🎬 [LiveNativeVideoPlayer] Added status observer to player item")
|
|
}
|
|
|
|
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 observeValue(
|
|
forKeyPath keyPath: String?,
|
|
of object: Any?,
|
|
change: [NSKeyValueChangeKey: Any]?,
|
|
context: UnsafeMutableRawPointer?
|
|
) {
|
|
if keyPath == "status" {
|
|
if let playerItem = object as? AVPlayerItem {
|
|
print("🎬 [LiveNativeVideoPlayer] Player item status changed to: \(playerItem.status.rawValue)")
|
|
switch playerItem.status {
|
|
case .failed:
|
|
if let error = playerItem.error {
|
|
print("🎬 [LiveNativeVideoPlayer] ERROR: Player item failed with error: \(error)")
|
|
print("🎬 [LiveNativeVideoPlayer] Error domain: \(error._domain), code: \(error._code)")
|
|
print("🎬 [LiveNativeVideoPlayer] Error description: \(error.localizedDescription)")
|
|
}
|
|
case .readyToPlay:
|
|
print("🎬 [LiveNativeVideoPlayer] Player item is ready to play")
|
|
case .unknown:
|
|
print("🎬 [LiveNativeVideoPlayer] Player item status is unknown")
|
|
@unknown default:
|
|
print("🎬 [LiveNativeVideoPlayer] Player item status is unknown default")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
// Remove status observer
|
|
player?.currentItem?.removeObserver(self, forKeyPath: "status")
|
|
|
|
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()
|
|
}
|
|
}
|