创建带有AR面部滤镜的实时流媒体视频APP

在过去的十年中,直播已逐渐成为最受欢迎的娱乐形式之一。越来越多的人喜欢通过直播向公众分享他们的生活。

如今,有很多流媒体APP可供选择,例如 Twitch、Facebook Live 和 Youtube Live。这些APP提供了许多有趣的附加功能,例如面部过滤器和语音更改选项。但是你有想过自己创建一个流媒体APP吗?你是否因为其中一些繁冗复杂的功能而踌躇不前?

幸运的是,Agora Video SDKBanuba Face AR SDK使开发带有人脸滤镜的流媒体APP变得快速而简单。今天,我将展示如何在 iOS 上做到这一点。

要求

  1. 基本了解 Swift 和 iOS SDK 基本知识。

  2. Agora.io 开发者账号

  3. Xcode 和 iOS 设备。

  4. CocoaPods(如果你还没有安装 CocoaPods,你可以在这里找到说明)。

  5. Banuba Face AR 演示APP

概述

本指南将介绍使用 Agora SDK 和 Banuba Face AR 在 iOS 上创建直播APP的步骤。以下是我们APP需要的核心功能列表:

  • 用户(流和观众)可以创建并登录到他们的帐户。用户帐户信息将保存在 Google Firebase 实时数据库中。
  • 用户可以设置虚拟房间来主持直播并成为主播。
  • 用户可以找到所有直播流并作为观众加入虚拟房间。
  • 主播可以使用面部滤镜功能通过虚拟面具或动画进行直播。
  • 虚拟房间中的观众可以发送文本消息,房间中的所有人都可以看到这些消息。
  • 用户可以通过他们的名字搜索其他用户并向他们发送私人短信。

你可以找到我的 演示APP作为本文的参考。

下载 Banuba FaceAR 演示

创建APP最简单方法是在现有的Banuba FaceAR APP之上创建。该软件包有一个用于演示SDK的默认 Banuba演示APP。下载APP且解压后,按照 Banuba 的在线说明进行设置。需注意以下有关演示APP的信息:

  • 你将需要一个 Banuba 客户端令牌来运行该项目。点击info@banuba.com了解更多信息。

设置Banuba容易遗漏的提示:
在第 6 步中,确保标记 BanubaEffectPlayer.framework 为“嵌入并签名”。
在第 8 步中,确保在添加效果文件夹时选择“创建文件夹引用”。

设置完后,最好编译 BanubaSdk 框架目标(确保将设备设置为通用 iOS 设备)以创建最新版本的 Banuba SDK。然后,你需要将刚刚创建的框架复制到放置BanubaEffectPlayer.framework 的相同位置,并以相同的方式将其添加为嵌入式库。

设置后的常规设置

重新编译 SDK 后,运行 BanubaSdkApp 目标,你应该看到以下内容:

可以在此处找到此屏幕 UI 的详细信息

This will be our streamer view. However, we’re going to need to do some work to get these face filters broadcast out to an audience. This is where the Agora Video SDK comes in handy.

这将是我们的主播视图。但是,我们需要做一些其他操作才能将这些面部滤镜直播给观众。这就到了Agora Video SDK的用武之地。

设置 CocoaPods

  1. 在终端中,导航到包含BanubaSdkApp.xcodeproj 项目的目录并运行pod init 以初始化 CocoaPods。这应该是src/BanubaSdk/BanubaSdkApp
  2. 打开创建的 Podfile 并为 Agora 库以及我们将用于用户管理的 Firebase 库添加 Pod,:
platform :ios, '12.0'

target 'BanubaSdkApp' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for BanubaSdkApp
  pod 'AgoraUIKit'
  pod 'AgoraRtm_iOS'
  pod 'Firebase/Analytics'
  pod 'Firebase/Auth'
  pod 'Firebase/Database'
  pod 'FirebaseUI'

end

