dsh-agora 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/assets/agora/SKILL.md +113 -0
- package/assets/agora/references/cli/README.md +161 -0
- package/assets/agora/references/cli/automation.md +189 -0
- package/assets/agora/references/cli/doctor.md +129 -0
- package/assets/agora/references/cli/env.md +158 -0
- package/assets/agora/references/cli/install-auth.md +152 -0
- package/assets/agora/references/cli/projects.md +116 -0
- package/assets/agora/references/cli/quickstarts.md +117 -0
- package/assets/agora/references/cloud-recording/README.md +86 -0
- package/assets/agora/references/conversational-ai/README.md +285 -0
- package/assets/agora/references/conversational-ai/agent-client-toolkit-react.md +182 -0
- package/assets/agora/references/conversational-ai/agent-samples.md +101 -0
- package/assets/agora/references/conversational-ai/agent-toolkit-android.md +209 -0
- package/assets/agora/references/conversational-ai/agent-toolkit-ios.md +208 -0
- package/assets/agora/references/conversational-ai/agent-toolkit.md +201 -0
- package/assets/agora/references/conversational-ai/agent-ui-kit.md +63 -0
- package/assets/agora/references/conversational-ai/architecture.md +221 -0
- package/assets/agora/references/conversational-ai/auth-flow.md +154 -0
- package/assets/agora/references/conversational-ai/conversational-ai-studio.md +173 -0
- package/assets/agora/references/conversational-ai/go-sdk.md +184 -0
- package/assets/agora/references/conversational-ai/integration-from-quickstart.md +203 -0
- package/assets/agora/references/conversational-ai/python-sdk.md +122 -0
- package/assets/agora/references/conversational-ai/quickstarts.md +710 -0
- package/assets/agora/references/conversational-ai/server-custom-llm.md +45 -0
- package/assets/agora/references/conversational-ai/server-mcp.md +40 -0
- package/assets/agora/references/conversational-ai/server-sdk-rename.md +78 -0
- package/assets/agora/references/conversational-ai/server-sdks.md +128 -0
- package/assets/agora/references/doc-fetching.md +67 -0
- package/assets/agora/references/integration-patterns.md +201 -0
- package/assets/agora/references/mcp-tools.md +49 -0
- package/assets/agora/references/rtc/README.md +104 -0
- package/assets/agora/references/rtc/android.md +344 -0
- package/assets/agora/references/rtc/cross-platform-coordination.md +61 -0
- package/assets/agora/references/rtc/flutter.md +282 -0
- package/assets/agora/references/rtc/ios.md +306 -0
- package/assets/agora/references/rtc/nextjs.md +87 -0
- package/assets/agora/references/rtc/react-native.md +266 -0
- package/assets/agora/references/rtc/react.md +186 -0
- package/assets/agora/references/rtc/web.md +506 -0
- package/assets/agora/references/rtm/README.md +80 -0
- package/assets/agora/references/rtm/android.md +277 -0
- package/assets/agora/references/rtm/ios.md +231 -0
- package/assets/agora/references/rtm/web.md +348 -0
- package/assets/agora/references/server/README.md +22 -0
- package/assets/agora/references/server/tokens.md +74 -0
- package/assets/agora/references/server-gateway/README.md +80 -0
- package/assets/agora/references/server-gateway/linux-cpp.md +251 -0
- package/assets/agora/references/testing-guidance/SKILL.md +65 -0
- package/assets/agora/references/testing-guidance/completeness-gate.md +28 -0
- package/assets/agora/references/testing-guidance/convoai-rest.md +83 -0
- package/assets/agora/references/testing-guidance/mobile-rtm-and-renewal.md +109 -0
- package/assets/agora/references/testing-guidance/rtc-android.md +70 -0
- package/assets/agora/references/testing-guidance/rtc-ios.md +73 -0
- package/assets/agora/references/testing-guidance/rtc-react.md +51 -0
- package/assets/agora/references/testing-guidance/rtc-web.md +94 -0
- package/cordis.patch.yml +5 -0
- package/index.js +53 -0
- package/package.json +49 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# Agora RTC Web SDK (agora-rtc-sdk-ng)
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Installation](#installation)
|
|
6
|
+
- [Client Creation](#client-creation)
|
|
7
|
+
- [Joining a Channel](#joining-a-channel)
|
|
8
|
+
- [Creating Tracks](#creating-tracks)
|
|
9
|
+
- [Publishing and Subscribing](#publishing-and-subscribing)
|
|
10
|
+
- [Event Handling](#event-handling)
|
|
11
|
+
- [Leaving and Cleanup](#leaving-and-cleanup)
|
|
12
|
+
- [Token Renewal](#token-renewal)
|
|
13
|
+
- [Volume Monitoring](#volume-monitoring)
|
|
14
|
+
- [Network Quality](#network-quality)
|
|
15
|
+
- [Device Enumeration](#device-enumeration)
|
|
16
|
+
- [Complete Example: Video Call](#complete-example-video-call)
|
|
17
|
+
- [Multi-Channel Pattern](#multi-channel-pattern)
|
|
18
|
+
- [Advanced: Dynamic Subscription Management](#advanced-dynamic-subscription-management)
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install agora-rtc-sdk-ng
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Package: `agora-rtc-sdk-ng` (v4.x). Do NOT use deprecated `agora-rtc-sdk` (v3.x).
|
|
27
|
+
|
|
28
|
+
## Client Creation
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import AgoraRTC, { IAgoraRTCClient } from "agora-rtc-sdk-ng"
|
|
32
|
+
|
|
33
|
+
// Communication mode (all peers equal)
|
|
34
|
+
const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" })
|
|
35
|
+
|
|
36
|
+
// Live broadcasting mode (host/audience roles)
|
|
37
|
+
const client = AgoraRTC.createClient({ mode: "live", codec: "vp8" })
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Codec options: `"vp8"`, `"vp9"`, `"h264"`, `"h265"`, `"av1"`. Use `"vp8"` for broadest browser support. Safari 12.1 and earlier does not support VP8. `"av1"` is available in newer SDK versions for modern browsers.
|
|
41
|
+
|
|
42
|
+
## Joining a Channel
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// Join with auto-assigned UID
|
|
46
|
+
const uid = await client.join(APP_ID, "channel-name", TOKEN, null)
|
|
47
|
+
|
|
48
|
+
// Join with specific UID
|
|
49
|
+
const uid = await client.join(APP_ID, "channel-name", TOKEN, 12345)
|
|
50
|
+
|
|
51
|
+
// For live broadcasting, set role before joining
|
|
52
|
+
await client.setClientRole("host") // Can publish + subscribe
|
|
53
|
+
await client.setClientRole("audience") // Can only subscribe
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `TOKEN`: Pass `null` for testing without tokens enabled. In production, always use a server-generated token.
|
|
57
|
+
- `uid`: Pass `null` for auto-assignment, or a specific number. String UIDs also supported.
|
|
58
|
+
|
|
59
|
+
### UID Constraints (Easy to Miss)
|
|
60
|
+
|
|
61
|
+
- Numeric UID: `0` to `2^32 - 1` (32-bit unsigned integer).
|
|
62
|
+
- String UID: ASCII only, maximum `255` characters.
|
|
63
|
+
- Keep UID type consistent per channel: all users should use either numeric or string UIDs, not a mix.
|
|
64
|
+
|
|
65
|
+
## Creating Tracks
|
|
66
|
+
|
|
67
|
+
### Microphone Audio
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const audioTrack = await AgoraRTC.createMicrophoneAudioTrack({
|
|
71
|
+
encoderConfig: "high_quality_stereo",
|
|
72
|
+
AEC: true, // Acoustic Echo Cancellation
|
|
73
|
+
ANS: true, // Automatic Noise Suppression
|
|
74
|
+
AGC: true, // Automatic Gain Control
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Camera Video
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const videoTrack = await AgoraRTC.createCameraVideoTrack({
|
|
82
|
+
encoderConfig: "720p_2",
|
|
83
|
+
cameraId: deviceId, // optional specific camera
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Both Simultaneously
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const [audioTrack, videoTrack] = await AgoraRTC.createMicrophoneAndCameraTracks(
|
|
91
|
+
{ encoderConfig: "high_quality_stereo" },
|
|
92
|
+
{ encoderConfig: "720p_2" }
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Custom Encoder Config
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const videoTrack = await AgoraRTC.createCameraVideoTrack({
|
|
100
|
+
encoderConfig: {
|
|
101
|
+
width: 640,
|
|
102
|
+
height: 360,
|
|
103
|
+
frameRate: 24,
|
|
104
|
+
bitrateMin: 400,
|
|
105
|
+
bitrateMax: 1000,
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Publishing and Subscribing
|
|
111
|
+
|
|
112
|
+
### Publish Local Tracks
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// Publish after joining
|
|
116
|
+
await client.publish([audioTrack, videoTrack])
|
|
117
|
+
|
|
118
|
+
// Play local video in DOM element
|
|
119
|
+
videoTrack.play("local-player") // element ID or HTMLElement
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Subscribe to Remote Users
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
client.on("user-published", async (user, mediaType) => {
|
|
126
|
+
await client.subscribe(user, mediaType)
|
|
127
|
+
|
|
128
|
+
if (mediaType === "video") {
|
|
129
|
+
user.videoTrack.play(`player-${user.uid}`)
|
|
130
|
+
}
|
|
131
|
+
if (mediaType === "audio") {
|
|
132
|
+
user.audioTrack.play() // no DOM element needed for audio
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Selective Subscription
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// Don't auto-subscribe to everyone; filter by UID
|
|
141
|
+
client.on("user-published", async (user, mediaType) => {
|
|
142
|
+
if (shouldSubscribe(user.uid)) {
|
|
143
|
+
await client.subscribe(user, mediaType)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Event Handling
|
|
149
|
+
|
|
150
|
+
Register ALL event handlers BEFORE calling `client.join()`.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// Remote user joins channel
|
|
154
|
+
client.on("user-joined", (user) => {
|
|
155
|
+
console.log("User joined:", user.uid)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Remote user publishes track (fires separately for audio and video)
|
|
159
|
+
client.on("user-published", async (user, mediaType) => {
|
|
160
|
+
await client.subscribe(user, mediaType)
|
|
161
|
+
if (mediaType === "video") user.videoTrack.play(`player-${user.uid}`)
|
|
162
|
+
if (mediaType === "audio") user.audioTrack.play()
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Remote user unpublishes track
|
|
166
|
+
client.on("user-unpublished", (user, mediaType) => {
|
|
167
|
+
if (mediaType === "video") {
|
|
168
|
+
document.getElementById(`player-${user.uid}`)?.remove()
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// Remote user leaves
|
|
173
|
+
client.on("user-left", (user) => {
|
|
174
|
+
document.getElementById(`player-${user.uid}`)?.remove()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Connection state changes
|
|
178
|
+
client.on("connection-state-change", (curState, prevState, reason) => {
|
|
179
|
+
console.log(`${prevState} -> ${curState}, reason: ${reason}`)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Network quality (fires every 2 seconds after joining)
|
|
183
|
+
client.on("network-quality", (stats) => {
|
|
184
|
+
// uplinkNetworkQuality/downlinkNetworkQuality: 0-6 (0=unknown, 1=excellent, 5=very bad, 6=disconnected)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
// Stream messages from data channel (used by Conversational AI)
|
|
188
|
+
client.on("stream-message", (uid, stream) => {
|
|
189
|
+
const text = new TextDecoder().decode(stream)
|
|
190
|
+
console.log("Stream message from", uid, text)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Token expiry warning
|
|
194
|
+
client.on("token-privilege-will-expire", async () => {
|
|
195
|
+
const newToken = await fetchTokenFromServer()
|
|
196
|
+
await client.renewToken(newToken)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// SDK exceptions
|
|
200
|
+
client.on("exception", (event) => {
|
|
201
|
+
console.warn("SDK exception:", event)
|
|
202
|
+
})
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Leaving and Cleanup
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
async function leave() {
|
|
209
|
+
// 1. Stop and close local tracks
|
|
210
|
+
if (audioTrack) {
|
|
211
|
+
audioTrack.stop()
|
|
212
|
+
audioTrack.close()
|
|
213
|
+
}
|
|
214
|
+
if (videoTrack) {
|
|
215
|
+
videoTrack.stop()
|
|
216
|
+
videoTrack.close()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 2. Leave channel
|
|
220
|
+
await client.leave()
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Always `stop()` then `close()`. Failure to close tracks leaves microphone/camera locked.
|
|
225
|
+
|
|
226
|
+
## Token Renewal
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
client.on("token-privilege-will-expire", async () => {
|
|
230
|
+
// Fetch new token from your server
|
|
231
|
+
const response = await fetch(`/api/token?channel=${channel}&uid=${uid}`)
|
|
232
|
+
const { token } = await response.json()
|
|
233
|
+
await client.renewToken(token)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// Also handle token-privilege-did-expire as fallback
|
|
237
|
+
client.on("token-privilege-did-expire", async () => {
|
|
238
|
+
// Token already expired — re-join with new token
|
|
239
|
+
const { token } = await fetch(`/api/token?channel=${channel}&uid=${uid}`).then(r => r.json())
|
|
240
|
+
await client.leave()
|
|
241
|
+
await client.join(APP_ID, channel, token, uid)
|
|
242
|
+
})
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Volume Monitoring
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// Poll volume levels at interval
|
|
249
|
+
setInterval(() => {
|
|
250
|
+
// Local
|
|
251
|
+
if (audioTrack) {
|
|
252
|
+
const level = audioTrack.getVolumeLevel() // 0.0 - 1.0
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Remote
|
|
256
|
+
client.remoteUsers.forEach(user => {
|
|
257
|
+
if (user.audioTrack) {
|
|
258
|
+
const level = user.audioTrack.getVolumeLevel()
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
}, 200)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Network Quality
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
client.on("network-quality", (stats) => {
|
|
268
|
+
// stats.uplinkNetworkQuality: 0-6
|
|
269
|
+
// stats.downlinkNetworkQuality: 0-6
|
|
270
|
+
// 0=unknown, 1=excellent, 2=good, 3=poor, 4=bad, 5=very bad, 6=disconnected
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Get detailed stats
|
|
274
|
+
const localStats = client.getLocalAudioStats()
|
|
275
|
+
const remoteStats = client.getRemoteAudioStats()
|
|
276
|
+
const localVideoStats = client.getLocalVideoStats()
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Device Enumeration
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const cameras = await AgoraRTC.getCameras()
|
|
283
|
+
const microphones = await AgoraRTC.getMicrophones()
|
|
284
|
+
const speakers = await AgoraRTC.getPlaybackDevices()
|
|
285
|
+
|
|
286
|
+
// Switch device on existing track
|
|
287
|
+
await videoTrack.setDevice(newCameraId)
|
|
288
|
+
await audioTrack.setDevice(newMicrophoneId)
|
|
289
|
+
|
|
290
|
+
// Listen for device changes
|
|
291
|
+
AgoraRTC.onMicrophoneChanged = (info) => {
|
|
292
|
+
console.log("Mic changed:", info.state, info.device)
|
|
293
|
+
}
|
|
294
|
+
AgoraRTC.onCameraChanged = (info) => {
|
|
295
|
+
console.log("Camera changed:", info.state, info.device)
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Complete Example: Video Call
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import AgoraRTC from "agora-rtc-sdk-ng"
|
|
303
|
+
|
|
304
|
+
const APP_ID = "your-app-id"
|
|
305
|
+
let client: IAgoraRTCClient
|
|
306
|
+
let localAudioTrack: IMicrophoneAudioTrack
|
|
307
|
+
let localVideoTrack: ICameraVideoTrack
|
|
308
|
+
|
|
309
|
+
async function join(channel: string, token: string | null) {
|
|
310
|
+
client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" })
|
|
311
|
+
|
|
312
|
+
// Register events BEFORE joining
|
|
313
|
+
client.on("user-published", async (user, mediaType) => {
|
|
314
|
+
await client.subscribe(user, mediaType)
|
|
315
|
+
if (mediaType === "video") {
|
|
316
|
+
const container = document.createElement("div")
|
|
317
|
+
container.id = `player-${user.uid}`
|
|
318
|
+
container.style.width = "640px"
|
|
319
|
+
container.style.height = "480px"
|
|
320
|
+
document.getElementById("remote-players")!.appendChild(container)
|
|
321
|
+
user.videoTrack!.play(container)
|
|
322
|
+
}
|
|
323
|
+
if (mediaType === "audio") {
|
|
324
|
+
user.audioTrack!.play()
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
client.on("user-unpublished", (user, mediaType) => {
|
|
329
|
+
if (mediaType === "video") {
|
|
330
|
+
document.getElementById(`player-${user.uid}`)?.remove()
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
client.on("user-left", (user) => {
|
|
335
|
+
document.getElementById(`player-${user.uid}`)?.remove()
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
// Join
|
|
339
|
+
const uid = await client.join(APP_ID, channel, token, null)
|
|
340
|
+
|
|
341
|
+
// Create and publish tracks
|
|
342
|
+
;[localAudioTrack, localVideoTrack] = await AgoraRTC.createMicrophoneAndCameraTracks()
|
|
343
|
+
localVideoTrack.play("local-player")
|
|
344
|
+
await client.publish([localAudioTrack, localVideoTrack])
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function leave() {
|
|
348
|
+
localAudioTrack?.stop()
|
|
349
|
+
localAudioTrack?.close()
|
|
350
|
+
localVideoTrack?.stop()
|
|
351
|
+
localVideoTrack?.close()
|
|
352
|
+
await client?.leave()
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Multi-Channel Pattern
|
|
357
|
+
|
|
358
|
+
For large-scale viewing (64+ users), use multiple client instances joining different channels:
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
const clients: IAgoraRTCClient[] = []
|
|
362
|
+
const NUM_CHANNELS = 4
|
|
363
|
+
|
|
364
|
+
for (let i = 0; i < NUM_CHANNELS; i++) {
|
|
365
|
+
const client = AgoraRTC.createClient({ mode: "live", codec: "vp9" })
|
|
366
|
+
clients.push(client)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Join each client to a different channel
|
|
370
|
+
for (let i = 0; i < NUM_CHANNELS; i++) {
|
|
371
|
+
await clients[i].setClientRole("audience")
|
|
372
|
+
await clients[i].join(APP_ID, `${baseChannel}${i}`, null, null)
|
|
373
|
+
|
|
374
|
+
clients[i].on("user-published", async (user, mediaType) => {
|
|
375
|
+
// Manage subscriptions across channels
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Key: Single channel supports up to 17 video publishers (recommended) or 128 hosts. Multi-channel multiplies this.
|
|
381
|
+
|
|
382
|
+
## Advanced: Dynamic Subscription Management
|
|
383
|
+
|
|
384
|
+
For apps with many publishers, don't subscribe to all immediately. Use a priority queue:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
const videoPublishers = new Map() // uid -> client
|
|
388
|
+
const videoSubscriptions = new Map() // uid -> subscription info
|
|
389
|
+
|
|
390
|
+
client.on("user-published", (user, mediaType) => {
|
|
391
|
+
if (mediaType === "video") {
|
|
392
|
+
// Don't subscribe yet — add to available publishers
|
|
393
|
+
videoPublishers.set(user.uid, client)
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// Periodically manage subscriptions based on capacity
|
|
398
|
+
function manageSubscriptions(maxSubs: number) {
|
|
399
|
+
const prioritized = [...videoPublishers.keys()].slice(0, maxSubs)
|
|
400
|
+
|
|
401
|
+
// Subscribe to top-priority users not yet subscribed
|
|
402
|
+
for (const uid of prioritized) {
|
|
403
|
+
if (!videoSubscriptions.has(uid)) {
|
|
404
|
+
const user = client.remoteUsers.find(u => u.uid === uid)
|
|
405
|
+
if (user) {
|
|
406
|
+
client.subscribe(user, "video").then(() => {
|
|
407
|
+
user.videoTrack?.play(`player-${uid}`)
|
|
408
|
+
videoSubscriptions.set(uid, { startTime: Date.now() })
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Unsubscribe from users no longer in priority list
|
|
415
|
+
for (const uid of videoSubscriptions.keys()) {
|
|
416
|
+
if (!prioritized.includes(uid)) {
|
|
417
|
+
const user = client.remoteUsers.find(u => u.uid === uid)
|
|
418
|
+
if (user) client.unsubscribe(user, "video")
|
|
419
|
+
videoSubscriptions.delete(uid)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Track Muting
|
|
426
|
+
|
|
427
|
+
Use `setEnabled()` to mute/unmute local tracks. This stops the media capture (camera light turns off when video is disabled):
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
// Mute/unmute audio
|
|
431
|
+
await localAudioTrack.setEnabled(false) // mute (stops mic capture)
|
|
432
|
+
await localAudioTrack.setEnabled(true) // unmute
|
|
433
|
+
|
|
434
|
+
// Mute/unmute video
|
|
435
|
+
await localVideoTrack.setEnabled(false) // camera off
|
|
436
|
+
await localVideoTrack.setEnabled(true) // camera on
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
`setEnabled(false)` differs from `setMuted(true)`: `setEnabled` stops the device, while `setMuted` sends silence/black frames but keeps the device active.
|
|
440
|
+
|
|
441
|
+
## Screen Sharing: Dual-Client Pattern
|
|
442
|
+
|
|
443
|
+
Screen sharing requires a **separate client instance** to avoid replacing the camera track. The screen share client joins the same channel with a different UID.
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
// 1. Create a second client for screen sharing
|
|
447
|
+
const screenClient = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" })
|
|
448
|
+
|
|
449
|
+
// 2. Derive a screen-share UID (convention: camera UID + 100000)
|
|
450
|
+
const screenUid = cameraUid + 100000
|
|
451
|
+
|
|
452
|
+
// 3. Create screen track — returns single track or [video, audio] tuple
|
|
453
|
+
const screenTrackOrTracks = await AgoraRTC.createScreenVideoTrack({
|
|
454
|
+
encoderConfig: { width: 1920, height: 1080, frameRate: 15 },
|
|
455
|
+
optimizationMode: "detail", // "detail" for text/slides, "motion" for video
|
|
456
|
+
}, "auto") // "auto" = include system audio if available
|
|
457
|
+
|
|
458
|
+
// Handle the return type (single track or tuple)
|
|
459
|
+
const screenVideoTrack = Array.isArray(screenTrackOrTracks)
|
|
460
|
+
? screenTrackOrTracks[0]
|
|
461
|
+
: screenTrackOrTracks
|
|
462
|
+
const screenAudioTrack = Array.isArray(screenTrackOrTracks)
|
|
463
|
+
? screenTrackOrTracks[1]
|
|
464
|
+
: null
|
|
465
|
+
|
|
466
|
+
// 4. Join the same channel with a different UID and token
|
|
467
|
+
await screenClient.join(APP_ID, channelName, screenToken, screenUid)
|
|
468
|
+
|
|
469
|
+
// 5. Publish screen track(s)
|
|
470
|
+
const tracksToPublish = screenAudioTrack
|
|
471
|
+
? [screenVideoTrack, screenAudioTrack]
|
|
472
|
+
: [screenVideoTrack]
|
|
473
|
+
await screenClient.publish(tracksToPublish)
|
|
474
|
+
|
|
475
|
+
// 6. CRITICAL: Listen for "track-ended" — fires when user clicks browser's "Stop sharing"
|
|
476
|
+
screenVideoTrack.on("track-ended", async () => {
|
|
477
|
+
// Clean up screen share
|
|
478
|
+
for (const track of tracksToPublish) {
|
|
479
|
+
track.stop()
|
|
480
|
+
track.close()
|
|
481
|
+
}
|
|
482
|
+
await screenClient.leave()
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// 7. Cleanup function
|
|
486
|
+
async function stopScreenShare() {
|
|
487
|
+
for (const track of tracksToPublish) {
|
|
488
|
+
track.stop()
|
|
489
|
+
track.close()
|
|
490
|
+
}
|
|
491
|
+
await screenClient.leave()
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Tips:**
|
|
496
|
+
|
|
497
|
+
- Use RTM to notify other participants of screen share start/stop (include the screen UID so viewers can identify it)
|
|
498
|
+
- Remote participants see the screen share as a new user joining — use the UID convention to distinguish camera from screen share
|
|
499
|
+
- Generate a separate token for the screen share UID
|
|
500
|
+
|
|
501
|
+
## Official Documentation
|
|
502
|
+
|
|
503
|
+
For APIs or features not covered above:
|
|
504
|
+
|
|
505
|
+
- API Reference: <https://api-ref.agora.io/en/video-sdk/web/4.x/index.html>
|
|
506
|
+
- Guides: <https://docs.agora.io/en/video-calling/overview/product-overview>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Agora Signaling SDK (RTM)
|
|
2
|
+
|
|
3
|
+
The Agora Signaling SDK (internally called RTM) provides signaling, text messaging, presence, and metadata — used alongside or independently from RTC. RTC and the Signaling SDK are **independent systems**: their channels are separate namespaces.
|
|
4
|
+
|
|
5
|
+
## When to Use RTM
|
|
6
|
+
|
|
7
|
+
- Text chat during video calls
|
|
8
|
+
- Signaling (call invitations, control messages, hang-up)
|
|
9
|
+
- User presence and status tracking
|
|
10
|
+
- Custom data exchange (VAD signals, resolution requests, state sync)
|
|
11
|
+
- Receiving transcripts from Conversational AI agents
|
|
12
|
+
|
|
13
|
+
## Channel Types
|
|
14
|
+
|
|
15
|
+
RTM has two channel types with different semantics:
|
|
16
|
+
|
|
17
|
+
| | Message Channel | Stream Channel |
|
|
18
|
+
| ------------------- | ---------------------------------------- | ------------------------------------------- |
|
|
19
|
+
| **Model** | Pub/sub | Join + topic subscribe |
|
|
20
|
+
| **Join required** | No — subscribe to publish/receive | Yes — must join before publishing |
|
|
21
|
+
| **Topics** | No | Yes — messages published per topic |
|
|
22
|
+
| **Use for** | Signaling, chat, ConvoAI transcripts | High-frequency data, custom media streams |
|
|
23
|
+
| **Presence events** | Via `presence.getOnlineUsers()` or event | Built-in via channel join/leave events |
|
|
24
|
+
|
|
25
|
+
**Default choice**: Use message channels for most use cases. Use stream channels only if you need topic-based filtering or high-frequency updates (e.g., cursor positions, sensor data).
|
|
26
|
+
|
|
27
|
+
## Key Concepts
|
|
28
|
+
|
|
29
|
+
- **Presence**: Track online users and their metadata per channel. Subscribe to `presence` events to detect joins, leaves, and state changes in real time.
|
|
30
|
+
- **Storage**: Channel and user metadata — key-value store with versioning and compare-and-set (CAS) for conflict resolution.
|
|
31
|
+
- **Lock**: Distributed locking for coordinating shared resources across users.
|
|
32
|
+
- **RTM UIDs are strings** — not numeric like RTC. When using RTC and RTM together, `String(rtcUid)` is the simplest default mapping, but it is a convention, not a protocol requirement.
|
|
33
|
+
|
|
34
|
+
## Gotchas & Critical Rules
|
|
35
|
+
|
|
36
|
+
- **UID type mismatch causes silent failures** — RTC UIDs are numbers; RTM UIDs are strings. If your app maps the same person across both systems, convert explicitly and consistently. `String(rtcUid)` is the default convention. Type mismatches don't throw errors — they silently break user lookups across both systems.
|
|
37
|
+
- **RTM token subject and RTM login userId must match** — if your server minted an RTM token for `String(rtcUid)` (or any other identity), the RTM client must log in with that exact same string. Logging in RTM with a different random identity can look like a generic startup failure instead of a clear auth error.
|
|
38
|
+
- **Namespace isolation** — RTC channels and RTM channels are completely separate. Joining RTC channel `"meeting-1"` does NOT auto-subscribe you to RTM channel `"meeting-1"`. Subscribe both explicitly.
|
|
39
|
+
- **Login before all operations** — `rtmClient.login()` must complete (connection reaches `CONNECTED`) before any subscribe, publish, or presence call. Operations called while still connecting are not guaranteed to succeed.
|
|
40
|
+
- **Subscribe before presence** — Presence events (joins/leaves) require an active channel subscription. Publishing to a channel without subscribing means you won't receive presence notifications or responses.
|
|
41
|
+
- **RTM v2 API is a full rewrite** — Do NOT apply v1 patterns (`AgoraRTM.createInstance()`, `.createChannel()`) to v2. The APIs are incompatible. The Web reference (`web.md`) covers v2 only.
|
|
42
|
+
- **ConvoAI transcript delivery requires two flags** — For AI agent transcripts to arrive via RTM, the ConvoAI `/join` payload must include both `advanced_features.enable_rtm: true` AND `parameters.data_channel: "rtm"`. One flag alone is not sufficient.
|
|
43
|
+
- **Recent enablement can lag behind runtime availability** — if RTM or a related capability was just enabled at the control-plane layer, allow bounded wait/retry for up to about 5 minutes before concluding the runtime path is still broken.
|
|
44
|
+
|
|
45
|
+
## RTC + RTM Coordination Pattern
|
|
46
|
+
|
|
47
|
+
When pairing RTC and RTM in the same app, the default recommendation is:
|
|
48
|
+
|
|
49
|
+
1. Join RTC channel with numeric UID (or `0` for auto-assignment)
|
|
50
|
+
2. After RTC join resolves, log in to RTM with `String(rtcUid)`
|
|
51
|
+
3. Subscribe to the RTM message channel
|
|
52
|
+
4. Use RTC for media (audio/video tracks), RTM for all signaling and metadata
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
// RTC join resolves with the assigned numeric UID
|
|
56
|
+
const rtcUid = await rtcClient.join(appId, channelName, rtcToken, null);
|
|
57
|
+
|
|
58
|
+
// uid is set in the RTM constructor; login only needs the token
|
|
59
|
+
// new AgoraRTM.RTM(appId, String(rtcUid)) was already called at setup
|
|
60
|
+
await rtmClient.login({ token: rtmToken }); // RTM token from your server
|
|
61
|
+
await rtmClient.subscribe(channelName);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
RTM channel name does not need to match the RTC channel name, but using the same name is the conventional approach.
|
|
65
|
+
|
|
66
|
+
If your app already has a stable user identity independent of the RTC UID, you can log in to RTM earlier and keep a separate RTC↔RTM mapping. That pattern is useful when you want signaling or presence before media starts, but it adds coordination complexity.
|
|
67
|
+
|
|
68
|
+
## Platform Scope
|
|
69
|
+
|
|
70
|
+
RTM is a **client-side SDK**. It runs in browsers, iOS apps, and Android apps. There is no server-side RTM SDK and no Electron/Windows/desktop variant. If a user needs server-to-channel messaging from a backend, they should use the ConvoAI REST API (`/speak`) or build their own signaling layer.
|
|
71
|
+
|
|
72
|
+
All three platform files below document **RTM v2**. Do not apply v1 patterns (`AgoraRTM.createInstance()`, `.createChannel()`) to any of them — the APIs are incompatible.
|
|
73
|
+
|
|
74
|
+
## Platform Reference Files
|
|
75
|
+
|
|
76
|
+
- **[web.md](web.md)** — `agora-rtm` v2 (JS/TS): client, messaging, presence, stream channels
|
|
77
|
+
- **[ios.md](ios.md)** — `AgoraRtmClientKit` (Swift): init, login, subscribe, publish, delegate
|
|
78
|
+
- **[android.md](android.md)** — `RtmClient` (Kotlin): init, login, subscribe, publish, event listener
|
|
79
|
+
|
|
80
|
+
For test setup and mocking patterns, see [references/testing-guidance/SKILL.md](../testing-guidance/SKILL.md).
|