iOS实现画中画

本文介绍如何在iOS端实现画中画(悬浮窗)。

功能介绍

“画中画”(Picture-in-Picture,简称 PiP)是一种让视频“悬浮”在屏幕上的功能。开启后,视频会以一个小窗口的形式显示在屏幕一角,用户可以一边看视频,一边正常使用手机或电脑的其他应用,比如回消息、浏览网页或操作其他App,互不干扰。

示例代码

ARTC 提供了开源示例代码供您参考:iOS实现画中画

前提条件

要实现画中画,需要满足如下条件:

  • iOS 版本不能低于 15。

  • 进入后台后如果需要保持摄像头采集权限,需要为开发者账号添加 entitlement 权限,详情请参考苹果Entitlements权限文档

功能实现

1. 实现自定义视频渲染

iOS中如果需要画中画小窗中显示 RTC 的视频画面,需要实现自定义渲染功能,请参考:自定义视频渲染

2. 创建画中画控制器

func setupPictureInPicture() {
    guard #available(iOS 15.0, *) else {
        print("iOS < 15.0,不支持系统画中画")
        return
    }

    let callVC = AVPictureInPictureVideoCallViewController()
    callVC.preferredContentSize = CGSize(width: 720, height: 1280)
    callVC.view.backgroundColor = .clear

    self.pipCallViewController = callVC
}

@available(iOS 15.0, *)
func setupPipController(with seatView: CustomVideoRenderSeatView) {
    
    // 只保存引用和父视图信息,不做移动
    seatView.originalSuperview = seatView.superview
    self.pipSourceView = seatView
    
    // PIP 容器(先是空的)
    let callVC = AVPictureInPictureVideoCallViewController()
    callVC.preferredContentSize = CGSize(width: 720, height: 1280)
    callVC.view.backgroundColor = .clear
    self.pipCallViewController = callVC
    
    // 创建 ContentSource(activeVideoCallSourceView 在这里还是 seatView)
    // 注意:用 seatView 作为源,而不是 callVC.view
    let contentSource = AVPictureInPictureController.ContentSource(
        activeVideoCallSourceView: seatView,
        contentViewController: callVC
    )
    
    let pipController = AVPictureInPictureController(contentSource: contentSource)
    pipController.delegate = self
    pipController.canStartPictureInPictureAutomaticallyFromInline = false
    
    self.pipController = pipController
}

3. 进入画中画模式

调用系统 APIpipController.startPictureInPicture进入画中画模式

// MARK: - Enter PIP Mode
@IBAction func onEnterPIPModeBtnClicked(_ sender: UIButton) {
    enterPictureInPictureMode()
}

func enterPictureInPictureMode() {
    guard AVPictureInPictureController.isPictureInPictureSupported() else {
        UIAlertController.showAlertWithMainThread(msg: "当前设备不支持画中画", vc: self)
        return
    }

    guard #available(iOS 15.0, *) else {
        UIAlertController.showAlertWithMainThread(msg: "画中画功能需要 iOS 15 或更高版本", vc: self)
        return
    }

    // 如果还没有创建控制器,尝试初始化
    if pipController == nil {
        guard let seatView = self.seatViewList.first(where: { $0.uid == self.userId }) else {
            print("无法获取本地预览视图")
            return
        }
        setupPipController(with: seatView)
    }

    guard let pipController = self.pipController else { return }

    if pipController.isPictureInPictureActive {
        pipController.stopPictureInPicture()
    } else {
        pipController.startPictureInPicture()
    }
}

4. 开启画中画,将画面加载到画中画

// MARK: - AVPictureInPictureControllerDelegate
@available(iOS 15.0, *)
extension PictureInPictureMainVC: AVPictureInPictureControllerDelegate {

    func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("即将进入画中画模式")
        
        // 迁移seatView
        if let seatView = self.pipSourceView as? CustomVideoRenderSeatView,
           let callVC = self.pipCallViewController as? AVPictureInPictureVideoCallViewController {
            
            seatView.removeFromSuperview()
            callVC.view.addSubview(seatView)
            seatView.frame = callVC.view.bounds
            seatView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        }
    }


    func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("已进入画中画模式")
    }

    func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("即将退出画中画模式")
    }

    func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("已退出画中画模式")
        
        if let seatView = self.pipSourceView as? CustomVideoRenderSeatView,
           let originalSuperview = seatView.originalSuperview {
            
            seatView.removeFromSuperview()
            originalSuperview.addSubview(seatView)
            self.updateSeatViewsLayout()
        }
    }



    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController,
                   restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
        print("恢复用户界面")
        completionHandler(true)
    }

    func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController,
                   failedToStartPictureInPictureWithError error: Error) {
        print("启动画中画失败: \(error.localizedDescription)")
        DispatchQueue.main.async {
            UIAlertController.showAlertWithMainThread(msg: "启动画中画失败: \(error.localizedDescription)", vc: self)
        }
    }
}