AR實踐:基於ARKit實現電影中的全息視訊會議

聲網Agora發表於2018-03-14

作者簡介:龔宇華,聲網Agora.io 首席iOS研發工程師,負責iOS端移動應用產品設計和技術架構。

去年中旬,蘋果在 WWDC2017 推出了 ARKit。通過它,開發者可以更加快速地在 iOS 平臺開發 AR 應用,利用鏡頭將虛擬照進現實。最近蘋果還增強了 iOS 系統對 ARKit的支援,並將加大對 AR 應用的推廣力度。

在本篇中,我們將會把 ARKit 融入視訊會議場景中。本文將會介紹視訊中兩種場景的實現:

  • 將 ARKit 融入直播中

  • 將直播連麥的對方畫面渲染到 AR 場景中

我們將一起在直播場景中利用 ARKit 實現平面檢測,還將應用到 Agora SDK 2.1 的新功能“自定義視訊源與渲染器”。如果你在此之前還未了解過 ARKit 的基本類及其原理,可以先閱讀《上篇:ARKit 基礎知識》。

不多說,先上效果圖。儘管距離電影中看到的全息視訊會議效果還有距離,但大家可以試著對後期效果優化無限接近電影場景(文末有原始碼)。我們在這裡僅分享利用 AR 在視訊會議中的實現技巧。

AR實踐:基於ARKit實現電影中的全息視訊會議

準備工作1:基礎的AR功能

我們首先使用ARKit建立一個簡單的識別平面的應用做為開發基礎。

在Xcode中使用 Augmented Reality App 模版建立一個新專案,其中 Content Technology 選擇 SceneKit.

啟動平面檢測

在 ViewController 中設定 ARConfiguration 為平面檢測。

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應用,當識別出環境中的平面時,會在上面新增一個紅色的矩形,並漸隱消失。

AR實踐:基於ARKit實現電影中的全息視訊會議

準備工作2:基礎的直播功能

接下來我們需要使用 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 = selfagoraKit.joinChannel(byToken: nil, channelId: "agoraar", info: nil, uid: 0, joinSuccess: nil)複製程式碼

至此,所有的準備工作都已經完成,我們有了一個可以識別平面的AR應用,同時又可以進行音視訊通話,接下來要做的就是把這兩個功能結合起來。

將 ARKit 的畫面直播出去

因為 ARKit 已經佔用了裝置攝像頭,我們無法自己啟動 AVCaptureSession 進行採集。幸好 ARFramecapturedImage 介面提供了攝像頭採集到的資料可以供我們直接使用。

新增自定義視訊源

為了傳送視訊資料,我們需要構造一個實現了 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)
}
}複製程式碼

傳送 ARSCNView 資料

ARFramecapturedImage 是攝像頭採集到的原始資料,如果我們想傳送的是已經新增好虛擬物體的畫面,那就只能自己獲取 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 場景中

我們可以先在 AR 場景中新增一個 SCNNode, 接著通過 Metal 把連麥對方的視訊資料渲染到 SCNNode 上。這樣即可實現在 AR 環境中顯示連麥端的畫面。

新增虛擬螢幕

首先我們需要建立用來渲染遠端視訊的虛擬螢幕,並通過使用者的點選新增到 AR 場景中。

在 Storyboard 中給 ARSCNView 新增一個 UITapGestureRecognizer,當使用者點選螢幕後,通過 ARSCNViewhitTest 方法得到在平面上的位置,並把一個虛擬螢幕放在點選的位置上。

@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.

將自定義渲染器設定給 Agora SDK

通過實現 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 請見 聲網Agora 開發者社群Github

來源:https://juejin.im/post/5aa760855188255568686b51

相關文章