- PiPController: add KVO on isPictureInPicturePossible, avoid duplicate setup, better logging - HLSRecorder: add stopRequested check after Task.sleep to ensure recording exits cleanly - HLSRecorder: add 120s timeout to remuxToMP4 via TaskGroup to prevent hanging - HLSRecorder: improve presentActivitySheet VC lookup (key window, foregroundActive scene) - Add detailed logging throughout recording/export flow for debugging
116 lines
4.5 KiB
Swift
116 lines
4.5 KiB
Swift
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
|