扣子编程 Realtime SDK iOS 版基于火山引擎 RTC SDK 进行封装,支持将扣子编程的智能体功能与火山引擎 RTC SDK 集成,实现与智能体的实时音视频通话。扣子编程提示 Swift 和 Objective-C 语言的示例项目源码,你可以参考示例项目源码,快速从零开始构建一个简单的音视频通话应用。
开始集成 Realtime SDK 前,请确保开发环境满足以下要求:
| 操作 | 说明 |
|---|---|
|
发布智能体 |
已成功搭建并发布智能体为 API 服务。搭建步骤可参考搭建可视化智能体或搭建低延时语音助手,发布步骤请参见发布为 API 服务。 说明 若需使用视频理解能力,请为智能体配置一个视觉模型,例如豆包视觉理解模型。 |
|
获取访问密钥 |
获取访问密钥,用于身份认证与鉴权。
说明 扣子编程 SDK 封装了多种鉴权方式,能够有效简化鉴权流程,你可以参考鉴权示例代码实现不同方式的 OAuth 认证,以获取和管理访问扣子编程 API 所需的令牌。 |
|
准备开发环境 |
|
音视频通话 iOS 示例项目的目录结构如下:
├── Podfile
├── coze-swift
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── Base.lproj
│ ├── Config //配置 API 相关信息
│ ├── Info.plist
│ ├── Models //创建房间相关数据结构
│ ├── SceneDelegate.swift
│ ├── Services //API 请求封装
│ └── ViewController.swift //主界面
├── coze-swift.xcodeproj
└── coze-swift.xcworkspace
├── Podfile
├── Pods
├── coze-objc
│ ├── AppDelegate.h
│ ├── AppDelegate.m
│ ├── Assets.xcassets
│ ├── Base.lproj
│ ├── Config
│ ├── Info.plist
│ ├── Models
│ ├── SceneDelegate.h
│ ├── SceneDelegate.m
│ ├── Services
│ ├── ViewController.h
│ ├── ViewController.m
│ └── main.m
├── coze-objc.xcodeproj
└── coze-objc.xcworkspace
git clone https://github.com/coze-dev/coze-ios
pod install
coze-swift/Config/APIConfig.swift.template文件为。APIConfig.swift配置文件,配置 accessToken 和 botId。import Foundation
struct APIConfig {
static let baseURL = "https://api.coze.cn"
static let accessToken = "pat_sWaR9V4Yr8***" // 替换为从 https://www.coze.cn/open/oauth/pats 获取到的token
static let botId = "74415109067667***" // 替换为自己的bot ID
static let voiceId:String? = nil // 音色ID,可选
}
关键参数说明如下:
coze-objc/Config/APIConfig.h.template 文件为coze-objc/Config/APIConfig.h。APIConfig.h配置文件,配置 API_ACCESS_TOKEN 和 API_BOT_ID。
#ifndef APIConfig_h
#define APIConfig_h
#define API_BASE_URL @"https://api.coze.cn" // 替换为从 https://www.coze.cn/open/oauth/pats 获取到的token
#define API_ACCESS_TOKEN @"pat_KWQlw2nvTlLTMISAz***"
#define API_BOT_ID @"74281773215***"
#define API_VOICE_ID nil
#endif /* APIConfig_h */
关键参数说明如下:
coze-swift.xcworkspace或 coze-objc.xcworkspace。本步骤为如何创建一个新项目,如集成到已有项目,请跳过该步骤。
| 参数 | 说明 |
|---|---|
| Product Name | 输入项目的名称,该名称将用于标识应用程序。 |
| Team | 选择开发团队。如果尚未登录 Apple 账户,请点击 Add account… 按钮,并按照提示完成登录。登录完成后,即可从下拉菜单中选择您的 Apple 账户作为开发团队。 |
| Organization Identifier | 输入组织标识符,通常为反向域名格式(例如 com.yourcompany )。 |
| Interface | 选择 Storyboard,用于定义用户界面布局。 |
| Language | 选择 Swift 或 Objective-C,作为项目开发的主要编程语言。 |
选中项目,进入 TARGETS > coze-objc > Signing & Capabilities,选择 Automatically manage signing。

