import SwiftUI import AVKit @preconcurrency import MediaPlayer struct VideoPlayerView: View { let url: URL @Environment(\.dismiss) private var dismiss @State private var isInPiPMode = false @State private var isLoading = true @State private var isInNativeFullscreen = false var body: some View { ZStack { VideoPlayerViewController(url: url, isInPiPMode: $isInPiPMode, isLoading: $isLoading, isInNativeFullscreen: $isInNativeFullscreen) .ignoresSafeArea() .navigationBarTitleDisplayMode(.inline) .navigationBarBackButtonHidden(isInPiPMode) if isLoading { ProgressView() .progressViewStyle(.circular) .scaleEffect(1.5) .tint(.white) } // Close button - only show when NOT in native fullscreen mode #if os(iOS) if !isInNativeFullscreen { VStack { HStack { Spacer() Button(action: { SharedVideoManager.shared.stopVideo() }) { ZStack { // Background circle for better tap target Circle() .fill(Color.black.opacity(0.6)) .frame(width: 44, height: 44) Image(systemName: "xmark.circle.fill") .font(.system(size: UIDevice.current.userInterfaceIdiom == .pad ? 28 : 20)) .foregroundColor(.white) } } .buttonStyle(.plain) .frame(width: 44, height: 44) .contentShape(Circle()) // Make the entire circle tappable .padding(.top, UIDevice.current.userInterfaceIdiom == .pad ? 20 : 130) // Moved further below volume controls .padding(.trailing, UIDevice.current.userInterfaceIdiom == .pad ? 20 : 16) // Better aligned with volume button on iPhone } Spacer() } } #endif } .onAppear { print("🎬 DEBUG: VideoPlayerView onAppear") setupAudio() } .onDisappear { print("🎬 DEBUG: VideoPlayerView onDisappear") } } private func setupAudio() { do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback) try AVAudioSession.sharedInstance().setActive(true) print("🎬 DEBUG: Audio session configured for playback with movie mode") } catch { print("🎬 DEBUG: Failed to configure audio session: \(error)") } } } struct VideoPlayerViewController: UIViewControllerRepresentable { let url: URL @Binding var isInPiPMode: Bool @Binding var isLoading: Bool @Binding var isInNativeFullscreen: Bool func makeUIViewController(context: Context) -> AVPlayerViewController { print("🎬 DEBUG: makeUIViewController called with URL: \(url)") let playerItem = AVPlayerItem(url: url) let player = AVPlayer(playerItem: playerItem) // Set metadata directly on the player item setMetadataOnPlayerItem(playerItem) let controller = AVPlayerViewController() controller.player = player controller.allowsPictureInPicturePlayback = true controller.delegate = context.coordinator // Disable fullscreen functionality controller.entersFullScreenWhenPlaybackBegins = false controller.exitsFullScreenWhenPlaybackEnds = false print("🎬 DEBUG: Setting up player and controller") context.coordinator.setPlayerController(controller) // Add observers for buffering state and duration player.addObserver(context.coordinator, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil) player.currentItem?.addObserver(context.coordinator, forKeyPath: "duration", options: [.old, .new], context: nil) context.coordinator.hasObserver = true print("🎬 DEBUG: Observer added successfully") print("🎬 DEBUG: Starting playback") player.play() return controller } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator(isInPiPMode: $isInPiPMode, isLoading: $isLoading, isInNativeFullscreen: $isInNativeFullscreen) } private func setMetadataOnPlayerItem(_ playerItem: AVPlayerItem) { let videoManager = SharedVideoManager.shared var metadata: [AVMetadataItem] = [] // Title if let title = videoManager.currentTitle { let titleItem = AVMutableMetadataItem() titleItem.identifier = .commonIdentifierTitle titleItem.value = title as NSString titleItem.extendedLanguageTag = "und" metadata.append(titleItem) } // Artist let artistItem = AVMutableMetadataItem() artistItem.identifier = .commonIdentifierArtist artistItem.value = "Rockville Tolland SDA Church" as NSString artistItem.extendedLanguageTag = "und" metadata.append(artistItem) // Artwork if let artworkURLString = videoManager.currentArtworkURL, let artworkURL = URL(string: artworkURLString) { Task { do { let (data, _) = try await URLSession.shared.data(from: artworkURL) if UIImage(data: data) != nil { let artworkItem = AVMutableMetadataItem() artworkItem.identifier = .commonIdentifierArtwork artworkItem.value = data as NSData artworkItem.dataType = kCMMetadataBaseDataType_JPEG as String artworkItem.extendedLanguageTag = "und" await MainActor.run { var updatedMetadata = playerItem.externalMetadata updatedMetadata.append(artworkItem) playerItem.externalMetadata = updatedMetadata } } } catch { print("Failed to load artwork for metadata: \(error)") } } } playerItem.externalMetadata = metadata } class Coordinator: NSObject, AVPlayerViewControllerDelegate { @Binding var isInPiPMode: Bool @Binding var isLoading: Bool @Binding var isInNativeFullscreen: Bool internal var playerController: AVPlayerViewController? private var wasPlayingBeforeDismiss = false internal var hasObserver = false init(isInPiPMode: Binding, isLoading: Binding, isInNativeFullscreen: Binding) { _isInPiPMode = isInPiPMode _isLoading = isLoading _isInNativeFullscreen = isInNativeFullscreen print("🎬 DEBUG: Coordinator init") super.init() } func setPlayerController(_ controller: AVPlayerViewController) { print("🎬 DEBUG: setPlayerController called") playerController = controller // Register with SharedVideoManager so it can control PiP SharedVideoManager.shared.currentPlayerController = controller print("🎬 DEBUG: Player registered with SharedVideoManager") } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { print("🎬 DEBUG: observeValue called for keyPath: \(keyPath ?? "nil")") if keyPath == "timeControlStatus", let player = object as? AVPlayer { let status = player.timeControlStatus print("🎬 DEBUG: timeControlStatus = \(status.rawValue)") DispatchQueue.main.async { self.isLoading = player.timeControlStatus == .waitingToPlayAtSpecifiedRate print("🎬 DEBUG: isLoading set to \(self.isLoading)") } } else if keyPath == "duration", let playerItem = object as? AVPlayerItem { let duration = playerItem.duration if duration.isValid && !duration.isIndefinite { let durationSeconds = CMTimeGetSeconds(duration) print("🎬 DEBUG: Duration loaded: \(durationSeconds) seconds") } } } func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { print("🎬 DEBUG: willBeginFullScreenPresentation") DispatchQueue.main.async { self.isInNativeFullscreen = true } if let player = playerController?.player { wasPlayingBeforeDismiss = (player.rate > 0) print("🎬 DEBUG: wasPlayingBeforeDismiss = \(wasPlayingBeforeDismiss)") } } func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { print("🎬 DEBUG: willEndFullScreenPresentation - exiting native fullscreen") DispatchQueue.main.async { self.isInNativeFullscreen = false } if wasPlayingBeforeDismiss, let player = playerController?.player { print("🎬 DEBUG: Restoring playback rate to 1.0") player.rate = 1.0 } // Notify SharedVideoManager that we're exiting fullscreen DispatchQueue.main.async { SharedVideoManager.shared.showFullScreenPlayer = false print("🎬 DEBUG: Set SharedVideoManager.showFullScreenPlayer = false") } } func playerViewControllerWillStartPictureInPicture(_ playerViewController: AVPlayerViewController) { print("🎬 DEBUG: PiP WILL START") DispatchQueue.main.async { self.isInPiPMode = true SharedVideoManager.shared.setPiPMode(true) print("🎬 DEBUG: isInPiPMode set to true") } } func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) { print("🎬 DEBUG: PiP DID START") } func playerViewControllerWillStopPictureInPicture(_ playerViewController: AVPlayerViewController) { print("🎬 DEBUG: PiP WILL STOP") } func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { print("🎬 DEBUG: PiP DID STOP") DispatchQueue.main.async { self.isInPiPMode = false SharedVideoManager.shared.setPiPMode(false) print("🎬 DEBUG: isInPiPMode set to false") } } func playerViewController(_ playerViewController: AVPlayerViewController, failedToStartPictureInPictureWithError error: Error) { print("🎬 DEBUG: PiP FAILED TO START: \(error)") } func playerViewController(_ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { print("🎬 DEBUG: RESTORE USER INTERFACE FOR PIP STOP - This is the 'Return to App' action") print("🎬 DEBUG: Current isInPiPMode: \(isInPiPMode)") print("🎬 DEBUG: playerController exists: \(playerController != nil)") // This is the critical method for "Return to App" DispatchQueue.main.async { print("🎬 DEBUG: Calling completionHandler(true)") completionHandler(true) } } deinit { print("🎬 DEBUG: Coordinator deinit") if let player = playerController?.player, hasObserver { print("🎬 DEBUG: Removing observers in deinit") player.removeObserver(self, forKeyPath: "timeControlStatus") player.currentItem?.removeObserver(self, forKeyPath: "duration") hasObserver = false } } } }