本文介绍如何集成火山引擎 RTC SDK,并实现实时音视频通话。根据如下步骤操作,即可从 0 开始构建一个简单的音视频通话应用。
你也可以参考示例项目,了解更完整的项目实现。
在开始集成 RTC SDK 前,请确保满足以下要求:
macOS 开发电脑,且可以正常访问互联网
Xcode 14.1 或以上版本
Apple 开发者账号
iOS 11.0 或以上版本真机设备,且可以正常访问互联网
打开 Xcode,单击 Create New Project... 新建项目。
在项目模板页选择 iOS > App,单击 Next。
在项目配置页填写 Product Name(本文以 RTCDemo 为例)、Team、Organization Identifier。Interface 选择 Storyboard,Language 选择 Swift。单击 Next。
说明
如果你尚未登录 Apple 账户,单击 Add account… 并按照提示登录。完成后即可选择你的 Apple 账户作为开发团队。

选择项目存储位置,单击 Create。
选中项目,进入 TARGETS > RTCDemo > Signing & Capabilities,勾选 Automatically manage signing。
切换到 Info 页面,单击 + 添加音频和视频设备权限。
添加 Privacy - Microphone Usage Description,并填入使用麦克风的原因。
添加 Privacy - Camera Usage Description,并填入使用摄像头的原因。

在终端窗口执行如下命令安装 CocoaPods。
sudo gem install cocoapods
bash进入项目根目录,执行如下命令,创建 Podfile 文件。
pod init
bash打开 Podfile 文件,替换为如下内容并保存。
说明
'3.x.y.z' 替换为具体的版本号,最新版本号请参看下载 SDK。source 'https://github.com/volcengine/volcengine-specs.git'
target 'RTCDemo' do
pod 'VolcEngineRTC', '3.x.y.z'
end
ruby执行 pod install 命令安装 VolcEngineRTC 相关库。安装成功后,项目文件夹中出现 RTCDemo.xcworkspace 文件,使用 Xcode 打开该文件进行后续操作。
下载并解压火山引擎 RTC SDK 文件。
将解压后目录中的 VolcEngineRTC.xcframework 拖入到项目中,勾选 Copy items if needed。

选中项目,进入 TARGETS > RTCDemo > General,在 Frameworks, Libraries, and Embedded Content 中将 VolcEngineRTC.xcframework 属性设置为 Embed & Sign。

说明
本章节将先向你提供 API 调用时序图和完整的实现代码,再对具体的实现步骤展开介绍。
下图为使用火山引擎 RTC SDK 实现基础音视频通话的 API 调用时序图。