音视频通话需要使用麦克风、摄像头和网络权限,操作步骤如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false />
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<!-- 新增配置 -->
<key>NSCameraUsageDescription</key>
<string>需要访问您的相机以进行视频通话</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要访问您的麦克风以进行语音通话</string>
<key>NSAppTransportSecurity</key>
<string>需要网络权限</string>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
</dict>
<!-- 新增配置 -->
</dict>
</plist>
本文以通过 CocoaPods 直接集成为例。如需手动集成,请参考火山引擎 RTC 文档。
sudo gem install cocoapods
Podfile 文件。
pod init
Podfile 文件,并将其内容替换为以下代码。建议使用 VSCode 或其他代码编辑器打开项目文件。说明
请将 **3.58.1.19400 **替换为 RTC 的目标版本号,具体版本信息可在下载 RTC SDK 页面查看。
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
source 'https://cdn.cocoapods.org/'
source 'https://github.com/volcengine/volcengine-specs.git'
target 'coze-swift' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
pod 'VolcEngineRTC', '3.58.1.19400'
# Pods for RTCDemo-Swift
end
pod install 命令安装 VolcEngineRTC 相关库。说明
本文以 ViewController.m 中的实现步骤为例,讲解如何实现一个基本的音视频通话功能。本示例并未覆盖全部代码内容,如需查看完整代码,请从音视频通话 iOS 示例项目获取完整代码。
在ViewController.swift 或 ViewController.h 中引入必要的模块,并声明相关属性和方法。
import UIKit
import VolcEngineRTC
class ViewController: UIViewController, ByteRTCVideoDelegate, ByteRTCRoomDelegate {
var rtcVideo: ByteRTCVideo?
var rtcRoom: ByteRTCRoom?
private var roomInfo: RoomData?
}
#import <UIKit/UIKit.h>
#import <VolcEngineRTC/VolcEngineRTC.h>
#import "ApiResponse.h"
NS_ASSUME_NONNULL_BEGIN
@interface ViewController : UIViewController <ByteRTCVideoDelegate, ByteRTCRoomDelegate, UITableViewDelegate, UITableViewDataSource>
@end
NS_ASSUME_NONNULL_END
在 ViewController 中实现 createUI 方法,创建本地视频预览视图,定义控制按钮。
func createUI() {
let width = self.view.bounds.size.width
let height = self.view.bounds.size.height
// 本地预览视图占上半部分
let previewHeight = height * 0.4
localView.frame = CGRect(x: 0, y: 0, width: width, height: previewHeight)
self.view.addSubview(localView)
// 按钮区域
let buttonY = previewHeight + 10
let buttonWidth = (width - 30) / 2
joinButton.frame = CGRect(x: 10, y: buttonY, width: buttonWidth, height: 44)
cameraButton.frame = CGRect(x: width - buttonWidth - 10, y: buttonY, width: buttonWidth, height: 44)
self.view.addSubview(joinButton)
self.view.addSubview(cameraButton)
}
- (void)createUI {
CGFloat width = self.view.bounds.size.width;
CGFloat height = self.view.bounds.size.height;
// 本地预览视图
CGFloat previewHeight = height * 0.4;
self.localView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, width, previewHeight)];
self.localView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:self.localView];
// 按钮区域
CGFloat buttonY = previewHeight + 10;
CGFloat buttonWidth = (width - 30) / 2;
// 创建连接按钮
self.joinButton = [UIButton buttonWithType:UIButtonTypeCustom];
self.joinButton.frame = CGRectMake(10, buttonY, buttonWidth, 44);
[self.joinButton setTitle:@"连接" forState:UIControlStateNormal];
[self.joinButton addTarget:self action:@selector(connectButtonTapped) forControlEvents:UIControlEventTouchUpInside];
// 创建其他控制按钮...
[self.view addSubview:self.joinButton];
}
调用扣子编程的创建房间接口创建房间,通过 NetworkService 获取房间信息。
do {
// 获取房间信息
let response = try await NetworkService.shared.createRoom(
botId: APIConfig.botId,
voiceId: APIConfig.voiceId
)
// 异常处理
if response.code != 0 {
throw NSError(
domain: "", code: Int(response.code),
userInfo: [NSLocalizedDescriptionKey: response.msg])
}
roomInfo = response.data
} catch {
print("连接失败: \(error)")
}
[[NetworkService shared] createRoomWithBotId:API_BOT_ID
voiceId:API_VOICE_ID
completion:^(RoomResponse *response, NSError *error) {
if (error || response.code != 0) {
// 处理错误...
} else {
self.roomInfo = response.data;
[self buildRTCEngine];
[self bindLocalRenderView];
[self joinRoom];
}
}];
说明
创建房间后返回的 Token,其默认有效期为 3 分钟,如果 3 分钟内没有用户加入房间,或者用户静音 3 分钟,智能体将自动退出房间,房间随即被释放,后续若要再次和智能体对话,则需重新创建房间。
创建并初始化 RTC 引擎。
func buildRTCEngine() {
guard let roomInfo = self.roomInfo else { return }
self.rtcVideo = ByteRTCVideo.createRTCVideo(
roomInfo.app_id, delegate: self, parameters: [:])
}
- (void)buildRTCEngine {
if (!self.roomInfo) {
return;
}
self.rtcVideo = [ByteRTCVideo createRTCVideo:self.roomInfo.app_id
delegate:self
parameters:@{}];
[self.rtcVideo startAudioCapture];
}
startAudioCapture() 开启麦克风音频流采集,调用 startVideoCapture() 开启摄像头视频流采集。setLocalVideoCanvas() 方法设置本端视频渲染视图。// 开启音频采集
self.rtcVideo?.startAudioCapture()
// 开启视频采集(可选)
self.rtcVideo?.startVideoCapture()
// 开始音频采集
[self.rtcVideo startAudioCapture];
// 开始视频采集
[self.rtcVideo startVideoCapture];
- (void)bindLocalRenderView {
ByteRTCVideoCanvas *canvas = [[ByteRTCVideoCanvas alloc] init];
//设置本端视频渲染视图
canvas.view = self.localView;
canvas.renderMode = ByteRTCRenderModeHidden;
[self.rtcVideo setLocalVideoCanvas:ByteRTCStreamIndexMain withCanvas:canvas];
}
createRTCRoom 接口创建 RTC 房间。joinRoom 接口加入 RTC 房间。// 创建房间
self.rtcRoom = self.rtcVideo?.createRTCRoom(roomInfo.room_id)
self.rtcRoom?.delegate = self
let userInfo = ByteRTCUserInfo()
userInfo.userId = roomInfo.uid
let roomCfg = ByteRTCRoomConfig()
roomCfg.isAutoPublish = true
roomCfg.isAutoSubscribeAudio = true
roomCfg.isAutoSubscribeVideo = true
// 加入房间
self.rtcRoom?.joinRoom(roomInfo.token, userInfo: userInfo, roomConfig: roomCfg)
- (void)joinRoom {
self.rtcRoom = [self.rtcVideo createRTCRoom:self.roomInfo.room_id];
self.rtcRoom.delegate = self;
ByteRTCUserInfo *userInfo = [[ByteRTCUserInfo alloc] init];
userInfo.userId = self.roomInfo.uid;
ByteRTCRoomConfig *roomConfig = [[ByteRTCRoomConfig alloc] init];
roomConfig.isAutoPublish = YES;
roomConfig.isAutoSubscribeAudio = YES;
roomConfig.isAutoSubscribeVideo = YES;
[self.rtcRoom joinRoom:self.roomInfo.token
userInfo:userInfo
roomConfig:roomConfig];
}
调用 setLocalVideoCanvas 方法配置本端视频渲染视图。
func bindLocalRenderView() {
let canvas = ByteRTCVideoCanvas.init()
canvas.view = self.localView
canvas.renderMode = .hidden
self.rtcVideo?.setLocalVideoCanvas(.indexMain, withCanvas: canvas)
}
- (void)bindLocalRenderView {
ByteRTCVideoCanvas *canvas = [[ByteRTCVideoCanvas alloc] init];
canvas.view = self.localView;
canvas.renderMode = ByteRTCRenderModeHidden;
[self.rtcVideo setLocalVideoCanvas:ByteRTCStreamIndexMain withCanvas:canvas];
}
处理接收到的用户消息并实时显示字幕。
当收到 conversation.message.delta 事件时,系统会根据事件类型处理消息:
messageList.append(message)将其添加到消息列表中。lastMessage += message对最后一条消息进行追加更新。处理完成后,调用messageTableView.reloadData()方法,刷新 UITableView 并显示最新的字幕内容。
func addMessage(_ message: String, eventType: String) {
DispatchQueue.main.async {
if self.lastEventType == "conversation.message.delta" {
if var lastMessage = self.messageList.last {
lastMessage += message
self.messageList[self.messageList.count - 1] = lastMessage
}
} else if eventType == "conversation.message.delta" {
self.messageList.append(message)
}
self.lastEventType = eventType
self.messageTableView.reloadData()
}
}
监听 onUserMessageReceived 回调,将收到的 messageDict 用户消息更新到界面上。
- (void)rtcRoom:(ByteRTCRoom *)rtcRoom
onUserMessageReceived:(NSString *)uid
message:(NSString *)message {
NSError *error;
NSData *jsonData = [message dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *messageDict = [NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:&error];
if (!error) {
NSString *eventType = messageDict[@"event_type"];
if ([eventType isEqualToString:@"conversation.message.delta"]) {
NSString *content = messageDict[@"data"][@"content"] ?: @"";
[self addMessage:content eventType:eventType];
}
}
}
在 cameraButtonTapped 回调中,通过按钮的 isSelected 状态切换,调用 startVideoCapture() 或 stopVideoCapture()方法,实现摄像头的启用或禁用功能。
@objc private func cameraButtonTapped() {
cameraButton.isSelected = !cameraButton.isSelected
if cameraButton.isSelected {
self.rtcVideo?.startVideoCapture()
} else {
self.rtcVideo?.stopVideoCapture()
}
}
- (void)cameraButtonTapped {
self.cameraButton.selected = !self.cameraButton.selected;
if (self.cameraButton.selected) {
[self.rtcVideo startVideoCapture];
} else {
[self.rtcVideo stopVideoCapture];
}
}
在 muteButtonTapped 回调中,根据按钮的选中状态调用 startAudioCapture() 或 stopAudioCapture()方法,从而实现麦克风的启用或静音功能。
@objc private func muteButtonTapped() {
muteButton.isSelected = !muteButton.isSelected
if muteButton.isSelected {
self.rtcVideo?.stopAudioCapture()
} else {
self.rtcVideo?.startAudioCapture()
}
}
- (void)muteButtonTapped {
self.muteButton.selected = !self.muteButton.selected;
if (self.muteButton.selected) {
[self.rtcVideo stopAudioCapture];
} else {
[self.rtcVideo startAudioCapture];
}
}
调用 sendUserMessage 方法,实现在语音通话过程中,用户可以打断通话。打断通话后,将停止音频流输出。
@objc private func interruptButtonTapped() {
let message = [
"id": "event_1",
"event_type": "conversation.chat.cancel",
"data": "{}"
]
do {
let jsonData = try JSONSerialization.data(withJSONObject: message)
if let jsonString = String(data: jsonData, encoding: .utf8) {
self.rtcRoom?.sendUserMessage(
APIConfig.botId,
message: jsonString,
config: ByteRTCMessageConfig.reliableOrdered)
}
} catch {
print("Error creating JSON: \(error)")
}
}
- (void)interruptButtonTapped {
NSDictionary *message = @{
@"id" : @"event_1",
@"event_type" : @"conversation.chat.cancel",
@"data" : @"{}"
};
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:message
options:0
error:&error];
if (!error) {
NSString *jsonString = [[NSString alloc] initWithData:jsonData
encoding:NSUTF8StringEncoding];
[self.rtcRoom sendUserMessage:API_BOT_ID
message:jsonString
config:ByteRTCMessageConfigReliableOrdered];
}
}
leaveRoom 和destroy 接口离开并销毁房间。destroyRTCVideo 接口释放引擎资源,避免内存泄漏。deinit {
// 销毁房间
self.rtcRoom?.leaveRoom()
self.rtcRoom?.destroy()
self.rtcRoom = nil
// 销毁引擎
ByteRTCVideo.destroyRTCVideo()
self.rtcVideo = nil
}
- (void)dealloc {
[self.rtcRoom leaveRoom];
[self.rtcRoom destroy];
self.rtcRoom = nil;
[ByteRTCVideo destroyRTCVideo];
self.rtcVideo = nil;
}
说明
在实现音视频通话后,如遇无声音、无画面、视频卡顿等问题时,您可以使用诊断工具快速排查和定位异常房间及用户,并获取异常根因分析、处理建议、分析报告等。
智能语音信令事件包括上行事件和下行事件。每个事件有 ID 和 EventType, 通过 EventType 可以区分具体的事件类型,每个事件类型对应的 Payload 在 Data 中,开发者可以按需去提取需要的内容。
sendUserMessage 发送,如果是嵌入式设备集成音视频,可通过 Realtime SDK 的 byte_rtc_rts_send_message 发送。详细信息可参考Realtime 上行事件。onUserMessageReceived回调,如果是嵌入式设备集成音视频,可订阅 Realtime SDK 的 on_message_received回调,接收下行事件。应用程序需要解析下行事件,并根据业务需求进行下一步操作。详细信息可参考Realtime 下行事件。智能语音信令事件的公共参数如下:
| 参数名称 | 类型 | 描述 |
|---|---|---|
| id | String | 事件 ID,也就是事件的唯一标识。由客户端或服务端生成,在故障排查场景下用于定位具体的事件,便于排查问题。 |
| event_type | String | 事件的类型。 |
| data | JSON | 事件的详细信息,其中包含具体事件的业务字段。 |
信令事件的使用流程如下:
session.created 事件,确认房间初始化完成。conversation.message.create)向智能体发送消息。conversation.message.delta)获取智能体的增量回复。conversation.chat.requires_action 后,执行插件操作并通过事件 conversation.chat.submit_tool_outputs 提交结果。error 事件捕获和处理异常情况。使用示例如下:
// 发送消息
let message = [
"id": "event_1",
"event_type": "conversation.message.create",
"data": [
"role": "user",
"content_type": "text",
"content": "讲一个笑话"
],
]
do {
let jsonData = try JSONSerialization.data(withJSONObject: message)
if let jsonString = String(data: jsonData, encoding: .utf8) {
self.rtcRoom?.sendUserMessage(
APIConfig.botId, message: jsonString,
config: ByteRTCMessageConfig.reliableOrdered)
}
} catch {
print("Error creating JSON: \(error)")
}
// 发送消息
NSDictionary *message = @{
@"id" : @"event_1",
@"event_type" : @"conversation.message.create",
@"data" : @{
@"role": @"user",
@"content_type": @"text",
@"content": @"讲一个笑话"
}
};
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:message
options:0
error:&error];
if (!error) {
NSString *jsonString = [[NSString alloc] initWithData:jsonData
encoding:NSUTF8StringEncoding];
[self.rtcRoom sendUserMessage:API_BOT_ID
message:jsonString
config:ByteRTCMessageConfigReliableOrdered];
}
在处理信令事件时,建议采用以下思路:
示例代码如下:
func rtcRoom(_ rtcRoom: ByteRTCRoom, onUserMessageReceived uid: String, message: String) {
print("收到用户消息 - 用户ID: \(uid), 消息: \(message)")
do {
if let jsonData = message.data(using: .utf8) {
let messageData = try JSONDecoder().decode(MessageData.self, from: jsonData)
if messageData.event_type == "conversation.message.delta"
|| messageData.event_type == "conversation.message.completed"
{
let content = messageData.data?.content ?? ""
self.addMessage(content, eventType: (messageData.event_type)!)
}
}
} catch {
print("JSON 解析错误: \(error)")
}
}
// 监听所有事件
- (void)rtcRoom:(ByteRTCRoom *)rtcRoom
onUserMessageReceived:(NSString *)uid
message:(NSString *)message {
NSLog(@"收到用户消息 - 用户ID: %@, 消息: %@", uid, message);
NSError *error;
NSData *jsonData = [message dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *messageDict = [NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:&error];
}
Sandbox: rsync.samba(xxxxx) deny(1)Xcode 15 及以上版本如果提示该错误,请按以下步骤进行处理:
在 Xcode 中,选中项目,选择 TARGETS > 项目名称 > Build Settings,在 Build Options 区域,将 User Script Sandboxing 的值修改为 No。
No such module 'VolcEngineRTC'请使用 iOS 真机进行测试,不支持使用模拟器运行。
doesn't match RTCDemo-Swift.app's iOS deployment target该错误可能是由于真机的 iOS 版本与 Xcode 的配置版本不一致导致的。解决方法如下:
在 Xcode 中,选中项目,选择 TARGETS > 项目名称 > General。在 Minimum Deployments 区域,将 iOS 版本号修改为与真机一致的版本。