本文介绍如何集成火山引擎 RTC SDK,并实现实时音视频通话。根据如下步骤操作,即可从 0 开始构建一个简单的音视频通话应用。
你也可以参考示例项目,了解更完整的项目实现。
在开始集成 RTC SDK 前,请确保满足以下要求:
打开 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。
说明
理论上来讲,Bundle Identifier 可以任意选择。不过对于个人开发者账户而言,一周只能创建最多 7个 Bundle Identifier。因此请注意管理已经创建的 Bundle Identifier。
切换到 Info 页面,单击 + 添加音频和视频设备权限。

在终端窗口执行如下命令安装 CocoaPods。
sudo gem install cocoapods
进入项目根目录,执行如下命令,创建 Podfile 文件。
pod init
打开 Podfile 文件,替换为如下内容并保存。
说明
'3.x.y.z' 替换为具体的版本号,最新版本号请参看下载 SDK。source 'https://github.com/volcengine/volcengine-specs.git' target 'RTCDemo' do pod 'VolcEngineRTC', '3.x.y.z' end
执行 pod install 命令安装 VolcEngineRTC 相关库。安装成功后,项目文件夹中出现 RTCDemo.xcworkspace 文件,使用 Xcode 打开该文件进行后续操作。
说明
执行 pod install 后,请务必打开 .xcworkspace 文件,而非 .xcodeproj 文件。这是因为如果以 .xcodeproj 文件打开项目的话,刚才使用 pod 安装的依赖库不会被打开,整个项目因而无法编译。
下载并解压火山引擎 RTC SDK 文件。
将解压后目录中的 VolcEngineRTC.xcframework、 RealXBase.xcframework 和 RTCFFmpeg.xcframework 拖入到项目中,勾选 Copy items if needed。

选中项目,进入 TARGETS > RTCDemo > General,在 Frameworks, Libraries, and Embedded Content 中将 VolcEngineRTC.xcframework、 RealXBase.xcframework 和 RTCFFmpeg.xcframework 的属性设置为 Embed & Sign。
应苹果公司的要求,你的 iOS App 如需要上线 App Store,必须准确描述 App 本身和集成的第三方 SDK 使用指定范围内系统接口的原因。自 2024 年 5 月 1 日起,如果你未提供相关描述,你的 App 将无法通过 App Store Connect 的审核。详见 Describing use of required reason API。
如果你在 App 中集成了 3.58 及之前版本的 RTC SDK,你必须添加相关说明:

