jellyflood/jellyflood tvOS/Views/VideoPlayer/LiveNativeVideoPlayer.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()
}
}