将以下示例代码替换 ViewController.swift 文件中的全部内容,连接并选择你的 iOS 真机设备,单击 XCode 窗口左上角的运行按钮(或使用 Command ⌘ + R 快捷键),即可快速实现音视频通话。
说明
你需要将代码中的 roomId、userId、kAppID、token 替换为你在控制台上生成临时 Token 时所使用的房间 ID 和用户 ID,以及获取到的 AppID 和临时 Token。
import UIKit
import VolcEngineRTC
let kAppID = "" // 填写 appId
let roomId = "" // 填写房间号
let userId = "" // 填写 userId
let token = "" // 填写临时 token
class ViewController: UIViewController, ByteRTCVideoDelegate, ByteRTCRoomDelegate {
var rtcVideo: ByteRTCVideo?
var rtcRoom: ByteRTCRoom?
override func viewDidLoad() {
super.viewDidLoad()
self.createUI()
self.buildRTCEngine()
self.bindLocalRenderView()
}
deinit {
// 销毁房间
self.rtcRoom?.leaveRoom()
self.rtcRoom?.destroy()
self.rtcRoom = nil
// 销毁引擎
ByteRTCVideo.destroyRTCVideo()
self.rtcVideo = nil
}
// MARK: Private method
@objc func joinRoom() {
joinButton.isSelected = !joinButton.isSelected
if joinButton.isSelected {
joinButton.setTitle("离开房间", for: .normal)
// 加入房间
self.rtcRoom = self.rtcVideo?.createRTCRoom(roomId)
self.rtcRoom?.delegate = self
let userInfo = ByteRTCUserInfo.init()
userInfo.userId = userId
let roomCfg = ByteRTCRoomConfig.init()
roomCfg.isAutoPublish = true
roomCfg.isAutoSubscribeAudio = true
roomCfg.isAutoSubscribeVideo = true
self.rtcRoom?.joinRoom(token, userInfo: userInfo, roomConfig: roomCfg)
}
else {
joinButton.setTitle("加入房间", for: .normal)
self.rtcRoom?.leaveRoom()
}
}
func buildRTCEngine() {
// 创建引擎
self.rtcVideo = ByteRTCVideo.createRTCVideo(kAppID, delegate: self, parameters: [:])
// 开启本地音视频采集
self.rtcVideo?.startVideoCapture()
self.rtcVideo?.startAudioCapture()
}
func bindLocalRenderView() {
// 设置本地渲染视图
let canvas = ByteRTCVideoCanvas.init()
canvas.view = self.localView
canvas.renderMode = .hidden
self.rtcVideo?.setLocalVideoCanvas(.main, withCanvas: canvas);
}
func bindRemoteRenderView(roomId: String, userId: String) {
// 设置远端用户视频渲染视图
let canvas = ByteRTCVideoCanvas.init()
canvas.view = remoteView
canvas.renderMode = .hidden
let streamKey = ByteRTCRemoteStreamKey.init()
streamKey.userId = userId;
streamKey.roomId = roomId;
streamKey.streamIndex = .main;
self.rtcVideo?.setRemoteVideoCanvas(streamKey, withCanvas: canvas)
}
func removeRemoteRenderView(roomId: String, userId: String) {
// 移除远端用户视频渲染视图
let canvas = ByteRTCVideoCanvas.init()
canvas.view = nil // 置为空
canvas.renderMode = .hidden
let streamKey = ByteRTCRemoteStreamKey.init()
streamKey.userId = userId;
streamKey.roomId = roomId;
streamKey.streamIndex = .main;
self.rtcVideo?.setRemoteVideoCanvas(streamKey, withCanvas: canvas)
}
// 添加视图
func createUI() -> Void {
let width = self.view.bounds.size.width*0.5
let height = self.view.bounds.size.height*0.5
// 本地预览
localView.frame = CGRect(x: 0, y: 0, width: width, height: height)
self.view.addSubview(localView)
// 远端预览
remoteView.frame = CGRect(x: width, y: 0, width: width, height: height)
self.view.addSubview(remoteView)
// 加入房间按钮
joinButton.frame = CGRect(x: 10, y: height + 30, width: width*2 - 20, height: 44)
self.view.addSubview(joinButton)
}
// MARK: Lazy load
lazy var joinButton: UIButton = {
let button = UIButton(type: .custom)
button.backgroundColor = .blue
button.setTitle("加入房间", for: .normal)
button.addTarget(self, action: #selector(joinRoom), for: .touchUpInside)
return button
}()
lazy var remoteView: UIView = {
let view = UIView.init()
view.backgroundColor = .lightGray
return view
}()
lazy var localView: UIView = {
let view = UIView.init()
view.backgroundColor = .lightGray
return view
}()
// MARK: ByteRTCVideoDelegate & ByteRTCRoomDelegate
//进房状态
func rtcRoom(_ rtcRoom: ByteRTCRoom, onRoomStateChanged roomId: String, withUid uid: String, state: Int, extraInfo: String) {
}
// 远端用户发流
func rtcRoom(_ rtcRoom: ByteRTCRoom, onUserPublishStream userId: String, type: ByteRTCMediaStreamType) {
if type == .video || type == .both {
DispatchQueue.main.async {
self .bindRemoteRenderView(roomId: rtcRoom.getId(), userId: userId)
}
}
}
func rtcRoom(_ rtcRoom: ByteRTCRoom, onUserUnpublishStream userId: String, type: ByteRTCMediaStreamType, reason: ByteRTCStreamRemoveReason) {
if type == .video || type == .both {
DispatchQueue.main.async {
self .removeRemoteRenderView(roomId: rtcRoom.getId(), userId: userId)
}
}
}
}
swift在 ViewController.swift 引入以下头文件。
import UIKit
import VolcEngineRTC
swift将 ViewController.swift 中的 roomId、userId、kAppID、token 替换为你在控制台上生成临时 Token 时所使用的房间 ID 和用户 ID,以及获取到的 AppID 和临时 Token。
let kAppID = "" // 填写 appId
let roomId = "" // 填写房间号
let userId = "" // 填写 userId
let token = "" // 填写临时 token
swift这里为了演示,我们创建两个 View 分别用于渲染本端视频和远端视频。界面左上角显示本端视频,右上角显示远端视频,下方为加入房间的按钮。
// 添加视图
func createUI() -> Void {
let width = self.view.bounds.size.width*0.5
let height = self.view.bounds.size.height*0.5
// 本地预览
localView.frame = CGRect(x: 0, y: 0, width: width, height: height)
self.view.addSubview(localView)
// 远端预览
remoteView.frame = CGRect(x: width, y: 0, width: width, height: height)
self.view.addSubview(remoteView)
// 加入房间按钮
joinButton.frame = CGRect(x: 10, y: height + 30, width: width*2 - 20, height: 44)
self.view.addSubview(joinButton)
}
// MARK: Lazy load
lazy var joinButton: UIButton = {
let button = UIButton(type: .custom)
button.backgroundColor = .blue
button.setTitle("加入房间", for: .normal)
button.addTarget(self, action: #selector(joinRoom), for: .touchUpInside)
return button
}()
lazy var remoteView: UIView = {
let view = UIView.init()
view.backgroundColor = .lightGray
return view
}()
lazy var localView: UIView = {
let view = UIView.init()
view.backgroundColor = .lightGray
return view
}()
swift调用 createRTCVideo 创建引擎,所有 RTC 相关的 API 调用都要在创建引擎之后。
self.rtcVideo = ByteRTCVideo.createRTCVideo(kAppID, delegate: self, parameters: [:])
swift创建引擎后,调用 startVideoCapture 开启视频采集,调用 startAudioCapture 开启音频采集。
self.rtcVideo?.startVideoCapture()
self.rtcVideo?.startAudioCapture()
swift调用 createRTCRoom 创建 RTC 房间,所有和房间相关的 API 都在 ByteRTCRoom 类。
joinRoom 表示进房,进房状态可以通过 ByteRTCRoomDelegate 中 onRoomStateChanged 回调。
self.rtcRoom = self.rtcVideo?.createRTCRoom(roomId)
self.rtcRoom?.delegate = self
let userInfo = ByteRTCUserInfo.init()
userInfo.userId = userId
let roomCfg = ByteRTCRoomConfig.init()
roomCfg.isAutoPublish = true
roomCfg.isAutoSubscribeAudio = true
roomCfg.isAutoSubscribeVideo = true
self.rtcRoom?.joinRoom(token, userInfo: userInfo, roomConfig: roomCfg)
swift调用 setLocalVideoCanvas 设置本端渲染窗口。
func bindLocalRenderView() {
// 设置本地渲染视图
let canvas = ByteRTCVideoCanvas.init()
canvas.view = self.localView
canvas.renderMode = .hidden
self.rtcVideo?.setLocalVideoCanvas(.main, withCanvas: canvas);
}
swift在收到远端用户的 onUserPublishStream 回调后,你需要调用 setRemoteVideoCanvas 设置远端视图以在通话中查看远端视频。
func bindRemoteRenderView(roomId: String, userId: String) {
// 设置远端用户视频渲染视图
let canvas = ByteRTCVideoCanvas.init()
canvas.view = remoteView
canvas.renderMode = .hidden
let streamKey = ByteRTCRemoteStreamKey.init()
streamKey.userId = userId;
streamKey.roomId = roomId;
streamKey.streamIndex = .main;
self.rtcVideo?.setRemoteVideoCanvas(streamKey, withCanvas: canvas)
}
swift在收到远端用户的 onUserUnpublishStream 回调后,你需要停止渲染远端视频。
func removeRemoteRenderView(roomId: String, userId: String) {
// 移除远端用户视频渲染视图
let canvas = ByteRTCVideoCanvas.init()
canvas.view = nil // 置为空
canvas.renderMode = .hidden
let streamKey = ByteRTCRemoteStreamKey.init()
streamKey.userId = userId;
streamKey.roomId = roomId;
streamKey.streamIndex = .main;
self.rtcVideo?.setRemoteVideoCanvas(streamKey, withCanvas: canvas)
}
swift以下代码在 ViewController.swift 的析构函数中执行。调用 leaveRoom 离开房间,destroy 销毁房间;调用 destroyRTCVideo 销毁 RTC 引擎。
deinit {
// 销毁房间
self.rtcRoom?.leaveRoom()
self.rtcRoom?.destroy()
self.rtcRoom = nil
// 销毁引擎
ByteRTCVideo.destroyRTCVideo()
self.rtcVideo = nil
}
swift在 Xcode 中连接并选择你的 iOS 真机设备,单击 XCode 窗口左上角的运行按钮(或使用 Command ⌘ + R 快捷键)。
说明
如果你尚未信任开发者,请根据 Xcode 提示,在 iOS 设备上打开设置,选择通用 > VPN 与设备管理,在开发者 APP 中单击信任开发者。
在 iOS 设备上打开 Demo 应用时,在弹窗中选择开启摄像头和麦克风权限。
(可选)在第二台设备上使用相同的 AppID 和 RoomID,更换 UserID 并生成新的临时 Token,即可加入同一个房间体验双端通话。
双端通话效果如下:
使用模拟器编译报错 No such module 'VolcEngineRTC'?
解决方案:如果你使用的是搭载 Apple 芯片的 Mac 电脑,该问题可能是编译的架构(ARM64)和模拟器的架构(x86_64)不匹配导致的,请使用真机编译。
Xcode 15 编译报错 Sandbox: rsync.samba(xxxxx) deny(1)?
解决方案:选中项目,进入 TARGETS > 项目名称 > Build Settings,在 Build Options 中将 User Script Sandboxing 修改为 No。