import SwiftUI import AVKit @preconcurrency import MediaPlayer // Global video player state class SharedVideoManager: ObservableObject { static let shared = SharedVideoManager() @Published var currentVideoURL: URL? @Published var showFullScreenPlayer = false @Published var isInPiPMode = false // Track the current player to force PiP stop weak var currentPlayerController: AVPlayerViewController? // Store current media info for lock screen controls var currentTitle: String? var currentArtworkURL: String? private init() { } func playVideo(url: URL, title: String? = nil, artworkURL: String? = nil) { // Store media info for lock screen controls currentTitle = title currentArtworkURL = artworkURL // If we already have a video playing, we need to switch to the new one if currentVideoURL != nil { if isInPiPMode { // Force stop PiP if it's active if let playerController = currentPlayerController { // Force stop the player to close PiP playerController.player?.pause() playerController.player = nil } } // Clear the old video first to ensure proper cleanup currentVideoURL = nil showFullScreenPlayer = false isInPiPMode = false currentPlayerController = nil // Small delay to let old player fully cleanup before starting new one DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { self.currentVideoURL = url self.showFullScreenPlayer = true self.isInPiPMode = false } } else { // No existing video, start immediately currentVideoURL = url showFullScreenPlayer = true isInPiPMode = false } } func hideFullScreenPlayer() { showFullScreenPlayer = false } func stopVideo() { // Stop the actual player first if let playerController = currentPlayerController { playerController.player?.pause() playerController.player = nil } // Clear all state currentVideoURL = nil showFullScreenPlayer = false isInPiPMode = false currentPlayerController = nil currentTitle = nil currentArtworkURL = nil } func setPiPMode(_ isPiP: Bool) { isInPiPMode = isPiP if isPiP { // When PiP starts, hide full screen but DON'T touch currentVideoURL showFullScreenPlayer = false } else { // When PiP ends, show full screen again showFullScreenPlayer = true } } } // Shared video overlay that exists at app level - GLOBAL PERSISTENT PLAYER struct SharedVideoOverlay: View { @StateObject private var videoManager = SharedVideoManager.shared @State private var currentPlayerURL: URL? var body: some View { ZStack { // ALWAYS render the persistent player when we have a URL // Force recreate when URL changes to ensure old player is destroyed if let url = videoManager.currentVideoURL { PersistentVideoPlayer(url: url) .id(url.absoluteString) // Force recreate when URL changes .opacity(videoManager.showFullScreenPlayer ? 1 : 0) // Show/hide but never destroy .allowsHitTesting(videoManager.showFullScreenPlayer) // Only interactive when visible .background(videoManager.showFullScreenPlayer ? Color.black : Color.clear) .ignoresSafeArea() .onAppear { } } } .onChange(of: videoManager.showFullScreenPlayer) { _, isVisible in } .onChange(of: videoManager.currentVideoURL) { oldURL, newURL in if let old = oldURL, let new = newURL, old != new { } else if newURL == nil { } } } } // Persistent video player that stays alive struct PersistentVideoPlayer: View { let url: URL var body: some View { VideoPlayerView(url: url) .onAppear { } .onDisappear { } } }