3.在终端中运行pod install 以安装库。

  1. 打开BanubaSdkApp.xcworkspace。

  2. 打开 Build Settings 并搜索“Framework Search Paths”。替换第一个搜索带有$(inherited 的搜索路径 "$(SRCROOT)/../../../../build" 以确保 Xcode 可以找到 Cocoapod 框架。

框架搜索路径

设置 Firebase

转到https://console.firebase.google.com并创建一个新的 Firebase 项目。按照说明在现有APP中设置 Firebase。我们将使用 Firebase 进行身份验证、分析和用户管理。

完成 Firebase 的设置后,你应该已完成以下步骤:

  1. 用 Firebase 注册APP的 Bundle ID(提醒一下,你可以在项目设置中General 下找到 Bundle ID)。
  2. 下载GoogleService-Info.plist 文件并将其添加到APP。
  3. 将 Firebase 导入 AppDelegate,并在FirebaseApp.configure() 中调用didFinishLaunchingWithOptions .
  4. 运行APP让 Firebase 验证通信。

然后,你将看到 Firebase 仪表板。单击左侧导航中的Develop ,然后单击Authentication 。单击设置登录方法 并启用电子邮件/密码Google 登录选项。注意,你需要设置面向公众的APP名称和支持电子邮件才能这样做。

在 Xcode 中,你还需要设置 URL 方案来处理 Google 登录。从GoogleService-Info.plist 复制REVERSED_CLIENT_ID 字段,然后打开项目设置的信息 部分中的URL 类型 窗格:

URL 方案

添加新的 URL 类型并将反向的客户端 ID 粘贴到 URL Schemes 字段中。我们还需编写一些代码,以便APP知道如何处理该 URL。我们将使用 Firebase UI,因此对我们来说,这就像告诉 Firebase 处理它一样简单。将以下内容添加到你的AppDelegate.swift :

import FirebaseUI


func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
   let sourceApplication = options[UIApplication.OpenURLOptionsKey.sourceApplication] as! String?
   if FUIAuth.defaultAuthUI()?.handleOpen(url, sourceApplication: sourceApplication) ?? false {
       return true
   }


   return false
}

注意:Firebase 提供了其他几个你希望得到允许的登录选项,但我们在此处不会做过多介绍。

设置视图

视图布局

打开Main.storyboard 。它已经包含了 Banuba 演示的视图,所以我们不用太过介意。我们将围绕它再添加一些屏幕来包含我们的新功能。我们需要一个带有三个视图的Tab Bar Controller :一个用于我们的实时流列表,一个用于聊天,另一个用于设置和其他作用(我们只是在本演示中用作占位符)。将 Tab Bar Controller 设置为新的 Initial View Controller。

对于第一个选项卡,我们需要一个导航控制器和一个带有按钮的表视图控制器,以便与你的个人流一起上线。该按钮应显示带有 segue 的现有 Banuba 屏幕。确保给它一个标识符,我们很快就会用上它。

做一个简单的TableViewCell

class LiveTableViewCell: UITableViewCell {


   @IBOutlet weak var nameLabel: UILabel!


}

最后,确保给单元原型一个重用标识符。

现在,将聊天视图控制器留空;我们稍后再谈。

使用 FirebaseUI 登录

在本教程中,我们将使用 Firebase 的内置 UI 来为我们处理登录。如果你想使用自己的登录页面,或者只是想让你的 UI 更灵活,你可以分别 在此处 此处找到使用电子邮件和 Google 以编程方式登录的文档。

我们将使用 FirebaseUI 登录到我们的APP。我们将拥有我们的标签栏控制器——我将其命名为MainScreenViewController ——处理显示默认的 FUIAuth 视图控制器。我们需要做的就是告诉它我们要允许哪些用户使用,以及在用户成功登录时需要告诉哪些人:

import FirebaseAuth
import FirebaseUI


extension MainScreenViewController: FUIAuthDelegate {
   func showFUIAuthScreen() {
       let authUI = FUIAuth.defaultAuthUI()
       authUI?.delegate = self


       let providers: [FUIAuthProvider] = [
           FUIGoogleAuth(),
           FUIEmailAuth()
       ]
       authUI?.providers = providers


       if let authViewController = authUI?.authViewController() {
           self.present(authViewController, animated: false)
       }
   }


   func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?) {
       //TODO
   }
}

我们可以在启动时调用这个函数,但是每次打开APP时都必须重新登录会很麻烦。要解决这个问题,我们可以使用 FirebaseAuth 提供给我们的东西——AuthStateDidChangeListener . 每当用户的身份验证状态发生变化时,它都会提醒我们,并允许我们仅在没有用户登录的情况下显示登录页面。添加这个功能非常简单:

var handle: AuthStateDidChangeListenerHandle?


override func viewWillAppear(_ animated: Bool) {
   handle = Auth.auth().addStateDidChangeListener { (auth, user) in
       if user == nil {
           self.showFUIAuthScreen()
       }
   }
}


override func viewWillDisappear(_ animated: Bool) {
   if let handle = handle {
       Auth.auth().removeStateDidChangeListener(handle)
   }
}

现在我们有一个功能性的登录页面,如果当前用户为零,就会显示该页面。

创建用户数据库

Firebase 将为我们跟踪我们的用户——一旦你与用户一起登录,你就可以在 Firebase 仪表板的“身份验证”选项卡上亲眼看到这一点。但是,这个用户列表对我们来说用处不大。虽然我们可以从中获取有关当前登录用户的信息,但它不允许我们获取其他用户的信息。为此,我们需要自己建一个数据库。

转到 Firebase 仪表板上的数据库选项卡,然后创建一个新的实时数据库。现在在测试模式下启动它,这样我们就可以轻松修改而不用担心在处理它时的安全性。我们可以在这里手动添加数据,但在代码中自动添加会更容易。

在登录时添加用户

回到我们的FUIAuthDelegate 分机。我们将利用该didSignInWith 回调使其在用户登录时将其添加到我们的数据库中:

func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?) {
   if let error = error {
       print(error.localizedDescription)
   } else {


       //Save the user to our list of users.
       if let user = authDataResult?.user {
           let ref = Database.database().reference()
           ref.child("users").child(user.uid).setValue(["username" : user.displayName?.lowercased(),
                                                        "displayname" : user.displayName,
                                                        "email": user.email])
       }
   }
}

此代码获取对我们主数据库的引用,并在新的“用户”节点中添加一个条目。节点的每个子节点都需要有一个唯一的键,因此我们使用 Firebase 提供的唯一 UID,并存储用户的电子邮件、他们的显示名称以及他们的显示名称的小写版本,以便于日后搜索。

注意,此代码将在用户每次登录时覆盖我们的用户节点。如果你想向我们的用户数据库添加其他字段,那你需要调整此代码,保证其不会删除内容。

上线

GoLiveViewController 中, 我们需要确保我们有对当前用户的引用,以便我们知道谁在进行直播,我们可以使用与登录相同的方法。

var handle: AuthStateDidChangeListenerHandle?
var currentUser: User?


override func viewWillAppear(_ animated: Bool) {
   handle = Auth.auth().addStateDidChangeListener { (auth, user) in
       if let user = user {
           self.currentUser = user
       }
   }
}


override func viewWillDisappear(_ animated: Bool) {
   if let handle = handle {
       Auth.auth().removeStateDidChangeListener(handle)
   }
}

当我们点击 Go Live 按钮时,我们希望将用户名传递到 Banuba 屏幕,以便我们可以进入我们的个人频道。我们还需要确保保存我们的位置,以便其他用户可以观看流。我们将使用 Firebase 数据库中的另一个节点来做到这一点。

var liveRef: DatabaseReference!


override func viewDidLoad() {
   super.viewDidLoad()


   // Do any additional setup after loading the view.
   liveRef = Database.database().reference(withPath: "live")
}


override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
   if segue.identifier == "GoLive" {
       liveRef.child(userName).setValue("true")
       if let banubaView = segue.destination as? ViewController {
           banubaView.roomName = sender as? String
       }
   }
}

我们还没有在 Banuba 视图控制器中创建一个 roomName 变量,但很快就会用到了。

当用户点击 Go Live 按钮时,我们会将他们的用户名添加到当前在线的用户列表中。然后我们将通过从该列表中读取信息来更新我们的表视图。

var liveUsers = [String]()


override func viewWillAppear(_ animated: Bool) {
   handle = Auth.auth().addStateDidChangeListener { (auth, user) in
       if let user = user {
           self.currentUser = user
           self.liveRef.observe(.childAdded) { (snapshot) in
               self.liveUsers.append(snapshot.key)
               self.tableView.insertRows(at: [IndexPath(row: self.liveUsers.count-1, section: 0)], with: .automatic)
           }
           self.liveRef.observe(.childRemoved) { (snapshot) in
               let name = snapshot.key
               if let index = self.liveUsers.firstIndex(of: name) {
                   self.liveUsers.remove(at: index)
                   self.tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
               }
           }
       }
   }
}


