You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

视频暂停后切后台无法启动AVPictureInPicture的问题求助

iOS AVPlayer PiP 异常:暂停后切后台无法激活画中画

问题场景

使用AVPlayer搭配AVPictureInPictureController实现画中画功能,正常播放时PiP可正常触发,但在应用内暂停视频后切换至后台,PiP无法自动激活。尝试通过监听应用退活跃/进入后台通知,在回调中先恢复播放再启动PiP,仍收到报错:Failed to start PiP: Failed to start picture in picture。

核心原因

  1. 播放器状态未同步:调用play()后立即启动PiP,此时播放器可能仍处于准备状态,rate尚未变为1,PiP启动要求播放器必须处于活跃播放状态。
  2. 音频会话失效:后台切换时音频会话可能被系统回收,未重新激活导致PiP启动失败。
  3. 操作时机与线程问题: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 10:15:56