// // 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 Defaults import SwiftUI import VLCUI struct LiveVideoPlayer: View { enum OverlayType { case chapters case confirmClose case main case smallMenu } @EnvironmentObject private var router: LiveVideoPlayerCoordinator.Router @ObservedObject private var currentProgressHandler: VideoPlayerManager.CurrentProgressHandler @ObservedObject private var videoPlayerManager: LiveVideoPlayerManager @State private var isPresentingOverlay: Bool = false @State private var isScrubbing: Bool = false @ViewBuilder private var playerView: some View { ZStack { VLCVideoPlayer(configuration: videoPlayerManager.currentViewModel.vlcVideoPlayerConfiguration) .proxy(videoPlayerManager.proxy) .onTicksUpdated { ticks, _ in let newSeconds = ticks / 1000 let newProgress = CGFloat(newSeconds) / CGFloat(videoPlayerManager.currentViewModel.item.runTimeSeconds) currentProgressHandler.progress = newProgress currentProgressHandler.seconds = newSeconds guard !isScrubbing else { return } currentProgressHandler.scrubbedProgress = newProgress } .onStateUpdated { state, _ in videoPlayerManager.onStateUpdated(newState: state) if state == .ended { if let _ = videoPlayerManager.nextViewModel, Defaults[.VideoPlayer.autoPlayEnabled] { videoPlayerManager.selectNextViewModel() } else { router.dismissCoordinator() } } } LiveVideoPlayer.Overlay() .eraseToAnyView() .environmentObject(videoPlayerManager) .environmentObject(videoPlayerManager.currentProgressHandler) .environmentObject(videoPlayerManager.currentViewModel!) .environmentObject(videoPlayerManager.proxy) .environment(\.isPresentingOverlay, $isPresentingOverlay) .environment(\.isScrubbing, $isScrubbing) } .onChange(of: videoPlayerManager.currentProgressHandler.scrubbedProgress) { _, newValue in guard !newValue.isNaN && !newValue.isInfinite else { return } DispatchQueue.main.async { videoPlayerManager.currentProgressHandler .scrubbedSeconds = Int(CGFloat(videoPlayerManager.currentViewModel.item.runTimeSeconds) * newValue) } } } @ViewBuilder private var loadingView: some View { VideoPlayer.LoadingView() } var body: some View { ZStack { Color.black if let _ = videoPlayerManager.currentViewModel { playerView } else { loadingView } } .ignoresSafeArea() .onChange(of: isScrubbing) { _, newValue in guard !newValue else { return } videoPlayerManager.proxy.setTime(.seconds(currentProgressHandler.scrubbedSeconds)) } } } extension LiveVideoPlayer { init(manager: LiveVideoPlayerManager) { self.init( currentProgressHandler: manager.currentProgressHandler, videoPlayerManager: manager ) } }