做者简介:龚宇华,声网Agora.io 首席iOS研发工程师,负责iOS端移动应用产品设计和技术架构。node
去年中旬,苹果在 WWDC2017 推出了 ARKit。经过它,开发者能够更加快速地在 iOS 平台开发 AR 应用,利用镜头将虚拟照进现实。最近苹果还加强了 iOS 系统对 ARKit的支持,并将加大对 AR 应用的推广力度。git
在本篇中,咱们将会把 ARKit 融入视频会议场景中。本文将会介绍视频中两种场景的实现:github
将 ARKit 融入直播中数组
将直播连麦的对方画面渲染到 AR 场景中session
咱们将一块儿在直播场景中利用 ARKit 实现平面检测,还将应用到 Agora SDK 2.1 的新功能“自定义视频源与渲染器”。若是你在此以前还未了解过 ARKit 的基本类及其原理,能够先阅读《上篇:ARKit 基础知识》。架构
很少说,先上效果图。尽管距离电影中看到的全息视频会议效果还有距离,但你们能够试着对后期效果优化无限接近电影场景(文末有源码)。咱们在这里仅分享利用 AR 在视频会议中的实现技巧。app
咱们首先使用ARKit建立一个简单的识别平面的应用作为开发基础。async
在Xcode中使用 Augmented Reality App 模版建立一个新项目,其中 Content Technology 选择 SceneKit.ide
在 ViewController 中设置 ARConfiguration 为平面检测。post
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.session.delegate = self
sceneView.showsStatistics = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
sceneView.session.run(configuration)
}
复制代码
实现 ARSCNViewDelegate 的回调方法 renderer:didAddNode:forAnchor:
,在识别出的平面上添加一个红色的面。
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else {
return
}
// 建立红色平面模型
let plane = SCNBox(width: CGFloat(planeAnchor.extent.x),
height: CGFloat(planeAnchor.extent.y),
length: CGFloat(planeAnchor.extent.z),
chamferRadius: 0)
plane.firstMaterial?.diffuse.contents = UIColor.red
// 用模型生成 Node 对象并添加到识别出的平面上
let planeNode = SCNNode(geometry: plane)
node.addChildNode(planeNode)
// 渐隐消失
planeNode.runAction(SCNAction.fadeOut(duration: 1))
}
复制代码
这样就完成了一个最简单的AR应用,当识别出环境中的平面时,会在上面添加一个红色的矩形,并渐隐消失。
接下来咱们须要使用 Agora SDK 在应用中添加直播功能。
首先在官网下载最新的 SDK 包并添加到咱们的 Demo 中。接着在 ViewController 中添加 AgoraRtcEngineKit 的实例,而且进行直播相关的设置。
let agoraKit: AgoraRtcEngineKit = {
let engine = AgoraRtcEngineKit.sharedEngine(withAppId: <#Your AppId#>, delegate: nil)
engine.setChannelProfile(.liveBroadcasting)
engine.setClientRole(.broadcaster)
engine.enableVideo()
return engine
}()
复制代码
最后在 viewDidLoad
方法中加入频道。
agoraKit.delegate = self
agoraKit.joinChannel(byToken: nil, channelId: "agoraar", info: nil, uid: 0, joinSuccess: nil)
复制代码
至此,全部的准备工做都已经完成,咱们有了一个能够识别平面的AR应用,同时又能够进行音视频通话,接下来要作的就是把这两个功能结合起来。
由于 ARKit 已经占用了设备摄像头,咱们没法本身启动 AVCaptureSession 进行采集。幸亏 ARFrame
的 capturedImage
接口提供了摄像头采集到的数据能够供咱们直接使用。
为了发送视频数据,咱们须要构造一个实现了 AgoraVideoSourceProtocol
协议的类 ARVideoSource
。其中 bufferType 返回 AgoraVideoBufferTypePixelBuffer
类型。
class ARVideoSource: NSObject, AgoraVideoSourceProtocol {
var consumer: AgoraVideoFrameConsumer?
func shouldInitialize() -> Bool { return true }
func shouldStart() { }
func shouldStop() { }
func shouldDispose() { }
func bufferType() -> AgoraVideoBufferType {
return .pixelBuffer
}
}
复制代码
给这个 ARVideoSource
类添加一个发送视频帧的方法:
func sendBuffer(_ buffer: CVPixelBuffer, timestamp: TimeInterval) {
let time = CMTime(seconds: timestamp, preferredTimescale: 10000)
consumer?.consumePixelBuffer(buffer, withTimestamp: time, rotation: .rotationNone)
}
复制代码
接着在 ViewController 中实例化一个 ARVideoSource
, 并在 viewDidLoad 中经过 setVideoSource
接口设置给 Agora SDK
let videoSource = ARVideoSource()
override func viewDidLoad() {
……
agoraKit.setVideoSource(videoSource)
……
}
复制代码
这样在咱们须要的时候,只要调用 videoSource 的 sendBuffer:timestamp:
方法,就能够把视频帧传给 Agora SDK 了。
咱们能够经过 ARSession 的回调拿到每一帧 ARFrame
,从中读出摄像头的数据,并使用 videoSource 发送出去。
在 viewDidLoad
中设置 ARSession 的回调
sceneView.session.delegate = self
复制代码
实现 ARSessionDelegate
回调,读取每一帧的摄像头数据,并传给 Agora SDK 。
extension ViewController: ARSessionDelegate {
func session(_ session: ARSession, didUpdate frame: ARFrame) {
videoSource.sendBuffer(frame.capturedImage, timestamp: frame.timestamp)
}
}
复制代码
ARFrame
的 capturedImage
是摄像头采集到的原始数据,若是咱们想发送的是已经添加好虚拟物体的画面,那就只能本身获取 ARSCNView
的数据了。这里提供一种简单的思路:设定一个定时器,定时去将 SCNView 转为 UIImage,接着转换为CVPixelBuffer,而后提供给 videoSource。下面只提供了示例逻辑代码。
func startCaptureView() {
// 0.1秒间隔的定时器
timer.schedule(deadline: .now(), repeating: .milliseconds(100))
timer.setEventHandler { [unowned self] in
// 将 sceneView 数据变成 UIImage
let sceneImage: UIImage = self.image(ofView: self.sceneView)
// 转化为 CVPixelBuffer 后提供给 Agora SDK
self.videoSourceQueue.async { [unowned self] in
let buffer: CVPixelBuffer = self.pixelBuffer(ofImage: sceneImage)
self.videoSource.sendBuffer(buffer, timestamp: Double(mach_absolute_time()))
}
}
timer.resume()
}
复制代码
咱们能够先在 AR 场景中添加一个 SCNNode, 接着经过 Metal 把连麦对方的视频数据渲染到 SCNNode 上。这样便可实如今 AR 环境中显示连麦端的画面。
首先咱们须要建立用来渲染远端视频的虚拟显示屏,并经过用户的点击添加到 AR 场景中。
在 Storyboard 中给 ARSCNView 添加一个 UITapGestureRecognizer
,当用户点击屏幕后,经过 ARSCNView
的 hitTest
方法获得在平面上的位置,并把一个虚拟显示屏放在点击的位置上。
@IBAction func doSceneViewTapped(_ recognizer: UITapGestureRecognizer) {
let location = recognizer.location(in: sceneView)
guard let result = sceneView.hitTest(location, types: .existingPlane).first else {
return
}
let scene = SCNScene(named: "art.scnassets/displayer.scn")!
let rootNode = scene.rootNode
rootNode.simdTransform = result.worldTransform
sceneView.scene.rootNode.addChildNode(rootNode)
let displayer = rootNode.childNode(withName: "displayer", recursively: false)!
let screen = displayer.childNode(withName: "screen", recursively: false)!
unusedScreenNodes.append(screen)
}
复制代码
用户经过点击能够添加多个显示屏,并被存在 unusedScreenNodes
数组中待用。
为了从 Agora SDK 获取到远端的视频数据,咱们须要构造一个实现了 AgoraVideoSinkProtocol
协议的类型 ARVideoRenderer
。
class ARVideoRenderer: NSObject {
var renderNode: SCNNode?
}
extension ARVideoRenderer: AgoraVideoSinkProtocol {
func shouldInitialize() -> Bool { return true }
func shouldStart() { }
func shouldStop() { }
func shouldDispose() { }
func bufferType() -> AgoraVideoBufferType {
return .rawData
}
func pixelFormat() -> AgoraVideoPixelFormat {
return .I420
}
func renderRawData(_ rawData: UnsafeMutableRawPointer, size: CGSize, rotation: AgoraVideoRotation) {
……
}
}
复制代码
经过 renderRawData:size:rotation:
方法能够拿到远端的视频数据,而后就可使用 Metal 渲染到 SCNNode 上。具体的 Metal 渲染代码能够参考文末的完整版 Demo.
经过实现 AgoraRtcEngineDelegate
协议的 rtcEngine:didJoinedOfUid:elapsed:
回调,能够获得连麦者加入频道的事件。在回调中建立 ARVideoRenderer
的实例,把前面用户经过点击屏幕建立的虚拟显示屏 Node 设置给 ARVideoRenderer,最后经过 setRemoteVideoRenderer:forUserId:
接口把自定义渲染器设置给 Agora SDK。
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
guard !unusedScreenNodes.isEmpty else {
return
}
let screenNode = unusedScreenNodes.removeFirst()
let renderer = ARVideoRenderer()
renderer.renderNode = screenNode
agoraKit.setRemoteVideoRenderer(renderer, forUserId: uid)
}
复制代码
这样当连麦端加入频道后,就会在虚拟显示屏上显示对方的视频,获得一个虚拟会议室的效果,正如咱们在文章开头所看到的。
用最新 2.1 版 Agora SDK 的自定义视频源和自定义视频渲染器接口,能够轻松地把 AR 和直播场景结合起来。Demo 基于 Agora SDK 以及 SD-RTN™ 运行,能够支持17人的同时视频连麦。能够预见,AR 技术会为实时视频连麦带来全新的体验。
完整 Demo 请见 Github
如遇到开发问题,欢迎访问声网 Agora问答版块,发帖与咱们的工程师交流。