override func viewWillDisappear(_ animated: Bool) {
   if let handle = handle {
       Auth.auth().removeStateDidChangeListener(handle)
       liveUsers.removeAll()
       tableView.reloadData()
   }
}


func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return liveUsers.count
}


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let cell = tableView.dequeueReusableCell(withIdentifier: "liveCell", for: indexPath)


   if let liveCell = cell as? LiveTableViewCell {
       liveCell.nameLabel.text = liveUsers[indexPath.row]
   }


   return cell
}

当实时用户列表发生变化时,我们利用数据库观察器来获得通知,然后根据需要更新我们的表。当我们设置观察器时,它会立即为每个已经在线的用户提供一个childAdded 事件,因此不需要额外的案例。

连接Banuba到 Agora

这是该APP的核心所在。我们需要将 Banuba 生成的视频帧直播给我们的观众。为此,我们将创建一个自定义视频源,该源负责获取 Banuba 生成的帧,并以它可以理解的方式将它们传递给 Agora 引擎。创建一个符合AgoraVideoSourceProtocol 要求的新类CustomVideoSource

import AgoraRtcKit


/**
A custom video source for the AgoraRtcEngine. This class conforms to the AgoraVideoSourceProtocol and is used to pass the AR pixel buffer as a video source of the Agora stream.
*/
class CustomVideoSource: NSObject, AgoraVideoSourceProtocol {
   var consumer: AgoraVideoFrameConsumer?
   var rotation: AgoraVideoRotation = .rotation180 //Banuba frames are flipped compared to what Agora expects.


   func shouldInitialize() -> Bool { return true }


   func shouldStart() { }


   func shouldStop() { }


   func shouldDispose() { }


   func bufferType() -> AgoraVideoBufferType {
       return .pixelBuffer
   }


   func captureType() -> AgoraVideoCaptureType {
       return .camera
   }


   func contentHint() -> AgoraVideoContentHint {
       return .motion
   }


   func sendBuffer(_ buffer: CVPixelBuffer, timestamp: TimeInterval) {
       let time = CMTime(seconds: timestamp, preferredTimescale: 1000)
       consumer?.consumePixelBuffer(buffer, withTimestamp: time, rotation: rotation)

大多数函数都不需要太多存根。真正的内容是sendBuffer 函数,它将接收一个CVPixelBuffer 输入,并将其传递给 Agora。

现在是时候开始编辑 Banuba 的ViewController.swift 文件了。我们需要设置一些变量:

import AgoraRtcKit
import FirebaseDatabase


var roomName: String?
var agoraKit: AgoraRtcEngineKit?
var videoSource = CustomVideoSource()

然后我们添加一个函数来初始化 Agora:

func initializeAgoraEngine() {
   agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: "YOUR_AGORA_APP_ID", delegate: self)




   agoraKit?.enableVideo() // - enable video
   agoraKit?.setVideoSource(self.videoSource) // - set the video source to the custom source


   agoraKit?.enableWebSdkInteroperability(true)
   agoraKit?.setChannelProfile(.liveBroadcasting)
   if let channel = roomName {
       agoraKit?.joinChannel(byToken: nil, channelId: channel, info: nil, uid: 0, joinSuccess: { (channel, uid, elapsed) in
           print("Join channel success")
       })
   }
}

确保将AgoraRtcEngineDelegate 协议添加到 ViewController 类。我们实际上演示中并不需要它,但如果我们想扩展APP的功能,它可能会派上用场。

然后我们告诉 Banuba 将帧发送到viewDidLoad

initializeAgoraEngine()
sdkManager.output?.startForwardingFrames(handler: { (buffer) in
   self.videoSource.sendBuffer(buffer, timestamp: Date().timeIntervalSinceReferenceDate)
})

最后,确保我们在离开屏幕时清理我们的直播:

deinit {
   if let userName = roomName {
       Database.database().reference(withPath: "live").child(userName).removeValue()
       agoraKit?.leaveChannel(nil)
   }
}

加入观众

我们现在将精美的 AR 帧发送到 Agora 频道,但我们无法查看它们。幸运的是,使用AgoraUIKit 可以,我们甚至不需要布局任何视图。

回到我们的GoLiveViewController ,当用户点击进入直播时添加一个处理程序:

import AgoraUIKit


func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
   let userName = liveUsers[indexPath.row]


