实现数字人实时播报
本文档用于说明如何快速集成客户端 SDK(ZEGO Express SDK 和数字人 SDK)实现数字人实时播报。
与数字人视频通话不同,数字人播报是单向观看场景——即用户不和数字人进行互动,只是拉数字人播报内容进行观看。
客户端只需要登录 RTC 房间 并拉流 ,不需要采集或推流 用户的音视频流。创建播报实例后,服务端可以通过 TTS 接口主动让数字人播报指定文本。
适用于数字人直播、新闻播报、活动主持和固定话术播报等场景。
本文档用于说明如何快速集成客户端 SDK(ZEGO Express SDK)实现数字人实时播报。
与数字人视频通话不同,数字人播报是单向观看场景——即用户不和数字人进行互动,只是拉数字人播报内容进行观看。
客户端只需要登录 RTC 房间 并拉流 ,不需要采集或推流 用户的音视频流。创建播报实例后,服务端可以通过 TTS 接口主动让数字人播报指定文本。
适用于数字人直播、新闻播报、活动主持和固定话术播报等场景。
与数字人视频通话的区别
前提条件
已在 ZEGO 控制台 创建项目,并申请有效的 AppID 和 ServerSecret(用于服务端 API 签名与 RTC Token04 生成),详情请参考 控制台 - 项目信息 。
已联系 ZEGO 技术支持开通数字人 PaaS 服务和相关接口的权限。
已获取有效的 digital_human_id(测试时可使用公共 ID:c4b56d5c-db98-4d91-86d4-5a97b507da97)。
已按 业务后台快速开始指引 集成播报数字人相关服务端 API。
已从下载页面 下载针对 AI Agent 优化的 ZEGO Express SDK,并集成到项目中。
已联系 ZEGO 技术支持获取针对 AI Agent 优化的 ZEGO Express SDK,并集成到项目中。
数字人播报不需要录音权限,也不会推送本地流。若同一个应用还包含语音通话或数字人视频通话入口,其他入口仍需要按对应文档申请录音权限。
示例代码
以下是接入实时互动 AI Agent API 的业务后台示例代码,您可以参考示例代码来实现自己的业务逻辑。
业务后台示例代码
包含获取 ZEGO Token、注册智能体、创建播报数字人实例、主动调用 TTS 和停止实例等能力。
以下是客户端示例代码,您可以参考示例代码来实现自己的业务逻辑。
Android 客户端示例代码
入口为 StartLiveDigitalHumanCall,对应 video.LiveDigitalHumanActivity,包含登录、拉流、数字人渲染、主动 TTS 和退出房间等能力。
iOS 客户端示例代码
入口为 StartLiveDigitalHuman,通过数字人页面的播报模式完成登录、拉流、数字人渲染、主动 TTS 和退出房间。
Web 客户端示例代码
入口为 Start Live Digital Human,包含登录、拉流、主动 TTS 和退出房间等能力。
以下视频演示了如何跑通服务端和客户端(Web)示例代码并跟智能体进行语音互动。
以下视频演示了如何跑通服务端和客户端(iOS)示例代码并跟智能体进行语音互动。
整体业务流程
服务端,参考业务后台快速开始 文档跑通业务后台示例代码,部署好业务后台
接入实时互动 AI Agent API 管理智能体。
完成以上两个步骤后即可实现观看数字人播报。
核心能力实现
集成 ZEGO Express SDK
在 app/build.gradle 中添加数字人 SDK:
app/build.gradle dependencies {
implementation 'im.zego:digitalmobile:1.3.0.43'
}app/build.gradle
dependencies {
implementation 'im.zego:digitalmobile:1.3.0.43'
}
在 AndroidManifest.xml 中声明网络权限。播报数字人场景不需要声明或申请 RECORD_AUDIO:
AndroidManifest.xml <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />AndroidManifest.xml
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
Android quickstart 为了同时支持语音通话和数字人视频通话,Manifest 中仍保留了 RECORD_AUDIO;进入 LiveDigitalHumanActivity 时不会申请该权限,也不会创建本地音频流。只保留播报入口时可以移除该权限。
播报场景无需运行时申请录音权限,进入页面后可直接初始化 ZEGO Express SDK:
ZegoEngineProfile profile = new ZegoEngineProfile();
profile.appID = appID; // 从 ZEGO 控制台获取
// !mark
profile.scenario = ZegoScenario.HIGH_QUALITY_CHATROOM;
profile.application = getApplication();
ZegoExpressEngine.createEngine(profile, null);ZegoEngineProfile profile = new ZegoEngineProfile();
profile.appID = appID; // 从 ZEGO 控制台获取
// !mark
profile.scenario = ZegoScenario.HIGH_QUALITY_CHATROOM;
profile.application = getApplication();
ZegoExpressEngine.createEngine(profile, null);
若在 Android 模拟器上运行,部分音视频能力和数字人渲染依赖真机硬件解码与 GPU 加速,可能无法正常显示画面或出现黑屏。建议在真机上调试和验证数字人播报效果。
iOS quickstart 通过 CocoaPods 集成 SDK:
Podfile target 'ai_agent_quickstart' do
use_frameworks! :linkage => :static
use_modular_headers!
pod 'ZegoExpressEngine', :path => 'libs/Express'
pod 'Masonry', '1.1.0'
pod 'ZegoDigitalMobile', '>= 1.3.0'
endPodfile
target 'ai_agent_quickstart' do
use_frameworks! :linkage => :static
use_modular_headers!
pod 'ZegoExpressEngine', :path => 'libs/Express'
pod 'Masonry', '1.1.0'
pod 'ZegoDigitalMobile', '>= 1.3.0'
end
播报数字人是单向观看场景,不需要在 Info.plist 中添加 NSMicrophoneUsageDescription,也不需要调用 requestRecordPermission:。如果同一个应用还支持数字人视频通话,可以保留视频通话所需的麦克风权限声明。
初始化播报页面时直接调用 ZegoExpressEngine,不申请麦克风权限:
- (void)initZegoExpressEngine {
ZegoEngineProfile *profile = [[ZegoEngineProfile alloc] init];
profile.appID = appID; // 从 ZEGO 控制台获取
profile.scenario = ZegoScenarioHighQualityChatroom;
[ZegoExpressEngine createEngineWithProfile:profile eventHandler:self];
}- (void)initZegoExpressEngine {
ZegoEngineProfile *profile = [[ZegoEngineProfile alloc] init];
profile.appID = appID; // 从 ZEGO 控制台获取
profile.scenario = ZegoScenarioHighQualityChatroom;
[ZegoExpressEngine createEngineWithProfile:profile eventHandler:self];
}
Web quickstart 使用 zego-express-engine-webrtc。播报入口不创建音频流、不调用 startPublishingStream,因此不需要麦克风权限:
npm install zego-express-engine-webrtcnpm install zego-express-engine-webrtc
import { ZegoExpressEngine } from "zego-express-engine-webrtc";
// appID: number,从 ZEGO 控制台项目信息获取
// server: 信令服务器地址,若使用 3.7.0 及以上版本 ZEGO Express SDK,可填写控制台获取的 Server 地址或者直接填空字符串
const zg = new ZegoExpressEngine(appID, "");import { ZegoExpressEngine } from "zego-express-engine-webrtc";
// appID: number,从 ZEGO 控制台项目信息获取
// server: 信令服务器地址,若使用 3.7.0 及以上版本 ZEGO Express SDK,可填写控制台获取的 Server 地址或者直接填空字符串
const zg = new ZegoExpressEngine(appID, "");
// 从 ZEGO 控制台获取
const APP_ID = 1234567890; // AppID,数字类型
const SERVER = ""; // 信令服务器地址,3.7.0+ 可填空字符串
// 从业务后台获取
const BASE_URL = "http://your-server-host:3000"; // 业务后台地址
const ROOM_ID = "room_xxx"; // RTC 房间 ID
const USER_ID = "user_xxx"; // 用户 ID
const DIGITAL_HUMAN_ID = "c4b56d5c-db98-4d91-86d4-5a97b507da97"; // 数字人 ID
const CONFIG_ID = "web"; // Web 端使用 web
// 模块级变量,供 roomStreamUpdate / remoteCameraStatusUpdate / 退出使用
let agentStreamId = "";
let agentInstanceId = "";// 从 ZEGO 控制台获取
const APP_ID = 1234567890; // AppID,数字类型
const SERVER = ""; // 信令服务器地址,3.7.0+ 可填空字符串
// 从业务后台获取
const BASE_URL = "http://your-server-host:3000"; // 业务后台地址
const ROOM_ID = "room_xxx"; // RTC 房间 ID
const USER_ID = "user_xxx"; // 用户 ID
const DIGITAL_HUMAN_ID = "c4b56d5c-db98-4d91-86d4-5a97b507da97"; // 数字人 ID
const CONFIG_ID = "web"; // Web 端使用 web
// 模块级变量,供 roomStreamUpdate / remoteCameraStatusUpdate / 退出使用
let agentStreamId = "";
let agentInstanceId = "";
const loginResult = await zg.loginRoom(roomID, token, {
userID,
userName,
});
if (!loginResult) {
throw new Error("登录 RTC 房间失败");
}const loginResult = await zg.loginRoom(roomID, token, {
userID,
userName,
});
if (!loginResult) {
throw new Error("登录 RTC 房间失败");
}
当前 quickstart 的公共初始化逻辑仍会调用 checkSystemRequirements 检查 WebRTC 和麦克风能力,以兼容语音通话入口;如果您的 Web 应用只实现数字人播报,可以移除麦克风检查。
集成数字人 SDK
数字人 SDK 已经发布在 Maven 仓库,可参考以下步骤将 SDK 集成到项目中。
添加 `maven` 配置
根据您的 Android Gradle 插件版本,选择对应的实现步骤。
修改您的 app 级别的 build.gradle 文件
dependencies {
...
// 数字人 SDK 依赖
implementation "im.zego:digitalmobile:1.3.0.43"
}dependencies {
...
// 数字人 SDK 依赖
implementation "im.zego:digitalmobile:1.3.0.43"
}支持 Android 6.0 (API 23) 及以上版本系统。
集成数字人 SDK
通知业务后台创建播报数字人实例
客户端调用业务后台的 POST /api/start-live-digital-human。业务后台收到请求后,会调用 ZEGO 的 CreateLiveDigitalHumanAgentInstance 接口创建播报数字人实例,接口详细参数请参考创建数字人播报实例 。RTC 模式下请求体至少包含 room_id、digital_human_id 和 config_id,不需要传 user_id 或 user_stream_id。
{
"room_id": "room_xxxxxxxx",
"digital_human_id": "digital_human_xxxxxxxx",
"config_id": "mobile"
}{
"room_id": "room_xxxxxxxx",
"digital_human_id": "digital_human_xxxxxxxx",
"config_id": "mobile"
}
digital_human_id 对应服务端 DigitalHuman 结构,其核心字段如下:
字段 说明 digital_human_id数字人 ID,用于指定数字人形象。 encode_code编码类型,仅支持 mobile(Android/iOS)和 web(Web)。Android 与 iOS 使用 mobile,Web 使用 web。
其中 Android 和 iOS 的 config_id 使用 mobile,Web 使用 web。DigitalHumanConfig.EncodeCode 合法取值仅 mobile 和 web。
业务后台在调用 ZEGO API 时,需要将 digital_human_id 和 config_id 构造为 DigitalHuman 对象:
{
"DigitalHumanId": "c4b56d5c-db98-4d91-86d4-5a97b507da97",
"ConfigId": "mobile",
"EncodeCode": "H264"
}{
"DigitalHumanId": "c4b56d5c-db98-4d91-86d4-5a97b507da97",
"ConfigId": "mobile",
"EncodeCode": "H264"
}
服务端成功响应中需要将以下信息返回给客户端:
字段 用途 agent_instance_id调用主动 TTS 和停止实例接口 agent_stream_idRTC 房间内数字人流的 ID agent_user_id数字人在 RTC 房间内的用户 ID digital_human_config初始化 Android/iOS 数字人 SDK 的配置
Android 使用 OkHttp 直接请求业务后台:
private void startLiveDigitalHuman(String baseUrl, String digitalHumanId,
String configId, String roomId) {
JSONObject bodyJson = new JSONObject();
bodyJson.put("digital_human_id", digitalHumanId);
bodyJson.put("config_id", configId);
bodyJson.put("room_id", roomId);
RequestBody body = RequestBody.create(
bodyJson.toString(), MediaType.parse("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url(baseUrl + "/api/start-live-digital-human")
.post(body)
.build();
new OkHttpClient().newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// 处理网络错误
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response)
throws IOException {
JSONObject result = new JSONObject(response.body().string());
if (result.getInt("code") == 0) {
String agentInstanceId = result.getString("agent_instance_id");
String agentStreamId = result.getString("agent_stream_id");
String digitalHumanConfig = result.getString("digital_human_config");
// 保存 agentInstanceId,后续用于 TTS 和 stop
startPlayingStream(agentStreamId);
initDigitalMobileSDK(digitalHumanConfig);
}
}
});
}private void startLiveDigitalHuman(String baseUrl, String digitalHumanId,
String configId, String roomId) {
JSONObject bodyJson = new JSONObject();
bodyJson.put("digital_human_id", digitalHumanId);
bodyJson.put("config_id", configId);
bodyJson.put("room_id", roomId);
RequestBody body = RequestBody.create(
bodyJson.toString(), MediaType.parse("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url(baseUrl + "/api/start-live-digital-human")
.post(body)
.build();
new OkHttpClient().newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// 处理网络错误
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response)
throws IOException {
JSONObject result = new JSONObject(response.body().string());
if (result.getInt("code") == 0) {
String agentInstanceId = result.getString("agent_instance_id");
String agentStreamId = result.getString("agent_stream_id");
String digitalHumanConfig = result.getString("digital_human_config");
// 保存 agentInstanceId,后续用于 TTS 和 stop
startPlayingStream(agentStreamId);
initDigitalMobileSDK(digitalHumanConfig);
}
}
});
}
iOS 使用 NSURLSession 直接请求业务后台,并保存响应中的实例 ID、流 ID 和数字人配置:
- (void)startLiveDigitalHuman {
NSURL *url = [NSURL URLWithString:
[baseURL stringByAppendingString:@"/api/start-live-digital-human"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
NSDictionary *params = @{
@"digital_human_id": digitalHumanId,
@"config_id": @"mobile",
@"room_id": roomID,
};
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:params options:0 error:nil];
[[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (error == nil && [result[@"code"] integerValue] == 0) {
agentInstanceId = result[@"agent_instance_id"];
agentStreamId = result[@"agent_stream_id"];
NSString *config = result[@"digital_human_config"];
[self initDigitalMobileSDK:config];
}
}] resume];
}- (void)startLiveDigitalHuman {
NSURL *url = [NSURL URLWithString:
[baseURL stringByAppendingString:@"/api/start-live-digital-human"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
NSDictionary *params = @{
@"digital_human_id": digitalHumanId,
@"config_id": @"mobile",
@"room_id": roomID,
};
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:params options:0 error:nil];
[[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (error == nil && [result[@"code"] integerValue] == 0) {
agentInstanceId = result[@"agent_instance_id"];
agentStreamId = result[@"agent_stream_id"];
NSString *config = result[@"digital_human_config"];
[self initDigitalMobileSDK:config];
}
}] resume];
}
Web 使用 fetch 直接请求业务后台。示例只传 RTC 房间 ID,因此不会传本地用户或本地流信息:
async function startLiveDigitalHuman(roomId: string) {
const response = await fetch(`${baseURL}/api/start-live-digital-human`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
digital_human_id: config.digitalHuman.id,
config_id: config.digitalHuman.configId,
room_id: roomId,
}),
});
const result = await response.json();
if (result.code !== 0) throw new Error(result.message);
return result;
}async function startLiveDigitalHuman(roomId: string) {
const response = await fetch(`${baseURL}/api/start-live-digital-human`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
digital_human_id: config.digitalHuman.id,
config_id: config.digitalHuman.configId,
room_id: roomId,
}),
});
const result = await response.json();
if (result.code !== 0) throw new Error(result.message);
return result;
}
用户进入房间(不推流)
播报数字人只需要登录 RTC 房间并接收数字人流,客户端不需要创建本地流或调用 startPublishingStream。
Android 需要先用 OkHttp 从业务后台获取 Token,再直接调用 ZEGO Express SDK 登录房间。登录成功后开启自定义视频渲染,再创建播报数字人实例:
String tokenUrl = baseUrl + "/api/zego-token?userId=" + userId;
Request tokenRequest = new Request.Builder().url(tokenUrl).get().build();
new OkHttpClient().newCall(tokenRequest).enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response)
throws IOException {
String token = new JSONObject(response.body().string()).getString("token");
ZegoEngineConfig engineConfig = new ZegoEngineConfig();
engineConfig.advancedConfig = new HashMap<String, String>() {{
//===== 数字人专用 ====//
put("set_audio_volume_ducking_mode", "1"); // 音量闪避开关,默认开启
put("enable_rnd_volume_adaptive", "true"); // 播放音量自适应开关,默认开启
put("sideinfo_callback_version", "3"); // 让 SEI 和 frame 一一对应,针对 ZEGO Express SDK 3.17 版本
put("sideinfo_bound_to_video_decoder", "true"); // 让 SEI 和 frame 一一对应,针对 ZEGO Express SDK 3.18 版本
}};
ZegoExpressEngine.setEngineConfig(engineConfig);
ZegoRoomConfig roomConfig = new ZegoRoomConfig();
roomConfig.isUserStatusNotify = true;
roomConfig.token = token;
ZegoExpressEngine.getEngine().loginRoom(
roomId, new ZegoUser(userId, userId), roomConfig,
(errorCode, extendedData) -> {
if (errorCode == 0) {
openExpressCustomRender();
startLiveDigitalHuman(baseUrl, digitalHumanId, "mobile", roomId);
}
});
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// 处理获取 Token 失败
}
});String tokenUrl = baseUrl + "/api/zego-token?userId=" + userId;
Request tokenRequest = new Request.Builder().url(tokenUrl).get().build();
new OkHttpClient().newCall(tokenRequest).enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response)
throws IOException {
String token = new JSONObject(response.body().string()).getString("token");
ZegoEngineConfig engineConfig = new ZegoEngineConfig();
engineConfig.advancedConfig = new HashMap<String, String>() {{
//===== 数字人专用 ====//
put("set_audio_volume_ducking_mode", "1"); // 音量闪避开关,默认开启
put("enable_rnd_volume_adaptive", "true"); // 播放音量自适应开关,默认开启
put("sideinfo_callback_version", "3"); // 让 SEI 和 frame 一一对应,针对 ZEGO Express SDK 3.17 版本
put("sideinfo_bound_to_video_decoder", "true"); // 让 SEI 和 frame 一一对应,针对 ZEGO Express SDK 3.18 版本
}};
ZegoExpressEngine.setEngineConfig(engineConfig);
ZegoRoomConfig roomConfig = new ZegoRoomConfig();
roomConfig.isUserStatusNotify = true;
roomConfig.token = token;
ZegoExpressEngine.getEngine().loginRoom(
roomId, new ZegoUser(userId, userId), roomConfig,
(errorCode, extendedData) -> {
if (errorCode == 0) {
openExpressCustomRender();
startLiveDigitalHuman(baseUrl, digitalHumanId, "mobile", roomId);
}
});
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// 处理获取 Token 失败
}
});
iOS 使用 NSURLSession 获取 Token,再直接调用 loginRoom。播报场景不调用 startPublishingStream:
- (void)loginRoomForBroadcast:(NSString *)token {
ZegoRoomConfig *roomConfig = [[ZegoRoomConfig alloc] init];
roomConfig.isUserStatusNotify = YES;
roomConfig.token = token;
ZegoUser *user = [[ZegoUser alloc] initWithUserID:userID userName:userID];
[[ZegoExpressEngine sharedEngine] loginRoom:roomID
user:user
config:roomConfig
callback:^(int errorCode, NSDictionary *extendedData) {
if (errorCode == 0) {
[self enableCustomVideoRender];
[self startLiveDigitalHuman];
}
}];
}- (void)loginRoomForBroadcast:(NSString *)token {
ZegoRoomConfig *roomConfig = [[ZegoRoomConfig alloc] init];
roomConfig.isUserStatusNotify = YES;
roomConfig.token = token;
ZegoUser *user = [[ZegoUser alloc] initWithUserID:userID userName:userID];
[[ZegoExpressEngine sharedEngine] loginRoom:roomID
user:user
config:roomConfig
callback:^(int errorCode, NSDictionary *extendedData) {
if (errorCode == 0) {
[self enableCustomVideoRender];
[self startLiveDigitalHuman];
}
}];
}
Web 直接登录房间,播报场景不创建音频流或推送本地流:
// 1. 从业务后台获取 Token
const tokenRes = await fetch(`${BASE_URL}/api/zego-token?userId=${USER_ID}&roomId=${ROOM_ID}`);
const { token } = await tokenRes.json();
// 2. 登录 RTC 房间
await zg.loginRoom(ROOM_ID, token, { userID: USER_ID, userName: USER_ID });
// 3. 创建播报数字人实例
const result = await startLiveDigitalHuman(ROOM_ID);
// 4. 提取并保存 agent_stream_id 和 agent_instance_id,供后续拉流、TTS 和退出使用
agentStreamId = result.agent_stream_id;
agentInstanceId = result.agent_instance_id;// 1. 从业务后台获取 Token
const tokenRes = await fetch(`${BASE_URL}/api/zego-token?userId=${USER_ID}&roomId=${ROOM_ID}`);
const { token } = await tokenRes.json();
// 2. 登录 RTC 房间
await zg.loginRoom(ROOM_ID, token, { userID: USER_ID, userName: USER_ID });
// 3. 创建播报数字人实例
const result = await startLiveDigitalHuman(ROOM_ID);
// 4. 提取并保存 agent_stream_id 和 agent_instance_id,供后续拉流、TTS 和退出使用
agentStreamId = result.agent_stream_id;
agentInstanceId = result.agent_instance_id;
初始化数字人 SDK 和自定义渲染
Android 和 iOS 需要将 ZEGO Express SDK 收到的原始视频帧与 SEI 数据传递给数字人 SDK,再由数字人 SDK 渲染数字人画面。必须在调用 startPlayingStream 之前开启自定义视频渲染。
Android 初始化数字人 SDK。示例中涉及三个视图成员变量,需提前在布局中声明并初始化:
digitalView:用于承载数字人 SDK 渲染画面的容器,数字人画面最终绘制在该 View 上,初始化时通过 attach 传给数字人 SDK。
loadingView:加载中占位视图,数字人首帧渲染前展示,首帧绘制回调中隐藏。
digitalPic:静态封面占位图,与 loadingView 类似在首帧绘制回调中隐藏,避免画面停留在静态图。
private void initDigitalMobileSDK(String digitalHumanConfig) {
digitalMobileSDK = ZegoDigitalHuman.create(this);
digitalMobileSDK.start(digitalHumanConfig,
new IZegoDigitalMobile.ZegoDigitalMobileListener() {
@Override
public void onSurfaceFirstFrameDraw() {
loadingView.setVisibility(View.GONE);
digitalPic.setVisibility(View.GONE);
}
});
digitalMobileSDK.attach(digitalView);
}private void initDigitalMobileSDK(String digitalHumanConfig) {
digitalMobileSDK = ZegoDigitalHuman.create(this);
digitalMobileSDK.start(digitalHumanConfig,
new IZegoDigitalMobile.ZegoDigitalMobileListener() {
@Override
public void onSurfaceFirstFrameDraw() {
loadingView.setVisibility(View.GONE);
digitalPic.setVisibility(View.GONE);
}
});
digitalMobileSDK.attach(digitalView);
}
在 openExpressCustomRender 中配置 RAW_DATA,并将回调数据转发给数字人 SDK。其中 onPlayerSyncRecvSEI 用于接收 ZEGO Express SDK 解析出的 SEI(Supplemental Enhancement Information)数据并转发给数字人 SDK,SEI 中携带数字人驱动所需的口型、表情等附加信息,是数字人口型准确的关键。关于 SEI 的详细说明请参考SEI 高级功能 。
private void openExpressCustomRender() {
ZegoCustomVideoRenderConfig renderConfig = new ZegoCustomVideoRenderConfig();
renderConfig.bufferType = ZegoVideoBufferType.RAW_DATA;
renderConfig.frameFormatSeries = ZegoVideoFrameFormatSeries.RGB;
renderConfig.enableEngineRender = false;
ZegoExpressEngine.getEngine().enableCustomVideoRender(true, renderConfig);
ZegoExpressEngine.getEngine().setCustomVideoRenderHandler(
new IZegoCustomVideoRenderHandler() {
@Override
public void onRemoteVideoFrameRawData(
ByteBuffer[] data, int[] dataLength, ZegoVideoFrameParam param,
String streamID) {
IZegoDigitalMobile.ZegoVideoFrameParam digitalParam =
new IZegoDigitalMobile.ZegoVideoFrameParam();
digitalParam.format =
IZegoDigitalMobile.ZegoVideoFrameFormat.getZegoVideoFrameFormat(
param.format.value());
digitalParam.height = param.height;
digitalParam.width = param.width;
digitalParam.rotation = param.rotation;
for (int i = 0; i < 4; i++) {
digitalParam.strides[i] = param.strides[i];
}
if (digitalMobileSDK != null) {
digitalMobileSDK.onRemoteVideoFrameRawData(
data, dataLength, digitalParam, streamID);
}
}
});
ZegoExpressEngine.getEngine().setEventHandler(new IZegoEventHandler() {
@Override
public void onPlayerSyncRecvSEI(String streamID, byte[] data) {
if (digitalMobileSDK != null) {
digitalMobileSDK.onPlayerSyncRecvSEI(streamID, data);
}
}
});
}private void openExpressCustomRender() {
ZegoCustomVideoRenderConfig renderConfig = new ZegoCustomVideoRenderConfig();
renderConfig.bufferType = ZegoVideoBufferType.RAW_DATA;
renderConfig.frameFormatSeries = ZegoVideoFrameFormatSeries.RGB;
renderConfig.enableEngineRender = false;
ZegoExpressEngine.getEngine().enableCustomVideoRender(true, renderConfig);
ZegoExpressEngine.getEngine().setCustomVideoRenderHandler(
new IZegoCustomVideoRenderHandler() {
@Override
public void onRemoteVideoFrameRawData(
ByteBuffer[] data, int[] dataLength, ZegoVideoFrameParam param,
String streamID) {
IZegoDigitalMobile.ZegoVideoFrameParam digitalParam =
new IZegoDigitalMobile.ZegoVideoFrameParam();
digitalParam.format =
IZegoDigitalMobile.ZegoVideoFrameFormat.getZegoVideoFrameFormat(
param.format.value());
digitalParam.height = param.height;
digitalParam.width = param.width;
digitalParam.rotation = param.rotation;
for (int i = 0; i < 4; i++) {
digitalParam.strides[i] = param.strides[i];
}
if (digitalMobileSDK != null) {
digitalMobileSDK.onRemoteVideoFrameRawData(
data, dataLength, digitalParam, streamID);
}
}
});
ZegoExpressEngine.getEngine().setEventHandler(new IZegoEventHandler() {
@Override
public void onPlayerSyncRecvSEI(String streamID, byte[] data) {
if (digitalMobileSDK != null) {
digitalMobileSDK.onPlayerSyncRecvSEI(streamID, data);
}
}
});
}
iOS 在 startPlayingStream 前调用 enableCustomVideoRender,并在数字人事件处理器中转发视频帧和 SEI:
- (BOOL)enableCustomVideoRender {
ZegoCustomVideoRenderConfig *renderConfig =
[[ZegoCustomVideoRenderConfig alloc] init];
renderConfig.bufferType = ZegoVideoBufferTypeRawData;
renderConfig.frameFormatSeries = ZegoVideoFrameFormatSeriesRGB;
renderConfig.enableEngineRender = NO;
ZegoExpressEngine *engine = [ZegoExpressEngine sharedEngine];
if (!engine) {
return NO;
}
[engine enableCustomVideoRender:YES config:renderConfig];
[engine setCustomVideoRenderHandler:self];
return YES;
}
- (void)onRemoteVideoFrameRawData:(unsigned char **)data
dataLength:(unsigned int *)dataLength
param:(ZegoVideoFrameParam *)param
streamID:(NSString *)streamID {
ZDMVideoFrameParam *digitalParam = [[ZDMVideoFrameParam alloc] init];
digitalParam.format = (ZDMVideoFrameFormat)param.format;
digitalParam.width = param.size.width;
digitalParam.height = param.size.height;
digitalParam.rotation = param.rotation;
for (int i = 0; i < 4; i++) {
[digitalParam setStride:param.strides[i] atIndex:i];
}
if (self.digitalMobile) {
[self.digitalMobile onRemoteVideoFrameRawData:data
dataLength:dataLength
param:digitalParam
streamID:streamID];
}
}
- (void)onPlayerSyncRecvSEI:(NSData *)data streamID:(NSString *)streamID {
if (self.digitalMobile) {
[self.digitalMobile onPlayerSyncRecvSEI:streamID data:data];
}
}- (BOOL)enableCustomVideoRender {
ZegoCustomVideoRenderConfig *renderConfig =
[[ZegoCustomVideoRenderConfig alloc] init];
renderConfig.bufferType = ZegoVideoBufferTypeRawData;
renderConfig.frameFormatSeries = ZegoVideoFrameFormatSeriesRGB;
renderConfig.enableEngineRender = NO;
ZegoExpressEngine *engine = [ZegoExpressEngine sharedEngine];
if (!engine) {
return NO;
}
[engine enableCustomVideoRender:YES config:renderConfig];
[engine setCustomVideoRenderHandler:self];
return YES;
}
- (void)onRemoteVideoFrameRawData:(unsigned char **)data
dataLength:(unsigned int *)dataLength
param:(ZegoVideoFrameParam *)param
streamID:(NSString *)streamID {
ZDMVideoFrameParam *digitalParam = [[ZDMVideoFrameParam alloc] init];
digitalParam.format = (ZDMVideoFrameFormat)param.format;
digitalParam.width = param.size.width;
digitalParam.height = param.size.height;
digitalParam.rotation = param.rotation;
for (int i = 0; i < 4; i++) {
[digitalParam setStride:param.strides[i] atIndex:i];
}
if (self.digitalMobile) {
[self.digitalMobile onRemoteVideoFrameRawData:data
dataLength:dataLength
param:digitalParam
streamID:streamID];
}
}
- (void)onPlayerSyncRecvSEI:(NSData *)data streamID:(NSString *)streamID {
if (self.digitalMobile) {
[self.digitalMobile onPlayerSyncRecvSEI:streamID data:data];
}
}
Web 端不集成数字人 SDK,无需自定义渲染配置,直接使用 ZEGO Express SDK 播放数字人视频流。
拉取数字人流
创建实例后,客户端使用服务端返回的 agent_stream_id 拉取数字人流。不同平台 quickstart 的拉流时机略有不同:Android 在创建实例接口成功后直接拉流;iOS 和 Web 通过房间流更新回调,匹配目标流后再拉流。
Android quickstart 在创建播报数字人实例成功后直接拉取 agent_stream_id,不使用 onRoomStreamUpdate:
// 设置拉流缓冲区间,缓解网络抖动导致的画面卡顿;min 为起始缓冲(ms),max 为最大缓冲(ms)
ZegoExpressEngine.getEngine()
.setPlayStreamBufferIntervalRange(agent_stream_id, 100, 2000);
ZegoExpressEngine.getEngine().startPlayingStream(agent_stream_id);// 设置拉流缓冲区间,缓解网络抖动导致的画面卡顿;min 为起始缓冲(ms),max 为最大缓冲(ms)
ZegoExpressEngine.getEngine()
.setPlayStreamBufferIntervalRange(agent_stream_id, 100, 2000);
ZegoExpressEngine.getEngine().startPlayingStream(agent_stream_id);
监听 onRoomStreamUpdate,仅在房间内出现服务端返回的 agentStreamId 时开始拉流:
- (void)startPlayStream:(NSString *)streamId {
[[ZegoExpressEngine sharedEngine]
setPlayStreamBufferIntervalRange:streamId min:0 max:2000];
[[ZegoExpressEngine sharedEngine] startPlayingStream:streamId];
}
- (void)onRoomStreamUpdate:(ZegoUpdateType)updateType
streamList:(NSArray<ZegoStream *> *)streamList
extendedData:(nullable NSDictionary *)extendedData
roomID:(NSString *)roomID {
if (updateType == ZegoUpdateTypeAdd) {
for (ZegoStream *stream in streamList) {
if ([stream.streamID isEqualToString:self.agentStreamId]) {
[self startPlayStream:self.agentStreamId];
break;
}
}
} else if (updateType == ZegoUpdateTypeDelete) {
for (ZegoStream *stream in streamList) {
if ([stream.streamID isEqualToString:self.agentStreamId]) {
[[ZegoExpressEngine sharedEngine] stopPlayingStream:stream.streamID];
}
}
}
}- (void)startPlayStream:(NSString *)streamId {
[[ZegoExpressEngine sharedEngine]
setPlayStreamBufferIntervalRange:streamId min:0 max:2000];
[[ZegoExpressEngine sharedEngine] startPlayingStream:streamId];
}
- (void)onRoomStreamUpdate:(ZegoUpdateType)updateType
streamList:(NSArray<ZegoStream *> *)streamList
extendedData:(nullable NSDictionary *)extendedData
roomID:(NSString *)roomID {
if (updateType == ZegoUpdateTypeAdd) {
for (ZegoStream *stream in streamList) {
if ([stream.streamID isEqualToString:self.agentStreamId]) {
[self startPlayStream:self.agentStreamId];
break;
}
}
} else if (updateType == ZegoUpdateTypeDelete) {
for (ZegoStream *stream in streamList) {
if ([stream.streamID isEqualToString:self.agentStreamId]) {
[[ZegoExpressEngine sharedEngine] stopPlayingStream:stream.streamID];
}
}
}
}
监听 roomStreamUpdate,匹配服务端返回的 agentStreamId 后创建远程流视图。agentStreamId 来自创建实例接口返回的 agent_stream_id,已在登录房间后从响应中提取:
let remoteView: any = null;
zg.on(
"roomStreamUpdate",
async (
roomID: string,
updateType: "DELETE" | "ADD",
streamList: ZegoStreamList[],
) => {
if (updateType === "ADD" && streamList.length > 0) {
for (const stream of streamList) {
// 仅拉取数字人流,过滤房间内其他流
if (stream.streamID !== agentStreamId) continue;
const mediaStream = await zg.startPlayingStream(stream.streamID);
remoteView = await zg.createRemoteStreamView(mediaStream);
remoteView?.playAudio();
break;
}
}
},
);
// 数字人视频流由服务端推送,相机状态变为 OPEN 表示视频流已就绪,此时再播放画面
zg.on(
"remoteCameraStatusUpdate",
(streamID: string, status: "OPEN" | "MUTE") => {
if (streamID === agentStreamId && status === "OPEN") {
remoteView?.playVideo("remoteStreamView");
}
}
);let remoteView: any = null;
zg.on(
"roomStreamUpdate",
async (
roomID: string,
updateType: "DELETE" | "ADD",
streamList: ZegoStreamList[],
) => {
if (updateType === "ADD" && streamList.length > 0) {
for (const stream of streamList) {
// 仅拉取数字人流,过滤房间内其他流
if (stream.streamID !== agentStreamId) continue;
const mediaStream = await zg.startPlayingStream(stream.streamID);
remoteView = await zg.createRemoteStreamView(mediaStream);
remoteView?.playAudio();
break;
}
}
},
);
// 数字人视频流由服务端推送,相机状态变为 OPEN 表示视频流已就绪,此时再播放画面
zg.on(
"remoteCameraStatusUpdate",
(streamID: string, status: "OPEN" | "MUTE") => {
if (streamID === agentStreamId && status === "OPEN") {
remoteView?.playVideo("remoteStreamView");
}
}
);
以上为 RTC 模式拉流方式。若使用 CDN 模式,客户端无需登录 RTC 房间,也不需要按上述流程拉取 RTC 流,而是直接通过通用播放器(如 HLS / FLV 播放器)拉取业务后台返回的 cdn_url 即可观看数字人播报。CDN 模式不需要集成数字人 SDK。
主动让数字人播报文本
数字人实例创建成功后,客户端调用业务后台 POST /api/send-agent-instance-tts,传入实例 ID 和文本:
{
"agent_instance_id": "2075416950128779264",
"text": "尊敬的开发者你好,欢迎使用 ZEGO AI Agent。"
}{
"agent_instance_id": "2075416950128779264",
"text": "尊敬的开发者你好,欢迎使用 ZEGO AI Agent。"
}
text 最大长度不超过 300 个字符。还可以按业务需要传入 add_history、priority、same_priority_option 等参数。详细说明请参考主动调用 TTS 。
播报数字人实例在空闲(无播报任务)超过 900 秒后会自动销毁,业务侧可通过 MaxIdleTime 调整该空闲超时时间。如需长时间保活,请定期调用主动 TTS 或调大 MaxIdleTime。
Android 使用 OkHttp 直接调用 TTS 接口:
JSONObject bodyJson = new JSONObject();
bodyJson.put("agent_instance_id", agentInstanceId);
bodyJson.put("text", text);
Request request = new Request.Builder()
.url(baseUrl + "/api/send-agent-instance-tts")
.post(RequestBody.create(bodyJson.toString(),
MediaType.parse("application/json; charset=utf-8")))
.build();
new OkHttpClient().newCall(request).enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response)
throws IOException {
JSONObject result = new JSONObject(response.body().string());
if (result.getInt("code") != 0) {
// 处理 TTS 请求失败
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// 处理网络错误
}
});JSONObject bodyJson = new JSONObject();
bodyJson.put("agent_instance_id", agentInstanceId);
bodyJson.put("text", text);
Request request = new Request.Builder()
.url(baseUrl + "/api/send-agent-instance-tts")
.post(RequestBody.create(bodyJson.toString(),
MediaType.parse("application/json; charset=utf-8")))
.build();
new OkHttpClient().newCall(request).enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response)
throws IOException {
JSONObject result = new JSONObject(response.body().string());
if (result.getInt("code") != 0) {
// 处理 TTS 请求失败
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// 处理网络错误
}
});
NSDictionary *params = @{
@"agent_instance_id": agentInstanceId,
@"text": text,
};
NSURL *url = [NSURL URLWithString:
[baseURL stringByAppendingString:@"/api/send-agent-instance-tts"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:params options:0 error:nil];
[[NSURLSession.sharedSession dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (error == nil && [result[@"code"] integerValue] == 0) {
NSLog(@"播报发送成功");
}
}] resume];NSDictionary *params = @{
@"agent_instance_id": agentInstanceId,
@"text": text,
};
NSURL *url = [NSURL URLWithString:
[baseURL stringByAppendingString:@"/api/send-agent-instance-tts"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:params options:0 error:nil];
[[NSURLSession.sharedSession dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (error == nil && [result[@"code"] integerValue] == 0) {
NSLog(@"播报发送成功");
}
}] resume];
const response = await fetch(`${baseURL}/api/send-agent-instance-tts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agent_instance_id: agentInstanceId,
text,
}),
});
const result = await response.json();
if (result.code !== 0) throw new Error(result.message);const response = await fetch(`${baseURL}/api/send-agent-instance-tts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agent_instance_id: agentInstanceId,
text,
}),
});
const result = await response.json();
if (result.code !== 0) throw new Error(result.message);
Web quickstart 会在播报模式下显示 TTS 输入框,并将服务端返回的 agent_instance_id 保存到页面状态。
退出房间结束播报
退出时需要停止 Agent 实例、停止拉流、退出 RTC 房间并销毁客户端 SDK。无论停止接口是否成功,都应释放 RTC 资源,避免房间和引擎残留。
@Override
protected void onDestroy() {
super.onDestroy();
JSONObject bodyJson = new JSONObject();
bodyJson.put("agent_instance_id", agentInstanceId);
Request request = new Request.Builder()
.url(baseUrl + "/api/stop")
.post(RequestBody.create(bodyJson.toString(),
MediaType.parse("application/json; charset=utf-8")))
.build();
new OkHttpClient().newCall(request).enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
ZegoExpressEngine.getEngine().stopPlayingStream(agentStreamId);
ZegoExpressEngine.getEngine().logoutRoom();
digitalMobile.stop();
ZegoExpressEngine.destroyEngine(null);
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// 即使请求失败,也应释放本地 RTC 和数字人 SDK 资源
ZegoExpressEngine.getEngine().logoutRoom();
digitalMobile.stop();
ZegoExpressEngine.destroyEngine(null);
}
});
}@Override
protected void onDestroy() {
super.onDestroy();
JSONObject bodyJson = new JSONObject();
bodyJson.put("agent_instance_id", agentInstanceId);
Request request = new Request.Builder()
.url(baseUrl + "/api/stop")
.post(RequestBody.create(bodyJson.toString(),
MediaType.parse("application/json; charset=utf-8")))
.build();
new OkHttpClient().newCall(request).enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
ZegoExpressEngine.getEngine().stopPlayingStream(agentStreamId);
ZegoExpressEngine.getEngine().logoutRoom();
digitalMobile.stop();
ZegoExpressEngine.destroyEngine(null);
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// 即使请求失败,也应释放本地 RTC 和数字人 SDK 资源
ZegoExpressEngine.getEngine().logoutRoom();
digitalMobile.stop();
ZegoExpressEngine.destroyEngine(null);
}
});
}
- (void)stopLiveDigitalHuman {
NSDictionary *params = @{ @"agent_instance_id": agentInstanceId };
NSURL *url = [NSURL URLWithString:
[baseURL stringByAppendingString:@"/api/stop"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:params options:0 error:nil];
[[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
[[ZegoExpressEngine sharedEngine] stopPlayingStream:agentStreamId];
[[ZegoExpressEngine sharedEngine] logoutRoom];
[digitalMobile stop];
[ZegoExpressEngine destroyEngine:nil];
}] resume];
}- (void)stopLiveDigitalHuman {
NSDictionary *params = @{ @"agent_instance_id": agentInstanceId };
NSURL *url = [NSURL URLWithString:
[baseURL stringByAppendingString:@"/api/stop"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:params options:0 error:nil];
[[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
[[ZegoExpressEngine sharedEngine] stopPlayingStream:agentStreamId];
[[ZegoExpressEngine sharedEngine] logoutRoom];
[digitalMobile stop];
[ZegoExpressEngine destroyEngine:nil];
}] resume];
}
async function logoutRoom() {
await fetch(`${baseURL}/api/stop`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_instance_id: agentInstanceId }),
});
zg.stopPlayingStream(agentStreamId);
zg.logoutRoom(roomID);
agentInstanceId = "";
}async function logoutRoom() {
await fetch(`${baseURL}/api/stop`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_instance_id: agentInstanceId }),
});
zg.stopPlayingStream(agentStreamId);
zg.logoutRoom(roomID);
agentInstanceId = "";
}
RTC 和 CDN 模式
本文以 RTC 模式为例:客户端登录房间并拉取 agent_stream_id。如果需要大规模直播,可以使用 CDN 模式:
模式 创建实例参数 客户端播放方式 适用场景 RTC room_idZEGO Express SDK 拉取 RTC 流 低延迟、小范围互动 CDN cdn_url使用播放器拉取 CDN 流 大规模直播
RTC 和 CDN 模式都使用 agent_instance_id 调用主动 TTS 和停止实例接口。CDN 模式不需要客户端登录 RTC 房间,也不需要集成数字人 SDK。CDN 拉流方式:可以用任何支持 CDN 播放的方式进行拉流,不做特殊说明。
数字人支持 RTC、CDN 拉流,如若需要让大规模的用户观看数字人播报内容,例如电商直播、直播课堂等场景,可以使用 CDN 直播模式。CDN 模式下 Web 端无需集成 ZEGO Express SDK,使用任意支持 HLS/FLV 的 Web 播放器(如 hls.js、video.js)拉取 cdn_url 即可。
监听回调
请监听 ZEGO Express SDK 的房间登录、拉流状态和错误回调,并在业务后台记录 agent_instance_id、agent_stream_id、request_id 和错误信息,便于定位创建实例、拉流或 TTS 失败原因。其中 request_id 由 ZEGO 服务端返回,是排查服务端链路问题的关键标识,请务必在出现异常时连同上述信息一起反馈给 ZEGO 技术支持。
接入测试过程中,强烈建议监听业务后台接收回调(Event 为 Exception 的事件),通过 Data.Code 和 Data.Message 快速定位 LLM/TTS 等参数配置问题。回调与错误码详情请参考接收回调 和异常事件错误码 。
如果数字人画面停留在静态图,请重点检查:数字人配置是否有效、agent_stream_id 是否正确、是否在 startPlayingStream 前开启了自定义视频渲染,以及是否将视频帧和 SEI 数据传给了数字人 SDK。
排查清单
接入过程中如遇异常,可对照以下清单逐项排查。
现象 排查方向 登录房间失败 检查 Token 是否有效、AppID 是否匹配、网络是否可达 ZEGO 服务。 创建实例返回失败 确认 digital_human_id、config_id(Android 为 mobile)、room_id 是否正确,业务后台签名是否有效。 拉不到流 / 无画面 确认 agent_stream_id 与服务端返回一致;检查是否在 startPlayingStream 前调用 enableCustomVideoRender。 画面停留在静态图 检查 digitalView/loadingView/digitalPic 是否正确 attach;确认视频帧和 SEI 已转发给 digitalMobileSDK;真机运行而非模拟器。 口型不准 确认 SEI 数据已通过 onPlayerSyncRecvSEI 转发;确认 advanceConfig 中 SEI 相关参数与 ZEGO Express SDK 版本匹配。 TTS 不播报 确认 agent_instance_id 正确、text ≤ 300 字符、实例未因 900 秒空闲被销毁。
现象 排查方向 登录房间失败 检查 Token 是否有效、AppID 是否匹配、网络是否可达 ZEGO 服务。 创建实例返回失败 确认 digital_human_id、config_id(iOS 为 mobile)、room_id 是否正确,业务后台签名是否有效。 拉不到流 / 无画面 在 onRoomStreamUpdate 中匹配 agentStreamId 后再拉流;确认 enableCustomVideoRender 在 startPlayingStream 前调用。 画面停留在静态图 确认视频帧和 SEI 已转发给 digitalMobile;Info.plist 不需要麦克风权限但需保证网络权限。 TTS 不播报 确认 agent_instance_id 正确、text ≤ 300 字符、实例未因 900 秒空闲被销毁。
现象 排查方向 登录房间失败 检查 Token 是否有效、AppID 是否匹配;3.7.0 及以上版本 server 可填空字符串。 创建实例返回失败 确认 digital_human_id、config_id(Web 为 web)、room_id 是否正确,业务后台签名是否有效。 无画面 / 无声音 在 roomStreamUpdate 中按 agentStreamId 过滤后再 startPlayingStream;等待 remoteCameraStatusUpdate 为 OPEN 后再 playVideo。 视频容器不显示 确认 playVideo 传入的容器 ID 为 remoteStreamView(注意拼写)。 TTS 不播报 确认 agent_instance_id 正确、text ≤ 300 字符、实例未因 900 秒空闲被销毁。
更多错误码请参考异常事件错误码 ,回调监听请参考接收回调 。