This document describes how to quickly integrate the client SDKs (ZEGO Express SDK and Digital Human SDK) to implement live digital human broadcasting.
Unlike digital human video calls, digital human broadcasting is a one-way viewing scenario - users do not interact with the digital human, they simply play stream the digital human's broadcast content for viewing.
The client only needs to log in to an RTC room and play stream; it does not need to capture or publish stream the user's audio and video. After creating a broadcast instance, the server can proactively make the digital human broadcast specified text via the TTS API.
Suitable for scenarios such as digital human live streaming, news broadcasting, event hosting, and scripted broadcasts.
This document describes how to quickly integrate the client SDK (ZEGO Express SDK) to implement live digital human broadcasting.
Unlike digital human video calls, digital human broadcasting is a one-way viewing scenario - users do not interact with the digital human, they simply play stream the digital human's broadcast content for viewing.
The client only needs to log in to an RTC room and play stream; it does not need to capture or publish stream the user's audio and video. After creating a broadcast instance, the server can proactively make the digital human broadcast specified text via the TTS API.
Suitable for scenarios such as digital human live streaming, news broadcasting, event hosting, and scripted broadcasts.
Differences from Digital Human Video Calls
Item
Digital Human Video Call
Live Digital Human Broadcasting
Interaction mode
Two-way: the digital human responds after the user speaks
One-way: users only view the digital human's broadcast content
Use cases
Conversational AI, AI customer service, digital human tutor
Digital human live streaming, news broadcasting
Does the client need to capture user audio?
Yes
No
Digital human driving mechanism
After receiving user audio, LLM outputs a response and drives the digital human via TTS
Broadcast content is fully controlled by the server, driving the digital human via TTS
You have created a project in the ZEGOCLOUD Console and obtained a valid AppID and ServerSecret (for server API signing and RTC Token04 generation). For details, see Console - Project Information.
You have contacted ZEGOCLOUD Technical Support to enable the Digital Human PaaS service and the relevant API permissions.
You have obtained a valid digital_human_id (for testing, you can use the public ID: 63c3aa64-1d80-4b04-a0be-1c65614eb7eb).
You have integrated the broadcast digital human server APIs as described in Server Quick Start Guide.
You have downloaded the ZEGO Express SDK optimized for AI Agent from the download page and integrated it into your project.
You have contacted ZEGOCLOUD Technical Support to obtain the ZEGO Express SDK optimized for AI Agent and integrated it into your project.
Note
Digital human broadcasting does not require microphone permissions and does not publish local streams. If the same app also includes voice call or digital human video call entries, those entries still need to request microphone permissions as described in their respective documentation.
Sample Code
The following is the business backend sample code for integrating the Conversational AI Agent API. You can refer to the sample code to implement your own business logic.
Business Backend Sample Code
Includes obtaining ZEGO Token, registering an agent, creating a broadcast digital human instance, proactively invoking TTS, and stopping instances.
The following is the client sample code. You can refer to the sample code to implement your own business logic.
Android Client Sample Code
The entry point is StartLiveDigitalHumanCall, corresponding to video.LiveDigitalHumanActivity. It includes login, play stream, digital human rendering, proactive TTS, and leaving the room.
iOS Client Sample Code
The entry point is StartLiveDigitalHuman. It completes login, play stream, digital human rendering, proactive TTS, and leaving the room through the broadcast mode on the digital human page.
Web Client Sample Code
The entry point is Start Live Digital Human. It includes login, play stream, proactive TTS, and leaving the room.
The following video demonstrates how to run through the server and client (Web) sample code and interact with the agent via voice.
The following video demonstrates how to run through the server and client (iOS) sample code and interact with the agent via voice.
Overall Business Flow
On the server side, refer to the Server Quick Start Guide to run through the business backend sample code and deploy the business backend.
Integrate the Conversational AI Agent API to manage agents.
On the client side, run through the sample code.
Create and manage agents through the business backend.
The Android quickstart retains RECORD_AUDIO in the Manifest to support both voice calls and digital human video calls. When entering LiveDigitalHumanActivity, the permission is not requested and no local audio stream is created. If only the broadcast entry is needed, this permission can be removed.
The broadcast scenario does not require runtime microphone permission. After entering the page, you can directly initialize the ZEGO Express SDK:
If running on an Android emulator, some audio/video capabilities and digital human rendering depend on hardware decoding and GPU acceleration on real devices, which may cause the display not to work properly or result in a black screen. It is recommended to debug and verify the digital human broadcast effect on a real device.
The iOS quickstart integrates the SDK via CocoaPods:
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'
end
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'
end
Digital human broadcasting is a one-way viewing scenario. You do not need to add NSMicrophoneUsageDescription in Info.plist or call requestRecordPermission:. If the same app also supports digital human video calls, you can keep the microphone permission declaration required for video calls.
When initializing the broadcast page, call ZegoExpressEngine directly without requesting microphone permission:
The Web quickstart uses zego-express-engine-webrtc. The broadcast entry does not create audio streams or call startPublishingStream, so no microphone permission is needed:
npm install zego-express-engine-webrtc
npm install zego-express-engine-webrtc
import { ZegoExpressEngine } from "zego-express-engine-webrtc";
// appID: number, obtained from ZEGOCLOUD Console project information
// server: signaling server address; for ZEGO Express SDK 3.7.0 and above, you can use the Server address from the console or leave it as an empty string
const zg = new ZegoExpressEngine(appID, "");
import { ZegoExpressEngine } from "zego-express-engine-webrtc";
// appID: number, obtained from ZEGOCLOUD Console project information
// server: signaling server address; for ZEGO Express SDK 3.7.0 and above, you can use the Server address from the console or leave it as an empty string
const zg = new ZegoExpressEngine(appID, "");
Below are commonly used variable declarations/configurations for the Web client:
// Obtain from ZEGOCLOUD Console
const APP_ID = 1234567890; // AppID, number type
const SERVER = ""; // Signaling server address, can be empty string for 3.7.0+
// Obtain from business backend
const BASE_URL = "http://your-server-host:3000"; // Business backend address
const ROOM_ID = "room_xxx"; // RTC room ID
const USER_ID = "user_xxx"; // User ID
const DIGITAL_HUMAN_ID = "63c3aa64-1d80-4b04-a0be-1c65614eb7eb"; // Digital human ID
const CONFIG_ID = "web"; // Use "web" for Web client
// Module-level variables, used by roomStreamUpdate / remoteCameraStatusUpdate / exit
let agentStreamId = "";
let agentInstanceId = "";
// Obtain from ZEGOCLOUD Console
const APP_ID = 1234567890; // AppID, number type
const SERVER = ""; // Signaling server address, can be empty string for 3.7.0+
// Obtain from business backend
const BASE_URL = "http://your-server-host:3000"; // Business backend address
const ROOM_ID = "room_xxx"; // RTC room ID
const USER_ID = "user_xxx"; // User ID
const DIGITAL_HUMAN_ID = "63c3aa64-1d80-4b04-a0be-1c65614eb7eb"; // Digital human ID
const CONFIG_ID = "web"; // Use "web" for Web client
// Module-level variables, used by roomStreamUpdate / remoteCameraStatusUpdate / exit
let agentStreamId = "";
let agentInstanceId = "";
const loginResult = await zg.loginRoom(roomID, token, {
userID,
userName,
});
if (!loginResult) {
throw new Error("Failed to log in to RTC room");
}
const loginResult = await zg.loginRoom(roomID, token, {
userID,
userName,
});
if (!loginResult) {
throw new Error("Failed to log in to RTC room");
}
The current quickstart's common initialization logic still calls checkSystemRequirements to check WebRTC and microphone capabilities for compatibility with the voice call entry. If your Web app only implements digital human broadcasting, you can remove the microphone check.
Integrating the Digital Human SDK
The Digital Human SDK has been published to the Maven repository. Refer to the following steps to integrate it into your project.
1
Add `maven` configuration
Choose the appropriate steps based on your Android Gradle plugin version.
2
Modify your app-level build.gradle file
dependencies {
...
// Digital Human SDK dependency
implementation "im.zego:digitalmobile:1.3.0.43"
}
dependencies {
...
// Digital Human SDK dependency
implementation "im.zego:digitalmobile:1.3.0.43"
}
Note
Supports Android 6.0 (API 23) and above.
Integrating the Digital Human SDK
Note
Supports iOS 12 and above.
Notify the Business Backend to Create a Broadcast Digital Human Instance
The client calls the business backend's POST /api/start-live-digital-human. Upon receiving the request, the business backend calls ZEGOCLOUD's CreateLiveDigitalHumanAgentInstance API to create a broadcast digital human instance. For detailed parameters, see Create Broadcast Digital Human Instance. In RTC mode, the request body must at least contain room_id, digital_human_id, and config_id; user_id and user_stream_id are not required.
Web uses fetch to directly request the business backend. The example only passes the RTC room ID, so no local user or local stream information is sent:
Broadcast digital human only requires logging in to the RTC room and receiving the digital human stream. The client does not need to create a local stream or call startPublishingStream.
Android needs to first obtain the Token from the business backend using OkHttp, then call the ZEGO Express SDK to log in to the room. After successful login, enable custom video rendering, then create a broadcast digital human instance:
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>() {{
//===== Digital human specific ====//
put("set_audio_volume_ducking_mode", "1"); // Audio volume ducking toggle, enabled by default
put("enable_rnd_volume_adaptive", "true"); // Playback volume adaptive toggle, enabled by default
put("sideinfo_callback_version", "3"); // Make SEI and frame correspond one-to-one, for ZEGO Express SDK 3.17
put("sideinfo_bound_to_video_decoder", "true"); // Make SEI and frame correspond one-to-one, for 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) {
// Handle Token retrieval failure
}
});
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>() {{
//===== Digital human specific ====//
put("set_audio_volume_ducking_mode", "1"); // Audio volume ducking toggle, enabled by default
put("enable_rnd_volume_adaptive", "true"); // Playback volume adaptive toggle, enabled by default
put("sideinfo_callback_version", "3"); // Make SEI and frame correspond one-to-one, for ZEGO Express SDK 3.17
put("sideinfo_bound_to_video_decoder", "true"); // Make SEI and frame correspond one-to-one, for 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) {
// Handle Token retrieval failure
}
});
iOS uses NSURLSession to obtain the Token, then calls loginRoom directly. The broadcast scenario does not call startPublishingStream:
Web logs in to the room directly. The broadcast scenario does not create audio streams or publish local streams:
// 1. Obtain Token from business backend
const tokenRes = await fetch(`${BASE_URL}/api/zego-token?userId=${USER_ID}&roomId=${ROOM_ID}`);
const { token } = await tokenRes.json();
// 2. Log in to RTC room
await zg.loginRoom(ROOM_ID, token, { userID: USER_ID, userName: USER_ID });
// 3. Create broadcast digital human instance
const result = await startLiveDigitalHuman(ROOM_ID);
// 4. Extract and save agent_stream_id and agent_instance_id for subsequent play stream, TTS, and exit
agentStreamId = result.agent_stream_id;
agentInstanceId = result.agent_instance_id;
// 1. Obtain Token from business backend
const tokenRes = await fetch(`${BASE_URL}/api/zego-token?userId=${USER_ID}&roomId=${ROOM_ID}`);
const { token } = await tokenRes.json();
// 2. Log in to RTC room
await zg.loginRoom(ROOM_ID, token, { userID: USER_ID, userName: USER_ID });
// 3. Create broadcast digital human instance
const result = await startLiveDigitalHuman(ROOM_ID);
// 4. Extract and save agent_stream_id and agent_instance_id for subsequent play stream, TTS, and exit
agentStreamId = result.agent_stream_id;
agentInstanceId = result.agent_instance_id;
Initializing the Digital Human SDK and Custom Rendering
Android and iOS need to pass the raw video frames and SEI data received by the ZEGO Express SDK to the Digital Human SDK, which then renders the digital human. You must enable custom video rendering before calling startPlayingStream.
Android initializes the Digital Human SDK. The example involves three view member variables that need to be declared and initialized in the layout beforehand:
digitalView: The container for the Digital Human SDK rendering output. The digital human is ultimately drawn on this View. It is passed to the Digital Human SDK via attach during initialization.
loadingView: A loading placeholder view displayed before the digital human's first frame is rendered. It is hidden in the first frame draw callback.
digitalPic: A static cover placeholder image. Similar to loadingView, it is hidden in the first frame draw callback to prevent the screen from staying on a static image.
In openExpressCustomRender, configure RAW_DATA and forward the callback data to the Digital Human SDK. onPlayerSyncRecvSEI is used to receive the SEI (Supplemental Enhancement Information) data parsed by the ZEGO Express SDK and forward it to the Digital Human SDK. The SEI carries additional information such as lip-sync and expressions needed for driving the digital human, which is critical for accurate lip-sync. For details on SEI, see SEI Advanced Features.
The Web client does not integrate the Digital Human SDK and does not require custom rendering configuration. It directly uses the ZEGO Express SDK to play the digital human video stream.
Playing the Digital Human Stream
After creating the instance, the client uses the agent_stream_id returned by the server to play the digital human stream. The timing for playing streams varies slightly across platform quickstarts: Android plays the stream immediately after the create instance API succeeds; iOS and Web match the target stream via room stream update callbacks before playing.
The Android quickstart directly plays the agent_stream_id after the broadcast digital human instance is successfully created, without using onRoomStreamUpdate:
// Set play stream buffer interval range to mitigate stuttering caused by network jitter; min is the initial buffer (ms), max is the maximum buffer (ms)
ZegoExpressEngine.getEngine()
.setPlayStreamBufferIntervalRange(agent_stream_id, 100, 2000);
ZegoExpressEngine.getEngine().startPlayingStream(agent_stream_id);
// Set play stream buffer interval range to mitigate stuttering caused by network jitter; min is the initial buffer (ms), max is the maximum buffer (ms)
ZegoExpressEngine.getEngine()
.setPlayStreamBufferIntervalRange(agent_stream_id, 100, 2000);
ZegoExpressEngine.getEngine().startPlayingStream(agent_stream_id);
Listen for onRoomStreamUpdate and start playing the stream only when the agentStreamId returned by the server appears in the room:
- (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];
}
}
}
}
Listen for roomStreamUpdate and create a remote stream view after matching the agentStreamId returned by the server. The agentStreamId comes from the agent_stream_id returned by the create instance API, which was extracted from the response after logging in to the room:
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) {
// Only play the digital human stream, filter out other streams in the room
if (stream.streamID !== agentStreamId) continue;
const mediaStream = await zg.startPlayingStream(stream.streamID);
remoteView = await zg.createRemoteStreamView(mediaStream);
remoteView?.playAudio();
break;
}
}
},
);
// The digital human video stream is published by the server. Camera status changing to OPEN indicates the video stream is ready, at which point video playback begins
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) {
// Only play the digital human stream, filter out other streams in the room
if (stream.streamID !== agentStreamId) continue;
const mediaStream = await zg.startPlayingStream(stream.streamID);
remoteView = await zg.createRemoteStreamView(mediaStream);
remoteView?.playAudio();
break;
}
}
},
);
// The digital human video stream is published by the server. Camera status changing to OPEN indicates the video stream is ready, at which point video playback begins
zg.on(
"remoteCameraStatusUpdate",
(streamID: string, status: "OPEN" | "MUTE") => {
if (streamID === agentStreamId && status === "OPEN") {
remoteView?.playVideo("remoteStreamView");
}
}
);
CDN Mode Stream Playback
The above describes the RTC mode stream playback method. If using CDN mode, the client does not need to log in to an RTC room or follow the above flow to play an RTC stream. Instead, it can directly use a generic player (such as an HLS/FLV player) to play the cdn_url returned by the business backend to view the digital human broadcast. CDN mode does not require integrating the Digital Human SDK.
Proactively Making the Digital Human Broadcast Text
After the digital human instance is successfully created, the client calls the business backend's POST /api/send-agent-instance-tts, passing in the instance ID and text:
{
"agent_instance_id": "2075416950128779264",
"text": "Hello developer, welcome to ZEGOCLOUD AI Agent."
}
{
"agent_instance_id": "2075416950128779264",
"text": "Hello developer, welcome to ZEGOCLOUD AI Agent."
}
The maximum length of text is 300 characters. You can also pass additional parameters such as add_history, priority, and same_priority_option as needed. For details, see Proactive TTS Invocation.
Instance Lifecycle
A broadcast digital human instance is automatically destroyed after being idle (no broadcast tasks) for more than 900 seconds. The business side can adjust this idle timeout via MaxIdleTime. If you need to keep the instance alive for a long time, periodically invoke proactive TTS or increase MaxIdleTime.
Android uses OkHttp to directly call the TTS API:
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) {
// Handle TTS request failure
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// Handle network error
}
});
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) {
// Handle TTS request failure
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// Handle network error
}
});
The Web quickstart displays a TTS input box in broadcast mode and saves the agent_instance_id returned by the server to the page state.
Leaving the Room and Ending the Broadcast
When exiting, you need to stop the Agent instance, stop playing the stream, log out of the RTC room, and destroy the client SDK. Regardless of whether the stop API call succeeds, RTC resources should be released to avoid lingering rooms and engines.
@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) {
// Even if the request fails, release local RTC and Digital Human SDK resources
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) {
// Even if the request fails, release local RTC and Digital Human SDK resources
ZegoExpressEngine.getEngine().logoutRoom();
digitalMobile.stop();
ZegoExpressEngine.destroyEngine(null);
}
});
}
The broadcast scenario has no local audio stream, so there is no need to destroy audio capture streams.
RTC and CDN Modes
This document uses RTC mode as an example: the client logs in to a room and plays the agent_stream_id. If you need large-scale live streaming, you can use CDN mode:
Mode
Instance Creation Parameters
Client Playback Method
Use Cases
RTC
room_id
ZEGO Express SDK to play RTC stream
Low latency, small-scale interaction
CDN
cdn_url
Use a player to play CDN stream
Large-scale live streaming
Both RTC and CDN modes use agent_instance_id to call the proactive TTS and stop instance APIs. CDN mode does not require the client to log in to an RTC room or integrate the Digital Human SDK. For CDN stream playback, you can use any CDN-compatible playback method without special configuration.
Digital humans support both RTC and CDN stream playback. If you need a large number of users to watch digital human broadcasts, such as in e-commerce live streaming or live classroom scenarios, you can use CDN live streaming mode. In CDN mode, the Web client does not need to integrate the ZEGO Express SDK. Simply use any Web player that supports HLS/FLV (such as hls.js or video.js) to play the cdn_url.
Listening to Callbacks
Please listen to the ZEGO Express SDK's room login, play stream status, and error callbacks. Record the agent_instance_id, agent_stream_id, request_id, and error information in the business backend to help troubleshoot instance creation, stream playback, or TTS failures. The request_id is returned by the ZEGOCLOUD server and is a key identifier for troubleshooting server-side issues. When an exception occurs, be sure to provide this information along with the above details to ZEGOCLOUD Technical Support.
During integration testing, it is strongly recommended to listen for callbacks received by the business backend (events where the Event is Exception). Use Data.Code and Data.Message to quickly identify issues with LLM/TTS parameter configurations. For callback and error code details, see Receiving Callbacks and Exception Event Error Codes.
Note
If the digital human display stays on a static image, please check the following: whether the digital human configuration is valid, whether agent_stream_id is correct, whether custom video rendering was enabled before startPlayingStream, and whether video frames and SEI data are being passed to the Digital Human SDK.
Troubleshooting Checklist
If you encounter issues during integration, check against the following checklist item by item.
Android Troubleshooting Checklist
Symptom
Troubleshooting Direction
Room login failed
Check if the Token is valid, if AppID matches, and if the network can reach ZEGOCLOUD services.
Instance creation failed
Confirm that digital_human_id, config_id (Android uses mobile), and room_id are correct, and that the business backend signature is valid.
No stream / No video
Confirm agent_stream_id matches the server response; check if enableCustomVideoRender was called before startPlayingStream.
Display stays on static image
Check if digitalView/loadingView/digitalPic are correctly attached; confirm video frames and SEI are forwarded to digitalMobileSDK; run on a real device, not an emulator.
Inaccurate lip-sync
Confirm SEI data is forwarded via onPlayerSyncRecvSEI; confirm SEI-related parameters in advanceConfig match the ZEGO Express SDK version.
TTS not broadcasting
Confirm agent_instance_id is correct, text ≤ 300 characters, and the instance has not been destroyed due to 900 seconds of idle time.
iOS Troubleshooting Checklist
Symptom
Troubleshooting Direction
Room login failed
Check if the Token is valid, if AppID matches, and if the network can reach ZEGOCLOUD services.
Instance creation failed
Confirm that digital_human_id, config_id (iOS uses mobile), and room_id are correct, and that the business backend signature is valid.
No stream / No video
Match agentStreamId in onRoomStreamUpdate before playing the stream; confirm enableCustomVideoRender is called before startPlayingStream.
Display stays on static image
Confirm video frames and SEI are forwarded to digitalMobile; Info.plist does not need microphone permission but must have network permission.
TTS not broadcasting
Confirm agent_instance_id is correct, text ≤ 300 characters, and the instance has not been destroyed due to 900 seconds of idle time.
Web Troubleshooting Checklist
Symptom
Troubleshooting Direction
Room login failed
Check if the Token is valid, if AppID matches; for version 3.7.0 and above, server can be an empty string.
Instance creation failed
Confirm that digital_human_id, config_id (Web uses web), and room_id are correct, and that the business backend signature is valid.
No video / No audio
Filter by agentStreamId in roomStreamUpdate before calling startPlayingStream; wait for remoteCameraStatusUpdate to be OPEN before calling playVideo.
Video container not displaying
Confirm the container ID passed to playVideo is remoteStreamView (check spelling).
TTS not broadcasting
Confirm agent_instance_id is correct, text ≤ 300 characters, and the instance has not been destroyed due to 900 seconds of idle time.