   let agoraAudienceView = AgoraVideoViewController(appID: "YOUR_AGORA_APP_ID", token: nil, channel: userName)
   agoraAudienceView.setMaxStreams(streams: 1)
   agoraAudienceView.setIsAudience()
   agoraAudienceView.hideVideoMute() // Hide buttons we don't need
   agoraAudienceView.hideAudioMute()
   agoraAudienceView.hideSwitchCamera()
   agoraAudienceView.controlOffset = 60 // Make sure the controls are above the tab bar


   navigationController?.pushViewController(agoraAudienceView, animated: true)


   tableView.deselectRow(at: indexPath, animated: true)
}

这就是全部内容。AgoraUIKit 将为我们完成所有繁重的工作。如果你现在试用该APP,你应该能够登录,开始使用面部滤镜进行直播,并在另一台设备上查看该直播。

添加聊天

我们将使用 Agora 的实时消息 (RTM) SDK 来允许用户在进行视频通话时相互聊天。首先,让我们设置一些新视图。

用三个新视图替换我们APP的聊天部分:

  • 导航控制器。

  • 用于搜索用户聊天的视图。这需要一个UISearchBar 和一个UITableView 。确保将搜索栏的委托、 tableview 的委托和数据源连接到视图控制器。我们还需要一个带有单个标签的原型单元格,确保给它一个重用标识符。

  • 聊天视图。这需要一个UITableView 和一个UITextField 。记住连接它们的委托和表的数据源。制作另一个原型单元格,也带有单个标签。

搜索用户

将你的第一个原型单元连接到一个简单的UITableViewCell 子类:

class UserTableViewCell: UITableViewCell {


   @IBOutlet weak var displayName: UILabel!


}

然后我们将创建一个新UserSearchViewController 类。确保你在创建类时在Main.storyboard 中设置了自定义类。我们首先进行标准设置以获取对用户的引用以及对用户数据库的引用。

var userRef: DatabaseReference!
var resultsArray = [[String:String]]()


var handle: AuthStateDidChangeListenerHandle?
var currentUser: User?


override func viewDidLoad() {
   super.viewDidLoad()


   // Do any additional setup after loading the view.
   userRef = Database.database().reference(withPath: "users")
}


override func viewWillAppear(_ animated: Bool) {
   handle = Auth.auth().addStateDidChangeListener { (auth, user) in
       self.currentUser = user
       if user == nil {
           //Handle logout
       }
   }
}


override func viewWillDisappear(_ animated: Bool) {
   if let handle = handle {
       Auth.auth().removeStateDidChangeListener(handle)
   }
}

然后,当用户搜索时,我们执行数据库查询以获取匹配用户的列表:

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
   resultsArray.removeAll()
   tableView.reloadData()
}


func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
   if let searchText = searchBar.text?.lowercased(), searchText != "" {
       resultsArray.removeAll()
       queryText(searchText, inField: "username")
   } else {
       let alert = UIAlertController(title: "Error", message: "Please enter a username.", preferredStyle: .alert)
       alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
       present(alert, animated: true, completion: nil)
   }
}


func queryText(_ text: String, inField child: String) {
   userRef.queryOrdered(byChild: child)
       .queryStarting(atValue: text)
       .queryEnding(atValue: text+"\u{f8ff}")
       .observeSingleEvent(of: .value) { [weak self] (snapshot) in
           for case let item as DataSnapshot in snapshot.children {
               //Don't show the current user in search results
               if self?.currentUser?.uid == item.key {
                   continue
               }


               if var itemData = item.value as? [String:String] {
                   itemData["uid"] = item.key
                   self?.resultsArray.append(itemData)
               }
           }
           self?.tableView.reloadData()
   }
}

提示:如果你没有多部手机进行测试,你可以直接在 Firebase 控制台中将虚拟用户添加到你的数据库中。

然后我们需要显示我们找到的用户:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return resultsArray.count
}


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let cell = tableView.dequeueReusableCell(withIdentifier: "userCell", for: indexPath)


   if let userCell = cell as? UserTableViewCell {
       let userData = resultsArray[indexPath.row]
       userCell.displayName.text = userData["displayname"]
   }


   return cell
}

