Agora iOS SDK:AVSampleBufferDisplayLayer无法启用原生PiP及后台渲染问题
针对你在Agora iOS SDK中结合AVSampleBufferDisplayLayer使用原生画中画(PiP)的问题,以下是具体解决方案:
一、修正Agora初始化配置
首先清理冗余参数,确保后台视频输出配置正确,同时必须激活音频模块(iOS后台仅音频活跃进程能持续运行):
// 初始化Agora引擎 agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "你的AppID", delegate: self) // 配置后台视频输出 agoraKit.setParameters("{\"rtc.video.background_mode\": 1}") // 允许后台输出视频帧 agoraKit.setParameters("{\"rtc.video.enable_ios_background_mode\": true}") agoraKit.setParameters("{\"che.video.force_renderer_background\": true}") // 兼容旧版本渲染逻辑 // 基础直播配置 agoraKit.setChannelProfile(.liveBroadcasting) agoraKit.setClientRole(.broadcaster) agoraKit.enableVideo() agoraKit.enableLocalVideo(true) agoraKit.setVideoFrameDelegate(self) // 必须在enableVideo后设置 // 激活音频模块(后台运行必需,可静音) agoraKit.enableAudio() agoraKit.muteLocalAudioStream(true) // 配置系统音频会话 let audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory(.playback, mode: .default, options: .mixWithOthers) try audioSession.setActive(true) } catch { print("音频会话配置失败:\(error)") }
二、构建支持PiP的视频渲染管道
AVSampleBufferDisplayLayer不直接支持PiP,必须通过AVPlayerLayer配合AVPictureInPictureController实现。我们将在前台用AVSampleBufferDisplayLayer渲染,后台切换到AVPlayerLayer驱动PiP:
- 声明核心组件:
import AVKit private var playerLayer: AVPlayerLayer! private var pipController: AVPictureInPictureController? private var videoLayerRemote: AVSampleBufferDisplayLayer?
- 初始化渲染组件与PiP控制器:
func setupRenderAndPiP() { // 初始化前台渲染层 videoLayerRemote = AVSampleBufferDisplayLayer() videoLayerRemote?.frame = view.bounds view.layer.addSublayer(videoLayerRemote!) // 初始化AVPlayerLayer(用于PiP) playerLayer = AVPlayerLayer(player: AVPlayer()) playerLayer.frame = view.bounds playerLayer.hidden = true // 前台隐藏,后台显示 view.layer.addSublayer(playerLayer) // 初始化PiP控制器 guard AVPictureInPictureController.isPictureInPictureSupported() else { return } pipController = AVPictureInPictureController(playerLayer: playerLayer) pipController?.delegate = self }
- 调整
onRenderVideoFrame的帧处理逻辑,区分前后台渲染目标:
func onRenderVideoFrame(_ videoFrame: AgoraOutputVideoFrame, uid: UInt, channelId: String) -> Bool { guard uid != localUid else { return true } var sampleBuffer: CMSampleBuffer? // 保留你现有的I420/PixelBuffer转CMSampleBuffer逻辑 if videoFrame.type == 1 { // I420格式 guard let yBuffer = videoFrame.yBuffer, let uBuffer = videoFrame.uBuffer, let vBuffer = videoFrame.vBuffer else { return true } sampleBuffer = convertI420ToCMSampleBuffer(yPlane: yBuffer, uPlane: uBuffer, vPlane: vBuffer, width: Int(videoFrame.width), height: Int(videoFrame.height)) } else { guard let pixelBuffer = videoFrame.pixelBuffer else { return true } sampleBuffer = createSampleBuffer(from: pixelBuffer) } guard let buffer = sampleBuffer else { return true } let appState = UIApplication.shared.applicationState if appState == .active { // 前台:用AVSampleBufferDisplayLayer渲染 guard let videoLayer = videoLayerRemote else { return true } if videoLayer.status == .failed { videoLayer.flush() } if videoLayer.isReadyForMoreMediaData { videoLayer.enqueue(buffer) } } else { // 后台:将帧传入AVPlayerLayer(先把AVSampleBufferDisplayLayer移到playerLayer下) if videoLayerRemote?.superlayer !== playerLayer { videoLayerRemote?.removeFromSuperlayer() playerLayer.addSublayer(videoLayerRemote!) videoLayerRemote?.frame = playerLayer.bounds } guard let videoLayer = videoLayerRemote else { return true } if videoLayer.status == .failed { videoLayer.flush() } if videoLayer.isReadyForMoreMediaData { videoLayer.enqueue(buffer) } } return true }
三、处理应用前后台切换
注册通知并实现切换逻辑,自动启动/停止PiP,切换渲染层:
override func viewDidLoad() { super.viewDidLoad() setupRenderAndPiP() // 注册前后台通知 NotificationCenter.default.addObserver(self, selector: #selector(enterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(enterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } @objc private func enterBackground() { // 显示playerLayer,隐藏前台渲染层 playerLayer.hidden = false videoLayerRemote?.hidden = false // 启动PiP if let pipController = pipController, !pipController.isPictureInPictureActive { pipController.startPictureInPicture() } } @objc private func enterForeground() { // 停止PiP pipController?.stopPictureInPicture() // 切换回前台渲染层 playerLayer.hidden = true if videoLayerRemote?.superlayer !== view.layer { videoLayerRemote?.removeFromSuperlayer() view.layer.addSublayer(videoLayerRemote!) videoLayerRemote?.frame = view.bounds videoLayerRemote?.hidden = false } }
四、PiP控制器代理实现(可选)
处理PiP状态变化,比如用户手动关闭PiP的场景:
extension YourViewController: AVPictureInPictureControllerDelegate { func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { // 用户关闭PiP后,可选回到前台或停止视频流 if UIApplication.shared.applicationState == .background { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) } } }
关键注意事项
- 后台权限配置:在Xcode Capabilities中必须启用
Audio, AirPlay, and Picture in Picture和Background Processing;Info.plist中添加NSCameraUsageDescription和NSMicrophoneUsageDescription(音频模块活跃必需)。 - 时间戳正确性:确保你的
convertI420ToCMSampleBuffer和createSampleBuffer函数生成的CMSampleBuffer带有递增的、符合视频帧率的presentationTimeStamp,否则PiP会无法正常播放。 - Agora参数说明:
rtc.video.pip仅适用于Agora自带渲染视图,自定义渲染时无效,可直接删除该参数。
内容的提问来源于stack exchange,提问作者Himanshu Fulwani
相关产品推荐
相关产品推荐

