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,104 @@
|
|
|
1
|
+
# Agora RTC (Video/Voice SDK)
|
|
2
|
+
|
|
3
|
+
Real-time audio and video communication. Users join channels, publish local tracks, and subscribe to remote tracks.
|
|
4
|
+
|
|
5
|
+
## Critical Rules
|
|
6
|
+
|
|
7
|
+
1. **Register event handlers BEFORE joining** the channel, or you will miss events for users already present.
|
|
8
|
+
2. **`user-published` fires separately** for audio and video. A user publishing both triggers two events — handle each.
|
|
9
|
+
3. **Track cleanup**: Always `stop()` then `close()` local tracks before setting to null. Failure to clean up causes memory leaks and device locks. (React SDK hooks handle this automatically — see `react.md`.)
|
|
10
|
+
4. **HTTPS required** for Web SDK (except `localhost`).
|
|
11
|
+
5. **Token management is mandatory in production**. Handle `token-privilege-will-expire` (Web) / `onTokenPrivilegeWillExpire` (native) to renew tokens. UID in token must match UID used to join.
|
|
12
|
+
6. **Stream bombing prevention**: In production, generate tokens with subscriber role (`kRoleSubscriber` / `RtcRole.SUBSCRIBER`) for audience-only users to prevent unauthorized publishing.
|
|
13
|
+
7. **Audio autoplay (non-standard flows only)**: Browser autoplay policy is not an issue in typical RTC flows because the user has already clicked to join (satisfying the gesture requirement). However, if you auto-join on page load, trigger audio from a non-user event, or run in a headless/test environment, `audioTrack.play()` may be silently blocked. Wrap the join + publish sequence in a user gesture handler in these cases.
|
|
14
|
+
|
|
15
|
+
## Channel Profiles
|
|
16
|
+
|
|
17
|
+
- `rtc` (communication): All peers are equal. Best for video calls, conferencing.
|
|
18
|
+
- `live` (live streaming): Host/audience roles. Higher bitrate, lower latency for hosts.
|
|
19
|
+
|
|
20
|
+
## Video Encoder Profiles (Web)
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
"120p_1" → 160×120, 15fps, 65kbps
|
|
24
|
+
"180p_1" → 320×180, 15fps, 140kbps
|
|
25
|
+
"360p_1" → 640×360, 15fps, 400kbps
|
|
26
|
+
"480p_1" → 640×480, 15fps, 500kbps
|
|
27
|
+
"720p_1" → 1280×720, 15fps, 1130kbps
|
|
28
|
+
"720p_2" → 1280×720, 30fps, 2000kbps
|
|
29
|
+
"1080p_1" → 1920×1080, 15fps, 2080kbps
|
|
30
|
+
"1080p_2" → 1920×1080, 30fps, 3000kbps
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or use custom config: `{ width: 640, height: 360, frameRate: 24, bitrateMin: 400, bitrateMax: 1000 }`
|
|
34
|
+
|
|
35
|
+
## Audio Encoder Profiles (Web)
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
"speech_low_quality" → mono, 16kHz, 24kbps
|
|
39
|
+
"speech_standard" → mono, 32kHz, 24kbps
|
|
40
|
+
"high_quality" → mono, 48kHz, 40kbps
|
|
41
|
+
"high_quality_stereo" → stereo, 48kHz, 128kbps
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Dual Stream (Simulcast)
|
|
45
|
+
|
|
46
|
+
Send high+low quality streams simultaneously. Subscribers choose which to receive, enabling large-scale calls:
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// Web: Enable after joining
|
|
50
|
+
await client.enableDualStream();
|
|
51
|
+
client.setLowStreamParameter({
|
|
52
|
+
width: 160,
|
|
53
|
+
height: 90,
|
|
54
|
+
framerate: 24,
|
|
55
|
+
bitrate: 200,
|
|
56
|
+
});
|
|
57
|
+
// Switch remote user to low stream
|
|
58
|
+
client.setRemoteVideoStreamType(uid, 1); // 0=high, 1=low
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Screen Sharing (Web)
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
const screenTrack = await AgoraRTC.createScreenVideoTrack(
|
|
65
|
+
{
|
|
66
|
+
optimizationMode: 'detail', // or "motion" for video content
|
|
67
|
+
encoderConfig: { width: 1280, height: 720, frameRate: 15 },
|
|
68
|
+
},
|
|
69
|
+
'auto',
|
|
70
|
+
); // "auto" returns [videoTrack, audioTrack] if audio available
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Screen share typically uses a separate client instance to avoid replacing the camera track.
|
|
74
|
+
|
|
75
|
+
## Cross-Platform Interop Notes
|
|
76
|
+
|
|
77
|
+
When Web, iOS, and Android clients share the same channel:
|
|
78
|
+
|
|
79
|
+
- **Codec**: `"vp8"` and `"vp9"` scale better in multi-user calls — prefer these over `"h264"`, which does not scale well beyond small groups. If codecs differ between Web and native clients, Agora's server transcodes transparently (works but adds latency).
|
|
80
|
+
- **UID types**: iOS uses `UInt` (unsigned 32-bit), Android uses `Int` (signed 32-bit), Web uses `number`. UIDs > 2,147,483,647 wrap to negative on Android. RTM uses **string UIDs** — use `String(rtcUid)` as a mapping convention.
|
|
81
|
+
- **Audio profiles**: Align encoder settings across platforms to avoid one side sending stereo 128kbps while another expects mono. Use `"speech_standard"` (Web) / `AUDIO_PROFILE_DEFAULT` (native) for voice calls.
|
|
82
|
+
- **Orientation**: Mobile uses adaptive orientation (rotates with device). Web cameras are typically landscape. Handle aspect ratio changes on the viewer side.
|
|
83
|
+
- **Dual stream**: Enable on all platforms for large calls, not just Web.
|
|
84
|
+
|
|
85
|
+
## Platform Reference Files
|
|
86
|
+
|
|
87
|
+
Read the file matching the user's platform:
|
|
88
|
+
|
|
89
|
+
- **[web.md](web.md)** — `agora-rtc-sdk-ng` (JS/TS): client creation, tracks, events, complete examples
|
|
90
|
+
- **[react.md](react.md)** — `agora-rtc-react` hooks and components
|
|
91
|
+
- **[nextjs.md](nextjs.md)** — Next.js / SSR dynamic import patterns (App Router + Pages Router)
|
|
92
|
+
- **[ios.md](ios.md)** — `AgoraRtcEngineKit` (Swift): engine setup, delegation, permissions
|
|
93
|
+
- **[android.md](android.md)** — `RtcEngine` (Kotlin/Java): engine setup, callbacks, permissions
|
|
94
|
+
- **[react-native.md](react-native.md)** — `react-native-agora`: engine init, events, video views, complete example
|
|
95
|
+
- **[flutter.md](flutter.md)** — `agora_rtc_engine` (Dart): engine init, events, AgoraVideoView, complete example
|
|
96
|
+
- **[cross-platform-coordination.md](cross-platform-coordination.md)** — UID strategy, codec interop, screen sharing across platforms, audio routing, common cross-platform bugs
|
|
97
|
+
|
|
98
|
+
For additional platforms and advanced features: <https://docs-md.agora.io/en/video-calling/get-started/get-started-sdk.md> — voice-only: <https://docs-md.agora.io/en/voice-calling/get-started/get-started-sdk.md>
|
|
99
|
+
|
|
100
|
+
For test setup and mocking patterns, see [references/testing-guidance/SKILL.md](../testing-guidance/SKILL.md).
|
|
101
|
+
|
|
102
|
+
## When to Fetch More
|
|
103
|
+
|
|
104
|
+
Always use Level 2 fetch for: encoder profile parameter details, error code listings, release notes, Windows/Electron/Unity platform quick-starts. See [../doc-fetching.md](../doc-fetching.md).
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# Agora RTC Android SDK (Kotlin)
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Installation](#installation)
|
|
6
|
+
- [Engine Initialization](#engine-initialization)
|
|
7
|
+
- [Joining a Channel](#joining-a-channel)
|
|
8
|
+
- [Video Setup](#video-setup)
|
|
9
|
+
- [Audio Setup](#audio-setup)
|
|
10
|
+
- [Event Handling (IRtcEngineEventHandler)](#event-handling)
|
|
11
|
+
- [Leaving and Cleanup](#leaving-and-cleanup)
|
|
12
|
+
- [Token Renewal](#token-renewal)
|
|
13
|
+
- [Complete Example: Video Call](#complete-example-video-call)
|
|
14
|
+
|
|
15
|
+
API Reference: <https://api-ref.agora.io/en/video-sdk/android/4.x/API/rtc_api_overview.html>
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add to `build.gradle`:
|
|
20
|
+
|
|
21
|
+
```groovy
|
|
22
|
+
dependencies {
|
|
23
|
+
implementation 'io.agora.rtc:full-sdk:4.3.+'
|
|
24
|
+
// or for voice-only: implementation 'io.agora.rtc:voice-sdk:4.3.+'
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Add permissions to `AndroidManifest.xml`:
|
|
29
|
+
|
|
30
|
+
```xml
|
|
31
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
32
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
33
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
34
|
+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
35
|
+
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
36
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
37
|
+
<uses-permission android:name="android.permission.BLUETOOTH" />
|
|
38
|
+
<!-- Android 12+ (API 31+): required for Bluetooth audio headsets -->
|
|
39
|
+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Request runtime permissions for `CAMERA` and `RECORD_AUDIO` before initializing.
|
|
43
|
+
|
|
44
|
+
## Engine Initialization
|
|
45
|
+
|
|
46
|
+
```kotlin
|
|
47
|
+
import io.agora.rtc2.RtcEngine
|
|
48
|
+
import io.agora.rtc2.RtcEngineConfig
|
|
49
|
+
import io.agora.rtc2.Constants
|
|
50
|
+
|
|
51
|
+
val config = RtcEngineConfig().apply {
|
|
52
|
+
mContext = applicationContext
|
|
53
|
+
mAppId = "your-app-id"
|
|
54
|
+
mEventHandler = rtcEventHandler
|
|
55
|
+
mChannelProfile = Constants.CHANNEL_PROFILE_COMMUNICATION
|
|
56
|
+
// or Constants.CHANNEL_PROFILE_LIVE_BROADCASTING
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
val agoraEngine = RtcEngine.create(config)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Joining a Channel
|
|
63
|
+
|
|
64
|
+
```kotlin
|
|
65
|
+
import io.agora.rtc2.ChannelMediaOptions
|
|
66
|
+
|
|
67
|
+
val options = ChannelMediaOptions().apply {
|
|
68
|
+
clientRoleType = Constants.CLIENT_ROLE_BROADCASTER
|
|
69
|
+
channelProfile = Constants.CHANNEL_PROFILE_COMMUNICATION
|
|
70
|
+
publishMicrophoneTrack = true
|
|
71
|
+
publishCameraTrack = true
|
|
72
|
+
autoSubscribeAudio = true
|
|
73
|
+
autoSubscribeVideo = true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
agoraEngine.joinChannel(token, "channel-name", 0, options)
|
|
77
|
+
// token: null for testing, uid: 0 for auto-assignment
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Video Setup
|
|
81
|
+
|
|
82
|
+
```kotlin
|
|
83
|
+
// Enable video module
|
|
84
|
+
agoraEngine.enableVideo()
|
|
85
|
+
|
|
86
|
+
// Configure encoder
|
|
87
|
+
val videoConfig = VideoEncoderConfiguration().apply {
|
|
88
|
+
dimensions = VideoEncoderConfiguration.VideoDimensions(640, 360)
|
|
89
|
+
frameRate = VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_24.value
|
|
90
|
+
bitrate = VideoEncoderConfiguration.STANDARD_BITRATE
|
|
91
|
+
orientationMode = VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE
|
|
92
|
+
}
|
|
93
|
+
agoraEngine.setVideoEncoderConfiguration(videoConfig)
|
|
94
|
+
|
|
95
|
+
// Local preview
|
|
96
|
+
val localSurfaceView = SurfaceView(context)
|
|
97
|
+
localVideoContainer.addView(localSurfaceView)
|
|
98
|
+
agoraEngine.setupLocalVideo(
|
|
99
|
+
VideoCanvas(localSurfaceView, VideoCanvas.RENDER_MODE_HIDDEN, 0)
|
|
100
|
+
)
|
|
101
|
+
agoraEngine.startPreview()
|
|
102
|
+
|
|
103
|
+
// Remote video (call in onUserJoined callback)
|
|
104
|
+
val remoteSurfaceView = SurfaceView(context)
|
|
105
|
+
remoteVideoContainer.addView(remoteSurfaceView)
|
|
106
|
+
agoraEngine.setupRemoteVideo(
|
|
107
|
+
VideoCanvas(remoteSurfaceView, VideoCanvas.RENDER_MODE_HIDDEN, remoteUid)
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Audio Setup
|
|
112
|
+
|
|
113
|
+
```kotlin
|
|
114
|
+
// Enable audio (enabled by default)
|
|
115
|
+
agoraEngine.enableAudio()
|
|
116
|
+
|
|
117
|
+
// Audio profile (SDK 4.x: set profile and scenario separately)
|
|
118
|
+
agoraEngine.setAudioProfile(Constants.AUDIO_PROFILE_DEFAULT)
|
|
119
|
+
agoraEngine.setAudioScenario(Constants.AUDIO_SCENARIO_DEFAULT)
|
|
120
|
+
|
|
121
|
+
// Mute/unmute local audio
|
|
122
|
+
agoraEngine.muteLocalAudioStream(true) // mute
|
|
123
|
+
agoraEngine.muteLocalAudioStream(false) // unmute
|
|
124
|
+
|
|
125
|
+
// Mute/unmute local video (stops sending video, camera stays active)
|
|
126
|
+
agoraEngine.muteLocalVideoStream(true) // video off
|
|
127
|
+
agoraEngine.muteLocalVideoStream(false) // video on
|
|
128
|
+
|
|
129
|
+
// Or disable video entirely (stops camera capture)
|
|
130
|
+
agoraEngine.enableLocalVideo(false) // camera off
|
|
131
|
+
agoraEngine.enableLocalVideo(true) // camera on
|
|
132
|
+
|
|
133
|
+
// Switch between speaker and earpiece
|
|
134
|
+
agoraEngine.setEnableSpeakerphone(true) // speaker
|
|
135
|
+
agoraEngine.setEnableSpeakerphone(false) // earpiece
|
|
136
|
+
|
|
137
|
+
// Mute remote user
|
|
138
|
+
agoraEngine.muteRemoteAudioStream(remoteUid, true)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Event Handling
|
|
142
|
+
|
|
143
|
+
```kotlin
|
|
144
|
+
private val rtcEventHandler = object : IRtcEngineEventHandler() {
|
|
145
|
+
// Successfully joined channel
|
|
146
|
+
override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
|
|
147
|
+
runOnUiThread {
|
|
148
|
+
Log.d("Agora", "Joined channel: $channel, uid: $uid")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Remote user joined
|
|
153
|
+
override fun onUserJoined(uid: Int, elapsed: Int) {
|
|
154
|
+
runOnUiThread {
|
|
155
|
+
val remoteSurfaceView = SurfaceView(context)
|
|
156
|
+
remoteVideoContainer.addView(remoteSurfaceView)
|
|
157
|
+
agoraEngine.setupRemoteVideo(
|
|
158
|
+
VideoCanvas(remoteSurfaceView, VideoCanvas.RENDER_MODE_HIDDEN, uid)
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Remote user left
|
|
164
|
+
override fun onUserOffline(uid: Int, reason: Int) {
|
|
165
|
+
runOnUiThread {
|
|
166
|
+
agoraEngine.setupRemoteVideo(VideoCanvas(null, VideoCanvas.RENDER_MODE_HIDDEN, uid))
|
|
167
|
+
// Remove remote view from container
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Token expiring
|
|
172
|
+
override fun onTokenPrivilegeWillExpire(token: String?) {
|
|
173
|
+
// Fetch new token and renew
|
|
174
|
+
fetchNewToken { newToken ->
|
|
175
|
+
agoraEngine.renewToken(newToken)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Error
|
|
180
|
+
override fun onError(err: Int) {
|
|
181
|
+
Log.e("Agora", "Error code: $err")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Network quality
|
|
185
|
+
override fun onNetworkQuality(uid: Int, txQuality: Int, rxQuality: Int) {
|
|
186
|
+
// 0=unknown, 1=excellent, 2=good, 3=poor, 4=bad, 5=very bad, 6=disconnected
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Leaving and Cleanup
|
|
192
|
+
|
|
193
|
+
```kotlin
|
|
194
|
+
fun leaveChannel() {
|
|
195
|
+
agoraEngine.stopPreview()
|
|
196
|
+
agoraEngine.leaveChannel()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Full cleanup (activity/app destruction)
|
|
200
|
+
fun destroy() {
|
|
201
|
+
agoraEngine.stopPreview()
|
|
202
|
+
agoraEngine.leaveChannel()
|
|
203
|
+
RtcEngine.destroy()
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Token Renewal
|
|
208
|
+
|
|
209
|
+
```kotlin
|
|
210
|
+
override fun onTokenPrivilegeWillExpire(token: String?) {
|
|
211
|
+
lifecycleScope.launch {
|
|
212
|
+
val newToken = fetchTokenFromServer(channelName, localUid)
|
|
213
|
+
agoraEngine.renewToken(newToken)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Complete Example: Video Call
|
|
219
|
+
|
|
220
|
+
```kotlin
|
|
221
|
+
class VideoCallActivity : AppCompatActivity() {
|
|
222
|
+
private val appId = "your-app-id"
|
|
223
|
+
private var token: String? = null
|
|
224
|
+
private val channelName = "test"
|
|
225
|
+
private var agoraEngine: RtcEngine? = null
|
|
226
|
+
private var isJoined = false
|
|
227
|
+
|
|
228
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
229
|
+
super.onCreate(savedInstanceState)
|
|
230
|
+
setContentView(R.layout.activity_video_call)
|
|
231
|
+
|
|
232
|
+
// Request permissions first
|
|
233
|
+
if (checkPermissions()) {
|
|
234
|
+
initializeAgora()
|
|
235
|
+
} else {
|
|
236
|
+
requestPermissions()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private fun initializeAgora() {
|
|
241
|
+
val config = RtcEngineConfig().apply {
|
|
242
|
+
mContext = applicationContext
|
|
243
|
+
mAppId = appId
|
|
244
|
+
mEventHandler = rtcEventHandler
|
|
245
|
+
}
|
|
246
|
+
agoraEngine = RtcEngine.create(config)
|
|
247
|
+
agoraEngine?.enableVideo()
|
|
248
|
+
|
|
249
|
+
agoraEngine?.setVideoEncoderConfiguration(
|
|
250
|
+
VideoEncoderConfiguration().apply {
|
|
251
|
+
dimensions = VideoEncoderConfiguration.VideoDimensions(640, 360)
|
|
252
|
+
frameRate = VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_24.value
|
|
253
|
+
bitrate = VideoEncoderConfiguration.STANDARD_BITRATE
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fun joinChannel() {
|
|
259
|
+
// Setup local preview
|
|
260
|
+
val localView = SurfaceView(this)
|
|
261
|
+
findViewById<FrameLayout>(R.id.local_video_container).addView(localView)
|
|
262
|
+
agoraEngine?.setupLocalVideo(VideoCanvas(localView, VideoCanvas.RENDER_MODE_HIDDEN, 0))
|
|
263
|
+
agoraEngine?.startPreview()
|
|
264
|
+
|
|
265
|
+
// Join channel
|
|
266
|
+
val options = ChannelMediaOptions().apply {
|
|
267
|
+
clientRoleType = Constants.CLIENT_ROLE_BROADCASTER
|
|
268
|
+
channelProfile = Constants.CHANNEL_PROFILE_COMMUNICATION
|
|
269
|
+
publishMicrophoneTrack = true
|
|
270
|
+
publishCameraTrack = true
|
|
271
|
+
}
|
|
272
|
+
agoraEngine?.joinChannel(token, channelName, 0, options)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fun leaveChannel() {
|
|
276
|
+
agoraEngine?.stopPreview()
|
|
277
|
+
agoraEngine?.leaveChannel()
|
|
278
|
+
isJoined = false
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private val rtcEventHandler = object : IRtcEngineEventHandler() {
|
|
282
|
+
override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
|
|
283
|
+
runOnUiThread { isJoined = true }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
override fun onUserJoined(uid: Int, elapsed: Int) {
|
|
287
|
+
runOnUiThread {
|
|
288
|
+
val remoteView = SurfaceView(this@VideoCallActivity)
|
|
289
|
+
findViewById<FrameLayout>(R.id.remote_video_container).addView(remoteView)
|
|
290
|
+
agoraEngine?.setupRemoteVideo(
|
|
291
|
+
VideoCanvas(remoteView, VideoCanvas.RENDER_MODE_HIDDEN, uid)
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
override fun onUserOffline(uid: Int, reason: Int) {
|
|
297
|
+
runOnUiThread {
|
|
298
|
+
agoraEngine?.setupRemoteVideo(VideoCanvas(null, VideoCanvas.RENDER_MODE_HIDDEN, uid))
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
override fun onTokenPrivilegeWillExpire(token: String?) {
|
|
303
|
+
lifecycleScope.launch {
|
|
304
|
+
val newToken = fetchTokenFromServer(channelName, 0)
|
|
305
|
+
agoraEngine?.renewToken(newToken)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
override fun onDestroy() {
|
|
311
|
+
super.onDestroy()
|
|
312
|
+
agoraEngine?.stopPreview()
|
|
313
|
+
agoraEngine?.leaveChannel()
|
|
314
|
+
RtcEngine.destroy()
|
|
315
|
+
agoraEngine = null
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private fun checkPermissions(): Boolean {
|
|
319
|
+
return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==
|
|
320
|
+
PackageManager.PERMISSION_GRANTED &&
|
|
321
|
+
ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ==
|
|
322
|
+
PackageManager.PERMISSION_GRANTED
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private fun requestPermissions() {
|
|
326
|
+
ActivityCompat.requestPermissions(
|
|
327
|
+
this,
|
|
328
|
+
arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO),
|
|
329
|
+
PERMISSION_REQUEST_CODE
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
companion object {
|
|
334
|
+
private const val PERMISSION_REQUEST_CODE = 22
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## Official Documentation
|
|
340
|
+
|
|
341
|
+
For APIs or features not covered above:
|
|
342
|
+
|
|
343
|
+
- API Reference: <https://api-ref.agora.io/en/video-sdk/android/4.x/API/rtc_api_overview.html>
|
|
344
|
+
- Guides: <https://docs.agora.io/en/video-calling/overview/product-overview>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# RTC Cross-Platform Coordination
|
|
2
|
+
|
|
3
|
+
Patterns for apps where users on different platforms (Web, iOS, Android) join the same Agora RTC channel.
|
|
4
|
+
|
|
5
|
+
## UID Strategy
|
|
6
|
+
|
|
7
|
+
Agora assigns UIDs per channel. For multi-platform apps:
|
|
8
|
+
|
|
9
|
+
- **Auto-assign on all clients**: Pass `null`/`0` to `join()` — each platform auto-receives a unique numeric UID. Clients subscribe to all remote users regardless of platform.
|
|
10
|
+
- **Fixed UIDs**: Assign specific UIDs per user role (e.g., `1001` for host, `1002` for co-host) when you need deterministic lookup. Must be unique per channel — duplicates cause undefined behavior.
|
|
11
|
+
- **UID limits**: Numeric UIDs must be `0` to `2^32 - 1`. String UIDs must be ASCII and at most `255` characters.
|
|
12
|
+
- **RTC + RTM coordination**: After RTC join, use `String(rtcUid)` as the RTM user ID to correlate users across both systems (see [../rtm/README.md](../rtm/README.md)).
|
|
13
|
+
|
|
14
|
+
## Codec Interoperability
|
|
15
|
+
|
|
16
|
+
Agora handles codec negotiation automatically for most scenarios. What to know:
|
|
17
|
+
|
|
18
|
+
| Codec | Notes |
|
|
19
|
+
|-------|-------|
|
|
20
|
+
| VP8 | Web default. Scales well in multi-user calls. Supported on Safari 13+. Recommended. |
|
|
21
|
+
| VP9 | Better compression than VP8. Scales well on desktop. **iOS Safari: hardware-only** — requires iPhone 15 Pro / M3 Mac or newer; software fallback degrades battery significantly on older devices. |
|
|
22
|
+
| H.264 | Default on iOS and Android native SDKs. Does not scale well beyond small groups — avoid for multi-user Web calls. |
|
|
23
|
+
| H.265 (HEVC) | Not universally supported on Web; avoid for cross-platform channels. |
|
|
24
|
+
|
|
25
|
+
**Recommendation**: `'vp8'` is the safest default for multi-user Web calls — scales well and works on all modern Safari (13+). Use `'vp9'` only if you can ensure participants are on modern hardware. Avoid `'h264'` for multi-user Web scenarios. If codecs differ between Web and native clients, Agora's server transcodes transparently, which adds latency and is billed separately.
|
|
26
|
+
|
|
27
|
+
## Screen Sharing (Cross-Platform)
|
|
28
|
+
|
|
29
|
+
Screen share is a separate track/stream, not a replacement for the camera track.
|
|
30
|
+
|
|
31
|
+
- **Web**: `AgoraRTC.createScreenVideoTrack()` — publishes as a second video track. See [web.md](web.md) for dual-stream setup.
|
|
32
|
+
- **iOS**: `AgoraRtcEngineKit.startScreenCapture(_:)` with broadcast extension — different lifecycle than camera.
|
|
33
|
+
- **Android**: `MediaProjection` API + `RtcEngine.startScreenCapture()`.
|
|
34
|
+
|
|
35
|
+
Remote users on any platform subscribe to the screen share UID as a normal remote user — the stream is just another video track from a different UID.
|
|
36
|
+
|
|
37
|
+
**Key rule**: Screen share uses a separate channel join with a different UID. Never publish camera and screen share from the same UID.
|
|
38
|
+
|
|
39
|
+
## Audio Routing Differences
|
|
40
|
+
|
|
41
|
+
| Platform | Default audio output | Override |
|
|
42
|
+
|----------|---------------------|---------|
|
|
43
|
+
| Web | Speaker (browser-controlled) | Not configurable via SDK |
|
|
44
|
+
| iOS | Earpiece for `rtc` mode | `setDefaultAudioRouteToSpeakerphone(true)` for speaker |
|
|
45
|
+
| Android | Earpiece by default | `setEnableSpeakerphone(true)` |
|
|
46
|
+
|
|
47
|
+
When a user plugs in headphones, iOS/Android switch automatically. Web relies on the browser and OS audio routing — the SDK cannot override this.
|
|
48
|
+
|
|
49
|
+
## Testing Multi-Platform Channels Locally
|
|
50
|
+
|
|
51
|
+
1. **Web + Simulator/Emulator**: Connect both to the same channel; verify remote tracks appear on both sides.
|
|
52
|
+
2. **Different UIDs on same machine**: Open two browser tabs or two simulator instances — each gets its own UID automatically.
|
|
53
|
+
3. **Cross-device**: Use the Agora [Web Demo](https://webdemo.agora.io) to join from a browser while testing your native app in the same channel.
|
|
54
|
+
4. **Codec check**: In the Agora Console → Real-time Monitoring, inspect active streams to confirm codec negotiation.
|
|
55
|
+
|
|
56
|
+
## Common Cross-Platform Bugs
|
|
57
|
+
|
|
58
|
+
- **Remote user appears then immediately disappears on iOS** — usually a token expiry or UID collision. Check `connectionStateChanged` delegate for the reason code.
|
|
59
|
+
- **No video from Android on Web** — codec mismatch. Android may be sending H.265; Web can't decode it. Force H.264 on Android via `VideoEncoderConfiguration`.
|
|
60
|
+
- **Audio works, video doesn't on mobile** — camera permission not granted. Check permission before calling `startPreview()`.
|
|
61
|
+
- **Web user sees mobile user but not vice versa** — `user-published` event not registered before `client.join()`. Event handlers must be set up before joining — see RTC critical rules in [README.md](README.md).
|