如果你要运行APP并搜索其他用户,他们现在将出现在你的搜索中。但是,你可能还会注意到 Firebase 在控制台中发出警示:

[Firebase/数据库][I-RDB034028] 使用未指定的索引。你的数据将在客户端下载和过滤。考虑将 /users 处的“.indexOn”:“username”添加到你的安全规则中以获得更好的性能

因为我们没有对Firebase发出命令,所以这是 Firebase显示它没有通过我们的搜索字段为我们的用户编制索引。我们现在拥有的用户很少,没关系,但是如果我们想向庞大的用户群发布,我们应该解决这个问题。

幸运的是,添加规则很容易。前往 Firebase 仪表板中的数据库选项卡,然后打开规则。将 .indexOn 字段添加到你的用户数据库并点击发布:

最后,我们需要在选择用户时实际显示聊天视图。在Main.storyboard 中为聊天视图创建手动转场并为其命名。然后我们可以进行切换并将我们想要聊天的人的名字传递给下一个视图。

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  if let friendID = resultsArray[indexPath.row]["displayname"] {
      performSegue(withIdentifier: "showChat", sender: friendID)
  }


  tableView.deselectRow(at: indexPath, animated: true)
}


override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if segue.identifier == "showChat" {
      if let friendID = sender as? String, let destination = segue.destination as? ChatViewController {
          destination.friendID = friendID
      }
  }
}

发送消息

我们的最终画面是我们的用户可以真正相互聊天的地方。首先,我们需要初始化 Agora 的 RTM 模块,然后登录:

var agoraRtm: AgoraRtmKit?
var friendID: String?


var handle: AuthStateDidChangeListenerHandle?
var currentUser: User?


var messageList: [String] = []


override func viewDidLoad() {
   super.viewDidLoad()


   agoraRtm = AgoraRtmKit(appId: "YOUR_AGORA_APP_ID", delegate: self)


   tableView.register(UITableViewCell.self, forCellReuseIdentifier: "chatCell")
}


override func viewWillAppear(_ animated: Bool) {
   handle = Auth.auth().addStateDidChangeListener { (auth, user) in
       self.currentUser = user
       if let username = user?.displayName, let friendName = self.friendID {
           self.navigationItem.title = friendName
           self.agoraRtm?.login(byToken: nil, user: username, completion: { (error) in
               if error != .ok {
                   print("error logging in")
               }
           })
       } else {
           //Handle logout
       }
   }
}


override func viewWillDisappear(_ animated: Bool) {
   if let handle = handle {
       Auth.auth().removeStateDidChangeListener(handle)
   }
}

当我们的用户输入一条消息时,我们使用 RTM 发送它并将其添加到我们的表视图中:

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
   if let text = textField.text, text != "" {
       let option = AgoraRtmSendMessageOptions()
       option.enableOfflineMessaging = true
       agoraRtm?.send(AgoraRtmMessage(text: text), toPeer: friendID!, sendMessageOptions: option, completion: { (error) in
           if error == .ok || error == .cachedByServer {
               self.addMessage(user: self.currentUser!.displayName ?? self.currentUser!.uid, message: text)
           } else {
               print("Failed to send message: ", error)
           }
       })
       textField.text = ""
   }
   return true
}


func addMessage(user: String, message: String) {
   let message = "\(user): \(message)"
   messageList.append(message)
   let indexPath = IndexPath(row: self.messageList.count-1, section: 0)
   self.tableView.insertRows(at: [indexPath], with: UITableView.RowAnimation.automatic)
   self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}


func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   messageList.count
}


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let cell = tableView.dequeueReusableCell(withIdentifier: "chatCell", for: indexPath)


   let message = messageList[indexPath.row]
   cell.textLabel?.text = message
   cell.textLabel?.numberOfLines = 0




   return cell
}

然后我们实行AgoraRtmDelegate 接收并显示我们正在聊天的用户的消息:

extension ChatViewController: AgoraRtmDelegate {
   func rtmKit(_ kit: AgoraRtmKit, messageReceived message: AgoraRtmMessage, fromPeer peerId: String) {
       if peerId == friendID {
           addMessage(user: peerId, message: message.text)
       }
   }
}

处理键盘

