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,282 @@
|
|
|
1
|
+
# Agora RTC Flutter SDK
|
|
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](#event-handling)
|
|
11
|
+
- [Leaving and Cleanup](#leaving-and-cleanup)
|
|
12
|
+
- [Complete Example](#complete-example)
|
|
13
|
+
|
|
14
|
+
API Reference: <https://api-ref.agora.io/en/video-sdk/flutter/6.x/API/rtc_api_overview.html>
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add to `pubspec.yaml`:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
dependencies:
|
|
22
|
+
agora_rtc_engine: ^6.5.0
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Run `flutter pub get`.
|
|
26
|
+
|
|
27
|
+
Add permissions to `AndroidManifest.xml`:
|
|
28
|
+
|
|
29
|
+
```xml
|
|
30
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
31
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
32
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
33
|
+
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
34
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
35
|
+
<uses-permission android:name="android.permission.BLUETOOTH" />
|
|
36
|
+
<!-- Android 12+ (API 31+): required for Bluetooth audio headsets -->
|
|
37
|
+
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Add to `Info.plist` (iOS):
|
|
41
|
+
|
|
42
|
+
```xml
|
|
43
|
+
<key>NSCameraUsageDescription</key>
|
|
44
|
+
<string>Camera access for video calls</string>
|
|
45
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
46
|
+
<string>Microphone access for audio calls</string>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Engine Initialization
|
|
50
|
+
|
|
51
|
+
```dart
|
|
52
|
+
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
|
53
|
+
|
|
54
|
+
late RtcEngine _engine;
|
|
55
|
+
|
|
56
|
+
Future<void> initializeAgora() async {
|
|
57
|
+
_engine = createAgoraRtcEngine();
|
|
58
|
+
await _engine.initialize(const RtcEngineContext(
|
|
59
|
+
appId: 'your-app-id',
|
|
60
|
+
channelProfile: ChannelProfileType.channelProfileCommunication,
|
|
61
|
+
));
|
|
62
|
+
await _engine.enableVideo();
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Joining a Channel
|
|
67
|
+
|
|
68
|
+
```dart
|
|
69
|
+
Future<void> joinChannel() async {
|
|
70
|
+
await _engine.joinChannel(
|
|
71
|
+
token: null, // null for testing
|
|
72
|
+
channelId: 'channel-name',
|
|
73
|
+
uid: 0, // 0 for auto-assignment
|
|
74
|
+
options: const ChannelMediaOptions(
|
|
75
|
+
clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
|
76
|
+
publishMicrophoneTrack: true,
|
|
77
|
+
publishCameraTrack: true,
|
|
78
|
+
autoSubscribeAudio: true,
|
|
79
|
+
autoSubscribeVideo: true,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Video Setup
|
|
86
|
+
|
|
87
|
+
```dart
|
|
88
|
+
// Local preview — use uid=0
|
|
89
|
+
AgoraVideoView(
|
|
90
|
+
controller: VideoViewController(
|
|
91
|
+
rtcEngine: _engine,
|
|
92
|
+
canvas: const VideoCanvas(uid: 0),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
// Remote video — use the remote user's uid
|
|
97
|
+
AgoraVideoView(
|
|
98
|
+
controller: VideoViewController.remote(
|
|
99
|
+
rtcEngine: _engine,
|
|
100
|
+
canvas: VideoCanvas(uid: remoteUid),
|
|
101
|
+
connection: const RtcConnection(channelId: 'channel-name'),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Audio Setup
|
|
107
|
+
|
|
108
|
+
```dart
|
|
109
|
+
// Mute/unmute local audio
|
|
110
|
+
await _engine.muteLocalAudioStream(true) // mute
|
|
111
|
+
await _engine.muteLocalAudioStream(false) // unmute
|
|
112
|
+
|
|
113
|
+
// Mute/unmute local video
|
|
114
|
+
await _engine.muteLocalVideoStream(true) // video off
|
|
115
|
+
await _engine.muteLocalVideoStream(false) // video on
|
|
116
|
+
|
|
117
|
+
// Speaker vs earpiece
|
|
118
|
+
await _engine.setEnableSpeakerphone(true) // speaker
|
|
119
|
+
await _engine.setEnableSpeakerphone(false) // earpiece
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Event Handling
|
|
123
|
+
|
|
124
|
+
Register handlers **before** joining the channel.
|
|
125
|
+
|
|
126
|
+
```dart
|
|
127
|
+
_engine.registerEventHandler(
|
|
128
|
+
RtcEngineEventHandler(
|
|
129
|
+
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
|
|
130
|
+
print('Joined: ${connection.channelId}, uid: ${connection.localUid}');
|
|
131
|
+
},
|
|
132
|
+
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
|
|
133
|
+
print('Remote user joined: $remoteUid');
|
|
134
|
+
setState(() => _remoteUid = remoteUid);
|
|
135
|
+
},
|
|
136
|
+
onUserOffline: (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {
|
|
137
|
+
print('Remote user left: $remoteUid');
|
|
138
|
+
setState(() => _remoteUid = null);
|
|
139
|
+
},
|
|
140
|
+
onTokenPrivilegeWillExpire: (RtcConnection connection, String token) async {
|
|
141
|
+
final newToken = await fetchTokenFromServer();
|
|
142
|
+
await _engine.renewToken(newToken);
|
|
143
|
+
},
|
|
144
|
+
onError: (ErrorCodeType err, String msg) {
|
|
145
|
+
print('Error: $err $msg');
|
|
146
|
+
},
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Leaving and Cleanup
|
|
152
|
+
|
|
153
|
+
```dart
|
|
154
|
+
Future<void> leaveChannel() async {
|
|
155
|
+
await _engine.leaveChannel();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Full cleanup when widget is disposed
|
|
159
|
+
@override
|
|
160
|
+
void dispose() {
|
|
161
|
+
_engine.leaveChannel();
|
|
162
|
+
_engine.release();
|
|
163
|
+
super.dispose();
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Always call `release()` when the engine is no longer needed.
|
|
168
|
+
|
|
169
|
+
## Complete Example
|
|
170
|
+
|
|
171
|
+
```dart
|
|
172
|
+
import 'package:flutter/material.dart';
|
|
173
|
+
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
|
174
|
+
|
|
175
|
+
const appId = 'your-app-id';
|
|
176
|
+
const token = null; // null for testing
|
|
177
|
+
const channel = 'test';
|
|
178
|
+
|
|
179
|
+
class VideoCallPage extends StatefulWidget {
|
|
180
|
+
const VideoCallPage({super.key});
|
|
181
|
+
@override
|
|
182
|
+
State<VideoCallPage> createState() => _VideoCallPageState();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class _VideoCallPageState extends State<VideoCallPage> {
|
|
186
|
+
late RtcEngine _engine;
|
|
187
|
+
bool _joined = false;
|
|
188
|
+
int? _remoteUid;
|
|
189
|
+
|
|
190
|
+
@override
|
|
191
|
+
void initState() {
|
|
192
|
+
super.initState();
|
|
193
|
+
_initAgora();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Future<void> _initAgora() async {
|
|
197
|
+
_engine = createAgoraRtcEngine();
|
|
198
|
+
await _engine.initialize(const RtcEngineContext(appId: appId));
|
|
199
|
+
await _engine.enableVideo();
|
|
200
|
+
|
|
201
|
+
_engine.registerEventHandler(RtcEngineEventHandler(
|
|
202
|
+
onJoinChannelSuccess: (_, __) => setState(() => _joined = true),
|
|
203
|
+
onUserJoined: (_, uid, __) => setState(() => _remoteUid = uid),
|
|
204
|
+
onUserOffline: (_, uid, __) => setState(() => _remoteUid = null),
|
|
205
|
+
));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
Future<void> _join() async {
|
|
209
|
+
await _engine.startPreview();
|
|
210
|
+
await _engine.joinChannel(
|
|
211
|
+
token: token,
|
|
212
|
+
channelId: channel,
|
|
213
|
+
uid: 0,
|
|
214
|
+
options: const ChannelMediaOptions(
|
|
215
|
+
clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
|
216
|
+
publishMicrophoneTrack: true,
|
|
217
|
+
publishCameraTrack: true,
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
Future<void> _leave() async {
|
|
223
|
+
await _engine.leaveChannel();
|
|
224
|
+
setState(() {
|
|
225
|
+
_joined = false;
|
|
226
|
+
_remoteUid = null;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@override
|
|
231
|
+
void dispose() {
|
|
232
|
+
_engine.release();
|
|
233
|
+
super.dispose();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@override
|
|
237
|
+
Widget build(BuildContext context) {
|
|
238
|
+
return Scaffold(
|
|
239
|
+
body: Stack(
|
|
240
|
+
children: [
|
|
241
|
+
if (_joined)
|
|
242
|
+
AgoraVideoView(
|
|
243
|
+
controller: VideoViewController(
|
|
244
|
+
rtcEngine: _engine,
|
|
245
|
+
canvas: const VideoCanvas(uid: 0),
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
if (_remoteUid != null)
|
|
249
|
+
Positioned(
|
|
250
|
+
right: 16, top: 16, width: 120, height: 160,
|
|
251
|
+
child: AgoraVideoView(
|
|
252
|
+
controller: VideoViewController.remote(
|
|
253
|
+
rtcEngine: _engine,
|
|
254
|
+
canvas: VideoCanvas(uid: _remoteUid!),
|
|
255
|
+
connection: const RtcConnection(channelId: channel),
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
),
|
|
259
|
+
Positioned(
|
|
260
|
+
bottom: 32, left: 0, right: 0,
|
|
261
|
+
child: Center(
|
|
262
|
+
child: ElevatedButton(
|
|
263
|
+
onPressed: _joined ? _leave : _join,
|
|
264
|
+
child: Text(_joined ? 'Leave' : 'Join'),
|
|
265
|
+
),
|
|
266
|
+
),
|
|
267
|
+
),
|
|
268
|
+
],
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
For test setup and mocking patterns, see [references/testing-guidance/SKILL.md](../testing-guidance/SKILL.md).
|
|
276
|
+
|
|
277
|
+
## Official Documentation
|
|
278
|
+
|
|
279
|
+
For APIs or features not covered above:
|
|
280
|
+
|
|
281
|
+
- Quick-start guide: <https://docs-md.agora.io/en/video-calling/get-started/get-started-sdk.md?platform=flutter>
|
|
282
|
+
- API Reference: <https://api-ref.agora.io/en/video-sdk/flutter/6.x/API/rtc_api_overview.html>
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Agora RTC iOS SDK (Swift)
|
|
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 (Delegate)](#event-handling-delegate)
|
|
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/ios/4.x/API/rtc_api_overview_ng.html>
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Swift Package Manager
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
https://github.com/AgoraIO/AgoraRtcEngine_iOS
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### CocoaPods
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
pod 'AgoraRtcEngine_iOS', '~> 4.3'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Add to Info.plist:
|
|
32
|
+
|
|
33
|
+
```xml
|
|
34
|
+
<key>NSCameraUsageDescription</key>
|
|
35
|
+
<string>For video calling</string>
|
|
36
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
37
|
+
<string>For voice calling</string>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Engine Initialization
|
|
41
|
+
|
|
42
|
+
```swift
|
|
43
|
+
import AgoraRtcKit
|
|
44
|
+
|
|
45
|
+
let config = AgoraRtcEngineConfig()
|
|
46
|
+
config.appId = "your-app-id"
|
|
47
|
+
config.channelProfile = .communication // or .liveBroadcasting
|
|
48
|
+
|
|
49
|
+
let agoraEngine = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Joining a Channel
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
// Communication mode
|
|
56
|
+
let option = AgoraRtcChannelMediaOptions()
|
|
57
|
+
option.clientRoleType = .broadcaster // .broadcaster or .audience
|
|
58
|
+
option.channelProfile = .communication
|
|
59
|
+
|
|
60
|
+
agoraEngine.joinChannel(
|
|
61
|
+
byToken: token, // nil for testing
|
|
62
|
+
channelId: "channel-name",
|
|
63
|
+
uid: 0, // 0 = auto-assign
|
|
64
|
+
mediaOptions: option
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Video Setup
|
|
69
|
+
|
|
70
|
+
```swift
|
|
71
|
+
// Enable video module
|
|
72
|
+
agoraEngine.enableVideo()
|
|
73
|
+
|
|
74
|
+
// Configure video encoder
|
|
75
|
+
let videoConfig = AgoraVideoEncoderConfiguration(
|
|
76
|
+
size: CGSize(width: 640, height: 360),
|
|
77
|
+
frameRate: .fps24,
|
|
78
|
+
bitrate: AgoraVideoBitrateStandard,
|
|
79
|
+
orientationMode: .adaptative
|
|
80
|
+
)
|
|
81
|
+
agoraEngine.setVideoEncoderConfiguration(videoConfig)
|
|
82
|
+
|
|
83
|
+
// Local video preview
|
|
84
|
+
let localView = UIView(frame: localVideoContainer.bounds)
|
|
85
|
+
let canvas = AgoraRtcVideoCanvas()
|
|
86
|
+
canvas.uid = 0 // 0 = local user
|
|
87
|
+
canvas.view = localView
|
|
88
|
+
canvas.renderMode = .hidden
|
|
89
|
+
agoraEngine.setupLocalVideo(canvas)
|
|
90
|
+
agoraEngine.startPreview()
|
|
91
|
+
|
|
92
|
+
// Remote video (call in didJoinedOfUid delegate)
|
|
93
|
+
let remoteCanvas = AgoraRtcVideoCanvas()
|
|
94
|
+
remoteCanvas.uid = remoteUid
|
|
95
|
+
remoteCanvas.view = remoteVideoView
|
|
96
|
+
remoteCanvas.renderMode = .hidden
|
|
97
|
+
agoraEngine.setupRemoteVideo(remoteCanvas)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Audio Setup
|
|
101
|
+
|
|
102
|
+
```swift
|
|
103
|
+
// Enable audio (enabled by default)
|
|
104
|
+
agoraEngine.enableAudio()
|
|
105
|
+
|
|
106
|
+
// Audio profile
|
|
107
|
+
agoraEngine.setAudioProfile(.default)
|
|
108
|
+
|
|
109
|
+
// Mute/unmute local audio
|
|
110
|
+
agoraEngine.muteLocalAudioStream(true) // mute
|
|
111
|
+
agoraEngine.muteLocalAudioStream(false) // unmute
|
|
112
|
+
|
|
113
|
+
// Mute/unmute local video (stops sending video, camera stays active)
|
|
114
|
+
agoraEngine.muteLocalVideoStream(true) // video off
|
|
115
|
+
agoraEngine.muteLocalVideoStream(false) // video on
|
|
116
|
+
|
|
117
|
+
// Or disable video entirely (stops camera capture)
|
|
118
|
+
agoraEngine.enableLocalVideo(false) // camera off
|
|
119
|
+
agoraEngine.enableLocalVideo(true) // camera on
|
|
120
|
+
|
|
121
|
+
// Switch between speaker and earpiece
|
|
122
|
+
agoraEngine.setEnableSpeakerphone(true) // speaker
|
|
123
|
+
agoraEngine.setEnableSpeakerphone(false) // earpiece
|
|
124
|
+
|
|
125
|
+
// Mute remote user
|
|
126
|
+
agoraEngine.muteRemoteAudioStream(remoteUid, mute: true)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Event Handling (Delegate)
|
|
130
|
+
|
|
131
|
+
```swift
|
|
132
|
+
extension ViewController: AgoraRtcEngineDelegate {
|
|
133
|
+
// Successfully joined channel
|
|
134
|
+
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) {
|
|
135
|
+
print("Joined channel: \(channel), uid: \(uid)")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Remote user joined
|
|
139
|
+
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
|
|
140
|
+
// Setup remote video view
|
|
141
|
+
let canvas = AgoraRtcVideoCanvas()
|
|
142
|
+
canvas.uid = uid
|
|
143
|
+
canvas.view = remoteVideoView
|
|
144
|
+
canvas.renderMode = .hidden
|
|
145
|
+
engine.setupRemoteVideo(canvas)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Remote user left
|
|
149
|
+
func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
|
|
150
|
+
// Clean up remote view
|
|
151
|
+
let canvas = AgoraRtcVideoCanvas()
|
|
152
|
+
canvas.uid = uid
|
|
153
|
+
canvas.view = nil
|
|
154
|
+
engine.setupRemoteVideo(canvas)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Token expiring
|
|
158
|
+
func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) {
|
|
159
|
+
// Fetch new token from server and renew
|
|
160
|
+
fetchNewToken { newToken in
|
|
161
|
+
engine.renewToken(newToken)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Error
|
|
166
|
+
func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) {
|
|
167
|
+
print("Agora error: \(errorCode.rawValue)")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Network quality
|
|
171
|
+
func rtcEngine(_ engine: AgoraRtcEngineKit, networkQuality uid: UInt,
|
|
172
|
+
txQuality: AgoraNetworkQuality, rxQuality: AgoraNetworkQuality) {
|
|
173
|
+
// 0=unknown, 1=excellent, 2=good, 3=poor, 4=bad, 5=very bad
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Leaving and Cleanup
|
|
179
|
+
|
|
180
|
+
```swift
|
|
181
|
+
func leaveChannel() {
|
|
182
|
+
agoraEngine.stopPreview()
|
|
183
|
+
agoraEngine.leaveChannel(nil)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Full cleanup (app termination)
|
|
187
|
+
func destroy() {
|
|
188
|
+
agoraEngine.stopPreview()
|
|
189
|
+
agoraEngine.leaveChannel(nil)
|
|
190
|
+
AgoraRtcEngineKit.destroy()
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Token Renewal
|
|
195
|
+
|
|
196
|
+
```swift
|
|
197
|
+
func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) {
|
|
198
|
+
Task {
|
|
199
|
+
let newToken = await fetchTokenFromServer(channel: channelName, uid: localUid)
|
|
200
|
+
engine.renewToken(newToken)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Complete Example: Video Call
|
|
206
|
+
|
|
207
|
+
```swift
|
|
208
|
+
import UIKit
|
|
209
|
+
import AgoraRtcKit
|
|
210
|
+
|
|
211
|
+
class VideoCallViewController: UIViewController {
|
|
212
|
+
private var agoraEngine: AgoraRtcEngineKit!
|
|
213
|
+
private let appId = "your-app-id"
|
|
214
|
+
private var token: String? = nil
|
|
215
|
+
private let channelName = "test"
|
|
216
|
+
|
|
217
|
+
@IBOutlet weak var localVideoView: UIView!
|
|
218
|
+
@IBOutlet weak var remoteVideoView: UIView!
|
|
219
|
+
|
|
220
|
+
override func viewDidLoad() {
|
|
221
|
+
super.viewDidLoad()
|
|
222
|
+
initializeAgora()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private func initializeAgora() {
|
|
226
|
+
let config = AgoraRtcEngineConfig()
|
|
227
|
+
config.appId = appId
|
|
228
|
+
config.channelProfile = .communication
|
|
229
|
+
|
|
230
|
+
agoraEngine = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
|
|
231
|
+
agoraEngine.enableVideo()
|
|
232
|
+
agoraEngine.setVideoEncoderConfiguration(
|
|
233
|
+
AgoraVideoEncoderConfiguration(
|
|
234
|
+
size: CGSize(width: 640, height: 360),
|
|
235
|
+
frameRate: .fps24,
|
|
236
|
+
bitrate: AgoraVideoBitrateStandard,
|
|
237
|
+
orientationMode: .adaptative
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@IBAction func joinChannel(_ sender: Any) {
|
|
243
|
+
// Setup local preview
|
|
244
|
+
let localCanvas = AgoraRtcVideoCanvas()
|
|
245
|
+
localCanvas.uid = 0
|
|
246
|
+
localCanvas.view = localVideoView
|
|
247
|
+
localCanvas.renderMode = .hidden
|
|
248
|
+
agoraEngine.setupLocalVideo(localCanvas)
|
|
249
|
+
agoraEngine.startPreview()
|
|
250
|
+
|
|
251
|
+
// Join
|
|
252
|
+
let option = AgoraRtcChannelMediaOptions()
|
|
253
|
+
option.clientRoleType = .broadcaster
|
|
254
|
+
option.channelProfile = .communication
|
|
255
|
+
|
|
256
|
+
agoraEngine.joinChannel(
|
|
257
|
+
byToken: token,
|
|
258
|
+
channelId: channelName,
|
|
259
|
+
uid: 0,
|
|
260
|
+
mediaOptions: option
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@IBAction func leaveChannel(_ sender: Any) {
|
|
265
|
+
agoraEngine.stopPreview()
|
|
266
|
+
agoraEngine.leaveChannel(nil)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
deinit {
|
|
270
|
+
agoraEngine.stopPreview()
|
|
271
|
+
agoraEngine.leaveChannel(nil)
|
|
272
|
+
AgoraRtcEngineKit.destroy()
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
extension VideoCallViewController: AgoraRtcEngineDelegate {
|
|
277
|
+
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
|
|
278
|
+
let remoteCanvas = AgoraRtcVideoCanvas()
|
|
279
|
+
remoteCanvas.uid = uid
|
|
280
|
+
remoteCanvas.view = remoteVideoView
|
|
281
|
+
remoteCanvas.renderMode = .hidden
|
|
282
|
+
agoraEngine.setupRemoteVideo(remoteCanvas)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
|
|
286
|
+
let canvas = AgoraRtcVideoCanvas()
|
|
287
|
+
canvas.uid = uid
|
|
288
|
+
canvas.view = nil
|
|
289
|
+
agoraEngine.setupRemoteVideo(canvas)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) {
|
|
293
|
+
Task {
|
|
294
|
+
let newToken = await fetchTokenFromServer()
|
|
295
|
+
agoraEngine.renewToken(newToken)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Official Documentation
|
|
302
|
+
|
|
303
|
+
For APIs or features not covered above:
|
|
304
|
+
|
|
305
|
+
- API Reference: <https://api-ref.agora.io/en/video-sdk/ios/4.x/API/rtc_api_overview_ng.html>
|
|
306
|
+
- Guides: <https://docs.agora.io/en/video-calling/overview/product-overview>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Agora RTC — Next.js / SSR
|
|
2
|
+
|
|
3
|
+
The Agora Web SDK (`agora-rtc-sdk-ng`) and `agora-rtc-react` are browser-only. They cannot run during server-side rendering and will throw errors if imported at the module level in a Next.js Server Component or during SSR.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
`next/dynamic` with `ssr: false` works in Pages Router and in Client Components, but **does NOT work in Server Components** in Next.js 14+ App Router. A Server Component importing a dynamically-loaded client module that references Agora will still fail at build time if the import is not properly isolated.
|
|
8
|
+
|
|
9
|
+
## Pattern: Wrap Both the Provider and Component
|
|
10
|
+
|
|
11
|
+
The correct pattern for Next.js App Router is to dynamically import both your Agora component **and** the `AgoraRTCProvider` together, inside a Client Component:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
'use client';
|
|
15
|
+
|
|
16
|
+
import { useMemo, Suspense } from 'react';
|
|
17
|
+
import dynamic from 'next/dynamic';
|
|
18
|
+
|
|
19
|
+
// Dynamically import your RTC component with ssr disabled
|
|
20
|
+
const ConversationComponent = dynamic(() => import('./ConversationComponent'), {
|
|
21
|
+
ssr: false,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Dynamically import AgoraRTCProvider and create the client inside the same
|
|
25
|
+
// dynamic boundary — this keeps all Agora imports out of the SSR bundle
|
|
26
|
+
const AgoraProvider = dynamic(
|
|
27
|
+
async () => {
|
|
28
|
+
const { AgoraRTCProvider, default: AgoraRTC } =
|
|
29
|
+
await import('agora-rtc-react');
|
|
30
|
+
return {
|
|
31
|
+
default: ({ children }: { children: React.ReactNode }) => {
|
|
32
|
+
const client = useMemo(
|
|
33
|
+
() => AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' }),
|
|
34
|
+
[],
|
|
35
|
+
);
|
|
36
|
+
return <AgoraRTCProvider client={client}>{children}</AgoraRTCProvider>;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
{ ssr: false },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export default function Page() {
|
|
44
|
+
return (
|
|
45
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
46
|
+
<AgoraProvider>
|
|
47
|
+
<ConversationComponent />
|
|
48
|
+
</AgoraProvider>
|
|
49
|
+
</Suspense>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Why this works:** Both `AgoraRTCProvider` and `AgoraRTC.createClient` are inside the dynamic import callback, so they only execute in the browser. The `useMemo` ensures the client is created once per component mount, not on every render.
|
|
55
|
+
|
|
56
|
+
## Pattern: Simple Component Lazy Load (Pages Router / Client Components)
|
|
57
|
+
|
|
58
|
+
If you're in a Pages Router context or inside an existing `"use client"` boundary, the simpler pattern works:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
'use client';
|
|
62
|
+
|
|
63
|
+
import { useState, useEffect } from 'react';
|
|
64
|
+
|
|
65
|
+
export default function Page() {
|
|
66
|
+
const [VideoCall, setVideoCall] = useState<React.ComponentType | null>(null);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
import('./VideoCall').then((m) => setVideoCall(() => m.default));
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
if (!VideoCall) return <div>Loading...</div>;
|
|
73
|
+
return <VideoCall />;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Key Rules
|
|
78
|
+
|
|
79
|
+
- Mark any component that imports Agora with `"use client"`.
|
|
80
|
+
- Never import `agora-rtc-sdk-ng` or `agora-rtc-react` at the top level of a Server Component.
|
|
81
|
+
- Create the `AgoraRTC.createClient` instance inside a `useMemo` (or outside the component tree) — never in the render function directly.
|
|
82
|
+
- Wrap dynamic-loaded Agora components in `<Suspense>` to handle the async load state.
|
|
83
|
+
|
|
84
|
+
## Node.js Version Requirements
|
|
85
|
+
|
|
86
|
+
- Next.js 14 / 15: requires Node.js >= 18
|
|
87
|
+
- Next.js 16+: requires Node.js >= 20.9.0 (used in agent-samples clients)
|