import Foundation import AVKit import Combine #if os(iOS) /// 画中画(Picture-in-Picture)控制器 /// 封装 AVPictureInPictureController,管理 PiP 生命周期 @MainActor final class PiPController: NSObject, ObservableObject { @Published var isPiPActive = false @Published var isPiPSupported = false private var pipController: AVPictureInPictureController? private weak var playerLayer: AVPlayerLayer? private var pipPossibleObservation: NSKeyValueObservation? /// 用 AVPlayerLayer 初始化 PiP 控制器 func setup(with playerLayer: AVPlayerLayer) { // 避免重复初始化 guard pipController == nil else { return } self.playerLayer = playerLayer guard AVPictureInPictureController.isPictureInPictureSupported() else { NSLog("[MiniPlayer] PiP not supported on this device") isPiPSupported = false return } isPiPSupported = true guard let pip = AVPictureInPictureController(playerLayer: playerLayer) else { NSLog("[MiniPlayer] PiP controller creation returned nil") isPiPSupported = false return } pip.delegate = self self.pipController = pip // KVO 观察 isPictureInPicturePossible 变化 pipPossibleObservation = pip.observe(\.isPictureInPicturePossible, options: [.new]) { [weak self] controller, _ in let possible = controller.isPictureInPicturePossible NSLog("[MiniPlayer] PiP possible changed: %d", possible) Task { @MainActor in // 触发 UI 刷新(按钮状态可能需要更新) self?.objectWillChange.send() } } NSLog("[MiniPlayer] PiP controller initialized, possible=%d", pip.isPictureInPicturePossible) } /// 切换画中画状态 func togglePiP() { guard let pip = pipController else { NSLog("[MiniPlayer] PiP controller not initialized") return } NSLog("[MiniPlayer] togglePiP: active=%d, possible=%d", pip.isPictureInPictureActive, pip.isPictureInPicturePossible) if pip.isPictureInPictureActive { pip.stopPictureInPicture() } else if pip.isPictureInPicturePossible { pip.startPictureInPicture() } else { NSLog("[MiniPlayer] PiP not possible — check: 1) player has content 2) Xcode project has 'Audio, AirPlay, and Picture in Picture' background mode enabled in Signing & Capabilities") } } /// 当前是否可以启动 PiP var canStartPiP: Bool { pipController?.isPictureInPicturePossible ?? false } deinit { pipPossibleObservation = nil pipController?.delegate = nil } } // MARK: - AVPictureInPictureControllerDelegate extension PiPController: AVPictureInPictureControllerDelegate { nonisolated func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { NSLog("[MiniPlayer] PiP will start") Task { @MainActor in self.isPiPActive = true } } nonisolated func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { NSLog("[MiniPlayer] PiP did start") } nonisolated func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { NSLog("[MiniPlayer] PiP will stop") } nonisolated func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { NSLog("[MiniPlayer] PiP did stop") Task { @MainActor in self.isPiPActive = false } } nonisolated func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) { NSLog("[MiniPlayer] PiP failed to start: \(error.localizedDescription)") } nonisolated func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { // 用户点击 PiP 窗口的恢复按钮时调用 // 返回 true 表示我们已处理恢复(回到 app 内播放) NSLog("[MiniPlayer] PiP restore requested") completionHandler(true) } } #endif