如果你现在尝试测试该APP,你会立即发现一个问题:我们的文本字段位于屏幕底部,当你选择它时键盘将其覆盖。下面让我们解决这个问题。

@IBOutlet weak var bottomConstraint: NSLayoutConstraint!


override func viewDidLoad() {
   super.viewDidLoad()


   ...


   NotificationCenter.default.addObserver(self, selector: #selector(ChatViewController.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
   NotificationCenter.default.addObserver(self, selector: #selector(ChatViewController.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
   textField.becomeFirstResponder()
}


@objc func keyboardWillShow(notification: NSNotification) {
   guard let userInfo = notification.userInfo else { return }
   guard let keyboardSize = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }


   let keyboardFrame = keyboardSize.cgRectValue


   bottomConstraint.constant = 20 + keyboardFrame.height
}


@objc func keyboardWillHide(notification: NSNotification) {
   bottomConstraint.constant = 60
}

在这里,我们添加对 NSLayoutConstraint 的引用,将文本字段附加到屏幕底部。使用通知中心,我们可以找出键盘何时进行显示或隐藏,并自动调整我们的文本字段离屏幕底部的距离。

频道聊天

除了直接消息之外,如果我们的观众可以在观看直播的同时与房间里的其他人聊天,那就太好了。我们目前使用 AgoraUIKit 来处理没有内置 RTM 的观众。不过我们可以解决这个问题,因为我们可以将AgoraVideoViewController 编入子集。

创建一个新文件,我们将实现与一对一聊天性质相同的事情。

import UIKit
import AgoraUIKit
import AgoraRtmKit
import FirebaseAuth


class CustomAgoraViewController: AgoraVideoViewController, UITableViewDelegate, UITableViewDataSource, AgoraRtmDelegate {


   var chatTableView: UITableView!


   var agoraRtm: AgoraRtmKit?
   var rtmChannelName: String?
   var rtmChannel: AgoraRtmChannel?
   var bottomConstraint: NSLayoutConstraint?


   var handle: AuthStateDidChangeListenerHandle?
   var currentUser: User?


   var messageList: [String] = []


   override func viewWillAppear(_ animated: Bool) {
       super.viewWillAppear(animated)
       handle = Auth.auth().addStateDidChangeListener { (auth, user) in
           self.currentUser = user
           if let user = user, let channelName = self.rtmChannelName {
               self.agoraRtm = AgoraRtmKit.init(appId: "YOUR_AGORA_APP_ID", delegate: self)
               self.agoraRtm?.login(byToken: nil, user: user.displayName ?? user.uid) { (error) in
                   if error != .ok {
                       print("Error logging in: ", error.rawValue)
                   } else {
                       self.rtmChannel = self.agoraRtm?.createChannel(withId: channelName, delegate: self)
                       self.rtmChannel?.join(completion: { (error) in
                           if error != .channelErrorOk {
                               print("Error joining channel: ", error.rawValue)
                           }
                       })
                   }
               }
           } else {
               DispatchQueue.main.async {
                   self.dismiss(animated: true, completion: nil)
               }
           }
       }
   }


   override func viewWillDisappear(_ animated: Bool) {
       super.viewWillDisappear(animated)
       if let handle = handle {
           Auth.auth().removeStateDidChangeListener(handle)
       }
       if let channel = self.rtmChannel {
           channel.leave(completion: nil)
       }
   }
}

获取我们的用户这个步骤应该很熟悉了,此外,我们正在设置 Agora RTM。在这里,我们使用主播createChannel 的名称进行调用,如果频道不存在,它将另创建一个频道,如果存在,则会直接提供给我们createChannel 。然后我们将加入该频道并将向其发送消息,而不是发送给特定用户。

override func viewDidLoad() {
   super.viewDidLoad()


   // Do any additional setup after loading the view.
   chatTableView = UITableView(frame: .zero)
   view.addSubview(chatTableView)


   chatTableView.delegate = self
   chatTableView.dataSource = self
   chatTableView.backgroundColor = UIColor.clear


   chatTableView.translatesAutoresizingMaskIntoConstraints = false


   chatTableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
   chatTableView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.4).isActive = true
   chatTableView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5).isActive = true


   chatTableView.register(UITableViewCell.self, forCellReuseIdentifier: "chatCell")
   chatTableView.keyboardDismissMode = .onDrag
   collectionView.keyboardDismissMode = .interactive


   let chatField = UITextField(frame: .zero)
   view.addSubview(chatField)
   chatField.delegate = self
   chatField.backgroundColor = UIColor.white


   chatField.translatesAutoresizingMaskIntoConstraints = false


   chatField.topAnchor.constraint(equalTo: chatTableView.bottomAnchor).isActive = true
   chatField.leftAnchor.constraint(equalTo: chatTableView.leftAnchor).isActive = true
   chatField.widthAnchor.constraint(equalTo: chatTableView.widthAnchor).isActive = true
   chatField.heightAnchor.constraint(equalToConstant: 40).isActive = true
   bottomConstraint = view.bottomAnchor.constraint(equalTo: chatField.bottomAnchor, constant: 110)
   bottomConstraint?.isActive = true


   NotificationCenter.default.addObserver(self, selector: #selector(ChatViewController.keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
   NotificationCenter.default.addObserver(self, selector: #selector(ChatViewController.keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
}

在我们的 中viewDidLoad ,我们添加了处理聊天所需的表格视图和文本字段,并设置了一些编程约束,使其保持在正确的位置。我们还进行了与之前相同的通知中心设置,因此我们可以在这里正确处理键盘。

我们的表格视图和键盘设置没有太大变化:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   messageList.count
}


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let cell = tableView.dequeueReusableCell(withIdentifier: "chatCell", for: indexPath)


   let message = messageList[indexPath.row]
   cell.textLabel?.text = message
   cell.textLabel?.numberOfLines = 0
   cell.backgroundColor = .clear
   cell.textLabel?.textColor = .systemBlue


   return cell
}


