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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/assets/agora/SKILL.md +113 -0
  4. package/assets/agora/references/cli/README.md +161 -0
  5. package/assets/agora/references/cli/automation.md +189 -0
  6. package/assets/agora/references/cli/doctor.md +129 -0
  7. package/assets/agora/references/cli/env.md +158 -0
  8. package/assets/agora/references/cli/install-auth.md +152 -0
  9. package/assets/agora/references/cli/projects.md +116 -0
  10. package/assets/agora/references/cli/quickstarts.md +117 -0
  11. package/assets/agora/references/cloud-recording/README.md +86 -0
  12. package/assets/agora/references/conversational-ai/README.md +285 -0
  13. package/assets/agora/references/conversational-ai/agent-client-toolkit-react.md +182 -0
  14. package/assets/agora/references/conversational-ai/agent-samples.md +101 -0
  15. package/assets/agora/references/conversational-ai/agent-toolkit-android.md +209 -0
  16. package/assets/agora/references/conversational-ai/agent-toolkit-ios.md +208 -0
  17. package/assets/agora/references/conversational-ai/agent-toolkit.md +201 -0
  18. package/assets/agora/references/conversational-ai/agent-ui-kit.md +63 -0
  19. package/assets/agora/references/conversational-ai/architecture.md +221 -0
  20. package/assets/agora/references/conversational-ai/auth-flow.md +154 -0
  21. package/assets/agora/references/conversational-ai/conversational-ai-studio.md +173 -0
  22. package/assets/agora/references/conversational-ai/go-sdk.md +184 -0
  23. package/assets/agora/references/conversational-ai/integration-from-quickstart.md +203 -0
  24. package/assets/agora/references/conversational-ai/python-sdk.md +122 -0
  25. package/assets/agora/references/conversational-ai/quickstarts.md +710 -0
  26. package/assets/agora/references/conversational-ai/server-custom-llm.md +45 -0
  27. package/assets/agora/references/conversational-ai/server-mcp.md +40 -0
  28. package/assets/agora/references/conversational-ai/server-sdk-rename.md +78 -0
  29. package/assets/agora/references/conversational-ai/server-sdks.md +128 -0
  30. package/assets/agora/references/doc-fetching.md +67 -0
  31. package/assets/agora/references/integration-patterns.md +201 -0
  32. package/assets/agora/references/mcp-tools.md +49 -0
  33. package/assets/agora/references/rtc/README.md +104 -0
  34. package/assets/agora/references/rtc/android.md +344 -0
  35. package/assets/agora/references/rtc/cross-platform-coordination.md +61 -0
  36. package/assets/agora/references/rtc/flutter.md +282 -0
  37. package/assets/agora/references/rtc/ios.md +306 -0
  38. package/assets/agora/references/rtc/nextjs.md +87 -0
  39. package/assets/agora/references/rtc/react-native.md +266 -0
  40. package/assets/agora/references/rtc/react.md +186 -0
  41. package/assets/agora/references/rtc/web.md +506 -0
  42. package/assets/agora/references/rtm/README.md +80 -0
  43. package/assets/agora/references/rtm/android.md +277 -0
  44. package/assets/agora/references/rtm/ios.md +231 -0
  45. package/assets/agora/references/rtm/web.md +348 -0
  46. package/assets/agora/references/server/README.md +22 -0
  47. package/assets/agora/references/server/tokens.md +74 -0
  48. package/assets/agora/references/server-gateway/README.md +80 -0
  49. package/assets/agora/references/server-gateway/linux-cpp.md +251 -0
  50. package/assets/agora/references/testing-guidance/SKILL.md +65 -0
  51. package/assets/agora/references/testing-guidance/completeness-gate.md +28 -0
  52. package/assets/agora/references/testing-guidance/convoai-rest.md +83 -0
  53. package/assets/agora/references/testing-guidance/mobile-rtm-and-renewal.md +109 -0
  54. package/assets/agora/references/testing-guidance/rtc-android.md +70 -0
  55. package/assets/agora/references/testing-guidance/rtc-ios.md +73 -0
  56. package/assets/agora/references/testing-guidance/rtc-react.md +51 -0
  57. package/assets/agora/references/testing-guidance/rtc-web.md +94 -0
  58. package/cordis.patch.yml +5 -0
  59. package/index.js +53 -0
  60. 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).