MiniPlayer/Sources/PiPController.swift
yumoqing bfb7631714 fix(iOS): improve PiP KVO observation, fix recording stop flow and export sheet
- 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
2026-07-02 21:13:24 +08:00

116 lines
4.5 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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