// // 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() } }