视频暂停后切后台无法启动AVPictureInPicture的问题求助
iOS AVPlayer PiP 异常:暂停后切后台无法激活画中画
问题场景
使用AVPlayer搭配AVPictureInPictureController实现画中画功能,正常播放时PiP可正常触发,但在应用内暂停视频后切换至后台,PiP无法自动激活。尝试通过监听应用退活跃/进入后台通知,在回调中先恢复播放再启动PiP,仍收到报错:Failed to start PiP: Failed to start picture in picture。
核心原因
- 播放器状态未同步:调用
play()后立即启动PiP,此时播放器可能仍处于准备状态,rate尚未变为1,PiP启动要求播放器必须处于活跃播放状态。 - 音频会话失效:后台切换时音频会话可能被系统回收,未重新激活导致PiP启动失败。
- 操作时机与线程问题:
didEnterBackground通知触发时应用已进入后台,部分系统权限受限;且未确保PiP启动操作在主线程执行。
修复方案
1. 监听播放器播放状态,确保就绪后启动PiP
通过KVO监听AVPlayer的rate属性,当rate变为1(已开始播放)时,再触发PiP启动。
2. 重新激活音频会话
在启动PiP前重新初始化并激活音频会话,确保音频播放权限。
3. 调整通知监听时机
改用willResignActiveNotification提前处理,避免应用进入后台后权限受限;同时所有UI/播放器操作必须在主线程执行。
4. 优化PiP控制器初始化逻辑
避免强制解包,增加安全检查,确保PiP控制器已正确初始化。
修改后的完整代码
import UIKit import AVKit import AVFoundation class ViewControllerSec: UIViewController, AVPictureInPictureControllerDelegate { var pipPlayer: AVPlayer! var avCanvas: UIView! var pipCanvas: AVPlayerLayer? var pipController: AVPictureInPictureController? // 改为可选类型,避免强制解包风险 var mainViewController: UIViewController! var playerItem: AVPlayerItem! var videoAvasset: AVAsset! public func link(to parentViewController: UIViewController) { mainViewController = parentViewController setup() } private func setupAudio() { do { let session = AVAudioSession.sharedInstance() try session.setCategory(.playback, mode: .moviePlayback, options: [.mixWithOthers]) try session.setActive(true, options: .notifyOthersOnDeactivation) } catch { print("Audio session setup failed: \(error.localizedDescription)") } } @objc func playerItemDidFailToPlayToEnd(_ notification: Notification) { if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error { print("Failed to play to end: \(error.localizedDescription)") } } func setup() { setupAudio() // 1. 初始化AVPlayer guard let videoURL = URL(string: "https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.m3u8") else { return } videoAvasset = AVAsset(url: videoURL) playerItem = AVPlayerItem(asset: videoAvasset) addPlayerObservers() pipPlayer = AVPlayer(playerItem: playerItem) // 2. 初始化AVPlayerLayer avCanvas = UIView(frame: view.bounds) pipCanvas = AVPlayerLayer(player: pipPlayer) guard let pipCanvas else { return } pipCanvas.frame = avCanvas.bounds mainViewController.view.addSubview(avCanvas) avCanvas.layer.addSublayer(pipCanvas) // 3. 初始化AVPictureInPictureController if AVPictureInPictureController.isPictureInPictureSupported() { pipController = AVPictureInPictureController(playerLayer: pipCanvas) pipController?.delegate = self pipController?.canStartPictureInPictureAutomaticallyFromInline = true } // 添加操作按钮 let playButton = UIButton(frame: CGRect(x: 20, y: 50, width: 100, height: 50)) playButton.setTitle("Play", for: .normal) playButton.backgroundColor = .blue playButton.addTarget(self, action: #selector(playTapped), for: .touchUpInside) mainViewController.view.addSubview(playButton) let pauseButton = UIButton(frame: CGRect(x: 140, y: 50, width: 100, height: 50)) pauseButton.setTitle("Pause", for: .normal) pauseButton.backgroundColor = .red pauseButton.addTarget(self, action: #selector(pauseTapped), for: .touchUpInside) mainViewController.view.addSubview(pauseButton) let pipButton = UIButton(frame: CGRect(x: 260, y: 50, width: 150, height: 50)) pipButton.setTitle("Start PiP", for: .normal) pipButton.backgroundColor = .green pipButton.addTarget(self, action: #selector(startPictureInPicture), for: .touchUpInside) mainViewController.view.addSubview(pipButton) // 监听应用退活跃通知(替代didEnterBackground,提前触发逻辑) NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: .main) { [weak self] _ in guard let self = self, let pipController = self.pipController else { return } if !pipController.isPictureInPictureActive && self.pipPlayer.rate == 0 { // 重新激活音频会话 self.setupAudio() // 启动播放并监听rate变化 self.pipPlayer.play() self.addPlayerRateObserver() } } } private func addPlayerRateObserver() { // 监听播放器rate变化,确认播放开始后启动PiP pipPlayer.addObserver(self, forKeyPath: "rate", options: [.new], context: nil) } func addPlayerObservers() { // 监听playerItem状态 playerItem?.addObserver(self, forKeyPath: "status", options: [.old, .new], context: nil) // 监听播放完成 NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_:)), name: .AVPlayerItemDidPlayToEndTime, object: playerItem) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "status" { if let statusNumber = change?[.newKey] as? NSNumber { let status = AVPlayer.Status(rawValue: statusNumber.intValue)! switch status { case .readyToPlay: print("Player is ready to play") case .failed: print("Player failed: \(String(describing: playerItem?.error))") case .unknown: print("Player status is unknown") @unknown default: fatalError() } } } else if keyPath == "rate" { // 当rate变为1时,启动PiP并移除监听 if let rate = change?[.newKey] as? Float, rate == 1 { pipController?.startPictureInPicture() pipPlayer.removeObserver(self, forKeyPath: "rate") } } } @objc func playerDidFinishPlaying(_ notification: Notification) { print("Video finished playing.") } deinit { // 清理所有观察者 playerItem?.removeObserver(self, forKeyPath: "status") if pipPlayer != nil { pipPlayer.removeObserver(self, forKeyPath: "rate") } NotificationCenter.default.removeObserver(self) } @objc func playTapped() { pipPlayer.play() } @objc func pauseTapped() { pipPlayer.pause() } @objc func startPictureInPicture() { guard let pipController = pipController, !pipController.isPictureInPictureActive else { return } setupAudio() // 启动前确保音频会话激活 pipController.startPictureInPicture() } @objc func stopPictureInPicture() { guard let pipController = pipController, pipController.isPictureInPictureActive else { return } pipController.stopPictureInPicture() } // MARK: - AVPictureInPictureControllerDelegate Methods func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { print("PiP will start") } func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { print("PiP did start") } func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) { print("Failed to start PiP: \(error.localizedDescription)") if let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] { print("Underlying error: \(underlyingError)") } } func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { print("PiP will stop") } func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { print("PiP did stop") } }
关键修改点说明
- 将
pipController改为可选类型,避免强制解包导致的崩溃风险。 - 在
willResignActiveNotification中处理逻辑,提前触发操作,避免应用完全进入后台后权限受限。 - 添加
rate属性监听,确保播放器真正开始播放后再启动PiP。 - 每次启动PiP前重新激活音频会话,避免会话失效。
- 所有UI/播放器操作均在主线程执行(通过
queue: .main指定)。
内容的提问来源于stack exchange,提问作者Krenem00
相关产品推荐
相关产品推荐