获取 RTC SDK 的隐私清单文件:
说明
本章节将先向你提供 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, ByteRTCEngineDelegate, ByteRTCRoomDelegate { var rtcEngine: ByteRTCEngine? var rtcRoom: ByteRTCRoom? override func viewDidLoad() { super.viewDidLoad() self.createUI() self.buildRTCEngine() self.bindLocalRenderView() } deinit { // 销毁房间 self.rtcRoom?.leave() self.rtcRoom?.destroy() self.rtcRoom = nil // 销毁引擎 ByteRTCEngine.destroyRTCEngine() self.rtcEngine = nil } // MARK: Private method @objc func joinRoom() { joinButton.isSelected = !joinButton.isSelected if joinButton.isSelected { joinButton.setTitle("离开房间", for: .normal) // 在房间中时显示 “离开房间” // 加入房间 self.rtcRoom = self.rtcEngine?.createRTCRoom(roomId) self.rtcRoom?.delegate = self let userInfo = ByteRTCUserInfo.init() userInfo.userId = userId let roomCfg = ByteRTCRoomConfig.init() roomCfg.isPublishAudio = true roomCfg.isPublishVideo = true roomCfg.isAutoSubscribeAudio = true roomCfg.isAutoSubscribeVideo = true self.rtcRoom?.joinRoom(token, userInfo: userInfo, userVisibility: true, roomConfig: roomCfg) } else { joinButton.setTitle("加入房间", for: .normal) // 离开房间中后显示 “加入房间” self.rtcRoom?.leave() } } func buildRTCEngine() { // 创建引擎 let engineCfg = ByteRTCEngineConfig.init() engineCfg.appID = kAppID engineCfg.parameters = [:] self.rtcEngine = ByteRTCEngine.createRTCEngine(engineCfg, delegate: self) // 开启本地音视频采集 self.rtcEngine?.startVideoCapture() self.rtcEngine?.startAudioCapture() } func bindLocalRenderView() { // 设置本地渲染视图 let canvas = ByteRTCVideoCanvas.init() canvas.view = self.localView canvas.renderMode = .hidden self.rtcEngine?.setLocalVideoCanvas(withCanvas: canvas); } func bindRemoteRenderView(streamId: String) { // streamID:对方流的ID // 设置远端用户视频渲染视图 let canvas = ByteRTCVideoCanvas.init() canvas.view = remoteView canvas.renderMode = .hidden self.rtcEngine?.setRemoteVideoCanvas(streamId, withCanvas: canvas) } func removeRemoteRenderView(streamId: String) { // streamID:对方流的ID // 移除远端用户视频渲染视图 let canvas = ByteRTCVideoCanvas.init() canvas.view = nil // 置为空 canvas.renderMode = .hidden self.rtcEngine?.setRemoteVideoCanvas(streamId, 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: ByteRTCEngineDelegate & ByteRTCRoomDelegate 协议所需要的回调函数 // 进房状态 func rtcRoom(_ rtcRoom: ByteRTCRoom, onRoomStateChanged roomId: String, withUid uid: String, state: Int, extraInfo: String) { if state == 0 { print("Join Room Success") } } // 下面两个方法为:远端用户发流、远端用户销毁流(注:销毁流的部分已经集成于func rtcRoom(rtcRoom, onUserPublishStreamVideo streamId, info, isPublish)。设置isPublish = false即为销毁流) // onUserPublishStreamVideo方法负责控制视频流,另一个onUserPublishStreamAudio方法负责控制音频流 func rtcRoom(_ rtcRoom: ByteRTCRoom, onUserPublishStreamVideo streamId: String, info: ByteRTCStreamInfo, isPublish: Bool) { if isPublish { DispatchQueue.main.async { self.bindRemoteRenderView(streamId: streamId) } } else { DispatchQueue.main.async { self.removeRemoteRenderView(streamId: streamId) } } } func rtcRoom(_ rtcRoom: ByteRTCRoom, onUserPublishStreamAudio streamId: String, info: ByteRTCStreamInfo, isPublish: Bool) { } }
在 ViewController.swift 引入以下头文件。
import UIKit import VolcEngineRTC
将 ViewController.swift 中的 roomId、userId、kAppID、token 替换为你在控制台上生成临时 Token 时所使用的房间 ID 和用户 ID,以及获取到的 AppID 和临时 Token。
let kAppID = "" // 填写 appId let roomId = "" // 填写房间号 let userId = "" // 填写 userId let token = "" // 填写临时 token
这里为了演示,我们创建两个 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 }()
调用 createRTCEngine 创建引擎,所有 RTC 相关的 API 调用都要在创建引擎之后。
let engineCfg = ByteRTCEngineConfig.init() engineCfg.appID = kAppID engineCfg.parameters = [:] self.rtcEngine = ByteRTCEngine.createRTCEngine(engineCfg, delegate: self)
创建引擎后,调用 startVideoCapture 开启视频采集,调用 startAudioCapture 开启音频采集。
self.rtcEngine?.startVideoCapture() self.rtcEngine?.startAudioCapture()
调用 createRTCRoom 创建 RTC 房间,所有和房间相关的 API 都在 ByteRTCRoom 类。
joinRoom 表示进房,进房状态可以通过 ByteRTCRoomDelegate 中 onRoomStateChanged 回调。
self.rtcRoom = self.rtcEngine?.createRTCRoom(roomId) self.rtcRoom?.delegate = self let userInfo = ByteRTCUserInfo.init() userInfo.userId = userId let roomCfg = ByteRTCRoomConfig.init() roomCfg.isPublishAudio = true roomCfg.isPublishVideo = true roomCfg.isAutoSubscribeAudio = true roomCfg.isAutoSubscribeVideo = true self.rtcRoom?.joinRoom(token, userInfo: userInfo, userVisibility: true, roomConfig: roomCfg)
调用 setLocalVideoCanvas 设置本端渲染窗口。
func bindLocalRenderView() { // 设置本地渲染视图 let canvas = ByteRTCVideoCanvas.init() canvas.view = self.localView canvas.renderMode = .hidden self.rtcEngine?.setLocalVideoCanvas(.main, withCanvas: canvas); }
在收到远端用户的 onUserPublishVideoStream 或 onFirstRemoteVideoFrameDecoded 回调后,你需要调用 setRemoteVideoCanvas 设置远端视图以在通话中查看远端视频。建议您在 onFirstRemoteVideoFrameDecoded 回调中设置视频渲染,确保视频渲染在视频解码完成后进行,以避免卡顿。
func bindRemoteRenderView(streamId: String) { // streamID:对方流的ID // 设置远端用户视频渲染视图 let canvas = ByteRTCVideoCanvas.init() canvas.view = remoteView canvas.renderMode = .hidden self.rtcEngine?.setRemoteVideoCanvas(streamId, withCanvas: canvas) }
在收到远端用户的 onUserPublishVideoStream 回调并确认其中的 isPublish 参数为 false 后,你需要停止渲染远端视频。
func removeRemoteRenderView(streamId: String) { // streamID:对方流的ID // 移除远端用户视频渲染视图 let canvas = ByteRTCVideoCanvas.init() canvas.view = nil // 置为空 canvas.renderMode = .hidden self.rtcEngine?.setRemoteVideoCanvas(streamId, withCanvas: canvas) }
以下代码在 ViewController.swift 的析构函数中执行。调用 leave 离开房间,destroy 销毁房间;调用 destroyRTCEngine 销毁 RTC 引擎。
deinit { // 销毁房间 self.rtcRoom?.leave() self.rtcRoom?.destroy() self.rtcRoom = nil // 销毁引擎 ByteRTCEngine.destroyRTCEngine() self.rtcEngine = nil }
在 Xcode 中连接并选择你的 iOS 真机设备,单击 Xcode 窗口左上角的运行按钮(或使用 Command ⌘ + R 快捷键)。
说明
如果你尚未信任开发者,请根据 Xcode 提示,在 iOS 设备上打开设置,选择通用 > VPN 与设备管理,在开发者 APP 中单击信任开发者。
在 iOS 设备上打开 Demo 应用时,在弹窗中选择开启摄像头和麦克风权限。
(可选)在第二台设备上使用相同的 AppID 和 RoomID,更换 UserID 并生成新的临时 Token,即可加入同一个房间体验双端通话。
双端通话效果如下:
在实现音视频通话后,如遇无声音、无画面、视频卡顿等问题时,您可以使用诊断工具快速排查和定位异常房间及用户,并获取异常根因分析、处理建议、分析报告等。
No such module 'VolcEngineRTC'?Sandbox: rsync.samba(xxxxx) deny(1)?