@objc func keyboardWillShow(notification: NSNotification) {
   guard let userInfo = notification.userInfo else { return }
   guard let keyboardSize = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }


   let keyboardFrame = keyboardSize.cgRectValue


   bottomConstraint?.constant = 20 + keyboardFrame.height
}


@objc func keyboardWillHide(notification: NSNotification) {
   bottomConstraint?.constant = 110
}

我们处理消息的方式大致相同,尽管接收通道消息的回调稍有差异:

extension CustomAgoraViewController: UITextFieldDelegate {
   func addMessage(user: String, message: String) {
       let message = "\(user): \(message)"
       messageList.append(message)
       let indexPath = IndexPath(row: self.messageList.count-1, section: 0)
       self.chatTableView.insertRows(at: [indexPath], with: UITableView.RowAnimation.automatic)
       self.chatTableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
   }


   func textFieldShouldReturn(_ textField: UITextField) -> Bool {
       if let text = textField.text, text != "" {
           rtmChannel?.send(AgoraRtmMessage(text: text), completion: { (error) in
               if error != .errorOk {
                   print("Failed to send message: ", error)
               } else {
                   self.addMessage(user: self.currentUser!.displayName ?? self.currentUser!.uid, message: text)
               }
           })
           textField.text = ""
       }
       return true
   }
}


extension CustomAgoraViewController: AgoraRtmChannelDelegate {
   func channel(_ channel: AgoraRtmChannel, messageReceived message: AgoraRtmMessage, from member: AgoraRtmMember) {
       addMessage(user: member.userId, message: message.text)
   }
}

最后,我们需要更新代码并在我们的GoLiveViewController 中初始化观众视图。

let agoraAudienceView = CustomAgoraViewController(appID: "YOUR_AGORA_APP_ID", token: nil, channel: userName)
agoraAudienceView.setMaxStreams(streams: 1)
agoraAudienceView.setIsAudience()
agoraAudienceView.hideVideoMute()
agoraAudienceView.hideAudioMute()
agoraAudienceView.hideSwitchCamera()
agoraAudienceView.controlOffset = 60


agoraAudienceView.rtmChannelName = userName

结论

大功告成!我们现在就已经有一个配有 AR 面部滤镜的直播APP了。如果你想查看更多功能或有任何疑问,可以随时发表评论或发送电子邮件至 devrel@agora.io。祝你直播快乐!

原文作者 Zontan Fotland
原文链接 https://medium.com/agora-io/create-a-live-streaming-video-app-with-augmented-reality-7930be4ed9ae

推荐阅读
相关专栏
SDK 教程
164 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。