本文介绍如何在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)
}
}
}
该文章对您有帮助吗?