@defcomm/meetingsdk 0.1.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 +1000 -0
- package/dist/index.d.mts +543 -0
- package/dist/index.d.ts +543 -0
- package/dist/index.js +2289 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2253 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
# @afosecure/meetingsdk
|
|
2
|
+
|
|
3
|
+
React/TypeScript SDK for the **DEFCOMM Rust SFU**.
|
|
4
|
+
|
|
5
|
+
This guide is for developers integrating the SDK into a React application. It covers installation, configuration, joining, media, participants, screen sharing, reconnection, recording, room management, signaling, and production integration.
|
|
6
|
+
|
|
7
|
+
> **Important:** This package is an SFU client. It is **not** a mesh/P2P SDK.
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @afosecure/meetingsdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Configure the SDK once:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { updateSDKConfig } from "@afosecure/meetingsdk";
|
|
19
|
+
|
|
20
|
+
updateSDKConfig({
|
|
21
|
+
apiBase: "https://your-sfu.example.com/api",
|
|
22
|
+
wsBase: "wss://your-sfu.example.com/ws",
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then put your meeting UI inside `MeetingProvider`:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import {
|
|
30
|
+
MeetingProvider,
|
|
31
|
+
useMeeting,
|
|
32
|
+
useParticipants,
|
|
33
|
+
} from "@afosecure/meetingsdk";
|
|
34
|
+
|
|
35
|
+
function MeetingRoom() {
|
|
36
|
+
const {
|
|
37
|
+
join,
|
|
38
|
+
leave,
|
|
39
|
+
toggleMic,
|
|
40
|
+
toggleCam,
|
|
41
|
+
startScreenShare,
|
|
42
|
+
stopScreenShare,
|
|
43
|
+
startRecording,
|
|
44
|
+
stopRecording,
|
|
45
|
+
} = useMeeting();
|
|
46
|
+
|
|
47
|
+
const participants = useParticipants();
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div>
|
|
51
|
+
<button onClick={join}>Join</button>
|
|
52
|
+
<button onClick={toggleMic}>Mic</button>
|
|
53
|
+
<button onClick={toggleCam}>Camera</button>
|
|
54
|
+
<button onClick={startScreenShare}>Share screen</button>
|
|
55
|
+
<button onClick={stopScreenShare}>Stop sharing</button>
|
|
56
|
+
<button onClick={startRecording}>Record</button>
|
|
57
|
+
<button onClick={stopRecording}>Stop recording</button>
|
|
58
|
+
<button onClick={leave}>Leave</button>
|
|
59
|
+
|
|
60
|
+
{participants.map((participant) => (
|
|
61
|
+
<div key={participant.id}>{participant.name}</div>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function MeetingPage({ roomCode }: { roomCode: string }) {
|
|
68
|
+
return (
|
|
69
|
+
<MeetingProvider roomCode={roomCode} displayName="Afolabi">
|
|
70
|
+
<MeetingRoom />
|
|
71
|
+
</MeetingProvider>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Call `join()` from a user action so the browser can request camera/microphone permission normally. The SDK then obtains the SFU token, opens `/ws`, sends JOIN, creates publisher/subscriber PeerConnections, publishes local media, and subscribes to remote tracks. fileciteturn9file0L87-L113
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
# 1. Architecture
|
|
81
|
+
|
|
82
|
+
The SDK uses one publisher PeerConnection and one subscriber PeerConnection per participant:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
Browser
|
|
86
|
+
├── Publisher RTCPeerConnection ── publish_offer ──> Rust SFU
|
|
87
|
+
│
|
|
88
|
+
└── Subscriber RTCPeerConnection <─ subscribe_offer ── Rust SFU
|
|
89
|
+
│
|
|
90
|
+
MediaRouter
|
|
91
|
+
│
|
|
92
|
+
other participants
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The SFU-generated track ID is the authoritative bridge between signaling metadata and browser `MediaStreamTrack` objects. fileciteturn9file0L5-L20
|
|
96
|
+
|
|
97
|
+
Think of the system as:
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
Your React application
|
|
101
|
+
│
|
|
102
|
+
▼
|
|
103
|
+
@afosecure/meetingsdk
|
|
104
|
+
│
|
|
105
|
+
├── WebSocket signaling
|
|
106
|
+
└── WebRTC media
|
|
107
|
+
│
|
|
108
|
+
▼
|
|
109
|
+
DEFCOMM Rust SFU
|
|
110
|
+
│
|
|
111
|
+
┌───────┴────────┐
|
|
112
|
+
│ │
|
|
113
|
+
RTP/RTCP Recording
|
|
114
|
+
│
|
|
115
|
+
MediaRouter
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The application owns the UI. The SDK owns browser WebRTC/signaling orchestration. The SFU owns authoritative room and media state.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
# 2. Installation
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm install @afosecure/meetingsdk
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The package is intended for React + TypeScript applications.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
# 3. Configuration
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { updateSDKConfig } from "@afosecure/meetingsdk";
|
|
136
|
+
|
|
137
|
+
updateSDKConfig({
|
|
138
|
+
apiBase: "https://your-sfu.example.com/api",
|
|
139
|
+
wsBase: "wss://your-sfu.example.com/ws",
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Use:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
apiBase → REST API base
|
|
147
|
+
wsBase → WebSocket signaling endpoint
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
For local development:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
updateSDKConfig({
|
|
154
|
+
apiBase: "http://localhost:8080/api",
|
|
155
|
+
wsBase: "ws://localhost:8080/ws",
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
For production:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
updateSDKConfig({
|
|
163
|
+
apiBase: "https://sfu.example.com/api",
|
|
164
|
+
wsBase: "wss://sfu.example.com/ws",
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Do **not** use the old mesh `/watch/:room` endpoint. The current SFU uses `/ws` plus REST room/token APIs. fileciteturn9file0L76-L85
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
# 4. Meeting lifecycle
|
|
173
|
+
|
|
174
|
+
A normal meeting follows:
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
Configure SDK
|
|
178
|
+
↓
|
|
179
|
+
MeetingProvider
|
|
180
|
+
↓
|
|
181
|
+
User clicks Join
|
|
182
|
+
↓
|
|
183
|
+
Get SFU access token
|
|
184
|
+
↓
|
|
185
|
+
Open WebSocket
|
|
186
|
+
↓
|
|
187
|
+
JOIN
|
|
188
|
+
↓
|
|
189
|
+
Create publisher/subscriber PCs
|
|
190
|
+
↓
|
|
191
|
+
Publish microphone + camera
|
|
192
|
+
↓
|
|
193
|
+
Subscribe to remote tracks
|
|
194
|
+
↓
|
|
195
|
+
Attach media to UI
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The current protocol includes joining/leaving, SDP negotiation, subscriptions, ICE, track state, screen sharing, recording, admission control, and related events. fileciteturn9file0L22-L64
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
# 5. Stable participant identity
|
|
203
|
+
|
|
204
|
+
The SDK creates a browser participant ID only when one does not already exist.
|
|
205
|
+
|
|
206
|
+
Storage key:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
defcomm:participant_id
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Therefore:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
First load → generate ID
|
|
216
|
+
Reload → reuse ID
|
|
217
|
+
Temporary outage → reuse ID
|
|
218
|
+
Reconnect → reuse ID
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The room-specific resume token is stored as:
|
|
222
|
+
|
|
223
|
+
```text
|
|
224
|
+
defcomm:sfu:resume:<roomCode>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
A transient network failure does not clear the resume token. An intentional `leave()` does. fileciteturn9file0L116-L132
|
|
228
|
+
|
|
229
|
+
This distinction is critical:
|
|
230
|
+
|
|
231
|
+
```text
|
|
232
|
+
Network loss
|
|
233
|
+
→ reconnect/resume
|
|
234
|
+
|
|
235
|
+
User clicked Leave
|
|
236
|
+
→ actual departure
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
# 6. Joining a meeting
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
const {
|
|
245
|
+
join,
|
|
246
|
+
leave,
|
|
247
|
+
} = useMeeting();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Join from a user interaction:
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
<button onClick={join}>Join meeting</button>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Leave intentionally:
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
<button onClick={leave}>Leave</button>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Do not close the WebSocket manually to simulate leaving. Use the SDK's `leave()` method so the SFU receives the proper leave operation and local state is cleaned up.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
# 7. Participant model
|
|
267
|
+
|
|
268
|
+
Conceptually:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
type Participant = {
|
|
272
|
+
id: string;
|
|
273
|
+
name?: string;
|
|
274
|
+
|
|
275
|
+
connectionState?:
|
|
276
|
+
| "connected"
|
|
277
|
+
| "reconnecting"
|
|
278
|
+
| "disconnected";
|
|
279
|
+
|
|
280
|
+
media?: {
|
|
281
|
+
stream?: MediaStream | null;
|
|
282
|
+
screenStream?: MediaStream | null;
|
|
283
|
+
|
|
284
|
+
cameraTrack?: MediaStreamTrack;
|
|
285
|
+
screenTrack?: MediaStreamTrack;
|
|
286
|
+
audioTrack?: MediaStreamTrack;
|
|
287
|
+
|
|
288
|
+
micEnabled: boolean;
|
|
289
|
+
camEnabled: boolean;
|
|
290
|
+
isScreenSharing: boolean;
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
The SDK exposes these connection and media states for meeting UI. fileciteturn9file0L367-L385
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
# 8. Remote audio/video
|
|
300
|
+
|
|
301
|
+
Use `useRemoteMedia(participantId)`:
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
import { useRemoteMedia } from "@afosecure/meetingsdk";
|
|
305
|
+
|
|
306
|
+
function RemoteMedia({ participantId }: { participantId: string }) {
|
|
307
|
+
const {
|
|
308
|
+
videoRef,
|
|
309
|
+
audioRef,
|
|
310
|
+
isCamActive,
|
|
311
|
+
isMicEnabled,
|
|
312
|
+
} = useRemoteMedia(participantId);
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<>
|
|
316
|
+
<video
|
|
317
|
+
ref={videoRef}
|
|
318
|
+
autoPlay
|
|
319
|
+
playsInline
|
|
320
|
+
muted
|
|
321
|
+
/>
|
|
322
|
+
|
|
323
|
+
<audio
|
|
324
|
+
ref={audioRef}
|
|
325
|
+
autoPlay
|
|
326
|
+
/>
|
|
327
|
+
</>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The video element is muted because remote audio is attached separately through the audio element. fileciteturn9file0L161-L178
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
# 9. Track mapping
|
|
337
|
+
|
|
338
|
+
The SDK maps:
|
|
339
|
+
|
|
340
|
+
```text
|
|
341
|
+
SFU TrackInfo.track_id
|
|
342
|
+
↓
|
|
343
|
+
browser track.id ("track-" + id)
|
|
344
|
+
↓
|
|
345
|
+
normalize ID
|
|
346
|
+
↓
|
|
347
|
+
TrackInfo.participant_id
|
|
348
|
+
↓
|
|
349
|
+
Participant
|
|
350
|
+
↓
|
|
351
|
+
MediaStream
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Do not rely on:
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
track.participant_id
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Browsers do not provide `participant_id` as a standard `MediaStreamTrack` property. fileciteturn9file0L144-L159
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
# 10. Microphone and camera
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
const {
|
|
368
|
+
toggleMic,
|
|
369
|
+
toggleCam,
|
|
370
|
+
} = useMeeting();
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Use:
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
<button onClick={toggleMic}>Mic</button>
|
|
377
|
+
<button onClick={toggleCam}>Camera</button>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The state flow is:
|
|
381
|
+
|
|
382
|
+
```text
|
|
383
|
+
toggleMic()
|
|
384
|
+
↓
|
|
385
|
+
MediaStreamTrack.enabled
|
|
386
|
+
↓
|
|
387
|
+
track_state
|
|
388
|
+
↓
|
|
389
|
+
Rust SFU
|
|
390
|
+
↓
|
|
391
|
+
track_state_changed
|
|
392
|
+
↓
|
|
393
|
+
remote UI
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
The same mechanism is used for camera state. fileciteturn9file0L180-L204
|
|
397
|
+
|
|
398
|
+
### Important
|
|
399
|
+
|
|
400
|
+
Remote mute/camera icons should be **signaling authoritative**.
|
|
401
|
+
|
|
402
|
+
Do not infer a remote participant's state only from:
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
remoteTrack.muted
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
or missing RTP.
|
|
409
|
+
|
|
410
|
+
Use:
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
participant.media?.micEnabled
|
|
414
|
+
participant.media?.camEnabled
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The SFU is authoritative for these states. fileciteturn9file0L202-L204
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
# 11. Professional participant tile
|
|
422
|
+
|
|
423
|
+
A participant tile should combine media and connection state:
|
|
424
|
+
|
|
425
|
+
```tsx
|
|
426
|
+
function ParticipantTile({ participant }: { participant: Participant }) {
|
|
427
|
+
const reconnecting =
|
|
428
|
+
participant.connectionState === "reconnecting";
|
|
429
|
+
|
|
430
|
+
const micEnabled =
|
|
431
|
+
participant.media?.micEnabled ?? false;
|
|
432
|
+
|
|
433
|
+
const camEnabled =
|
|
434
|
+
participant.media?.camEnabled ?? false;
|
|
435
|
+
|
|
436
|
+
const sharing =
|
|
437
|
+
participant.media?.isScreenSharing ?? false;
|
|
438
|
+
|
|
439
|
+
return (
|
|
440
|
+
<div className="participant-tile">
|
|
441
|
+
{reconnecting && (
|
|
442
|
+
<span className="reconnecting">
|
|
443
|
+
Reconnecting...
|
|
444
|
+
</span>
|
|
445
|
+
)}
|
|
446
|
+
|
|
447
|
+
<video autoPlay playsInline muted />
|
|
448
|
+
|
|
449
|
+
<div>{participant.name}</div>
|
|
450
|
+
|
|
451
|
+
<MicIcon muted={!micEnabled} />
|
|
452
|
+
<CameraIcon off={!camEnabled} />
|
|
453
|
+
|
|
454
|
+
{sharing && <ScreenShareIcon />}
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Recommended mapping:
|
|
461
|
+
|
|
462
|
+
```text
|
|
463
|
+
connectionState
|
|
464
|
+
↓
|
|
465
|
+
reconnecting badge
|
|
466
|
+
|
|
467
|
+
micEnabled
|
|
468
|
+
↓
|
|
469
|
+
microphone icon
|
|
470
|
+
|
|
471
|
+
camEnabled
|
|
472
|
+
↓
|
|
473
|
+
camera icon
|
|
474
|
+
|
|
475
|
+
isScreenSharing
|
|
476
|
+
↓
|
|
477
|
+
screen-share layout
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
fileciteturn9file0L387-L405
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
# 12. Screen sharing
|
|
485
|
+
|
|
486
|
+
Screen sharing is a separate publication with:
|
|
487
|
+
|
|
488
|
+
```text
|
|
489
|
+
source = "screen"
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
It is not the participant's camera. fileciteturn9file0L206-L233
|
|
493
|
+
|
|
494
|
+
Start:
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
await startScreenShare();
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Stop:
|
|
501
|
+
|
|
502
|
+
```tsx
|
|
503
|
+
await stopScreenShare();
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Remote state:
|
|
507
|
+
|
|
508
|
+
```ts
|
|
509
|
+
participant.media.screenStream
|
|
510
|
+
participant.media.screenTrack
|
|
511
|
+
participant.media.isScreenSharing
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
fileciteturn9file0L235-L244
|
|
515
|
+
|
|
516
|
+
Recommended layout:
|
|
517
|
+
|
|
518
|
+
```text
|
|
519
|
+
┌───────────────────────────────────────────────┐
|
|
520
|
+
│ │
|
|
521
|
+
│ SCREEN SHARE │
|
|
522
|
+
│ │
|
|
523
|
+
│ │
|
|
524
|
+
└───────────────────────────────────────────────┘
|
|
525
|
+
|
|
526
|
+
┌────────┐ ┌────────┐ ┌────────┐
|
|
527
|
+
│ Afolabi│ │ Mega │ │ John │
|
|
528
|
+
└────────┘ └────────┘ └────────┘
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
The screen should be the primary stage. Participant cameras should become a secondary strip.
|
|
532
|
+
|
|
533
|
+
Do not attach the screen track to the participant camera element.
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
# 13. Reconnection and resume
|
|
538
|
+
|
|
539
|
+
The SDK is designed to resume the same logical participant after a temporary network/signaling failure:
|
|
540
|
+
|
|
541
|
+
```text
|
|
542
|
+
Network interruption
|
|
543
|
+
↓
|
|
544
|
+
WebSocket closes
|
|
545
|
+
↓
|
|
546
|
+
participant = reconnecting
|
|
547
|
+
↓
|
|
548
|
+
refresh access token
|
|
549
|
+
↓
|
|
550
|
+
JOIN + resume_token
|
|
551
|
+
↓
|
|
552
|
+
same logical participant
|
|
553
|
+
↓
|
|
554
|
+
new publisher/subscriber PCs
|
|
555
|
+
↓
|
|
556
|
+
republish local tracks
|
|
557
|
+
↓
|
|
558
|
+
resubscribe remote tracks
|
|
559
|
+
↓
|
|
560
|
+
fresh ICE/SDP
|
|
561
|
+
↓
|
|
562
|
+
connected
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
The SDK intentionally does not immediately destroy the local MediaStream when signaling temporarily disappears. It uses reconnect delays and refreshes the access token before reconnecting. fileciteturn9file0L246-L275
|
|
566
|
+
|
|
567
|
+
### UI rule
|
|
568
|
+
|
|
569
|
+
During a transient outage:
|
|
570
|
+
|
|
571
|
+
```text
|
|
572
|
+
Reconnecting...
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
should be shown rather than immediately removing the participant.
|
|
576
|
+
|
|
577
|
+
Only treat the participant as gone after the SFU reports an actual departure/disconnection.
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
# 14. Leave correctly
|
|
582
|
+
|
|
583
|
+
Intentional departure:
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
await leave();
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Do not manually close:
|
|
590
|
+
|
|
591
|
+
```ts
|
|
592
|
+
websocket.close();
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
to implement the Leave button.
|
|
596
|
+
|
|
597
|
+
The intended lifecycle is:
|
|
598
|
+
|
|
599
|
+
```text
|
|
600
|
+
Leave button
|
|
601
|
+
↓
|
|
602
|
+
leave
|
|
603
|
+
↓
|
|
604
|
+
SFU acknowledgement
|
|
605
|
+
↓
|
|
606
|
+
destroy PeerConnections
|
|
607
|
+
↓
|
|
608
|
+
clear room resume token
|
|
609
|
+
↓
|
|
610
|
+
remove local meeting state
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
A temporary network failure is different and should use reconnect/resume.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
# 15. Private-room admission
|
|
618
|
+
|
|
619
|
+
For private rooms:
|
|
620
|
+
|
|
621
|
+
```tsx
|
|
622
|
+
const {
|
|
623
|
+
approveJoinRequest,
|
|
624
|
+
rejectJoinRequest,
|
|
625
|
+
} = useMeeting();
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Approve:
|
|
629
|
+
|
|
630
|
+
```tsx
|
|
631
|
+
await approveJoinRequest(participantId);
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Reject:
|
|
635
|
+
|
|
636
|
+
```tsx
|
|
637
|
+
await rejectJoinRequest(participantId);
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
The SFU remains authoritative. The SDK does not fabricate approval state locally. fileciteturn9file0L278-L289
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
# 16. Server-side recording
|
|
645
|
+
|
|
646
|
+
Recording is **server-side**. The browser does not use `MediaRecorder`. fileciteturn9file0L291-L340
|
|
647
|
+
|
|
648
|
+
Start:
|
|
649
|
+
|
|
650
|
+
```tsx
|
|
651
|
+
await startRecording();
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Stop:
|
|
655
|
+
|
|
656
|
+
```tsx
|
|
657
|
+
await stopRecording();
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Status:
|
|
661
|
+
|
|
662
|
+
```tsx
|
|
663
|
+
const { recording } = await getRecordingStatus();
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Lifecycle:
|
|
667
|
+
|
|
668
|
+
```text
|
|
669
|
+
startRecording()
|
|
670
|
+
↓
|
|
671
|
+
start_recording
|
|
672
|
+
↓
|
|
673
|
+
Rust SFU RecordingManager
|
|
674
|
+
↓
|
|
675
|
+
recording_started
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
Stop:
|
|
679
|
+
|
|
680
|
+
```text
|
|
681
|
+
stopRecording()
|
|
682
|
+
↓
|
|
683
|
+
stop_recording
|
|
684
|
+
↓
|
|
685
|
+
RecordingManager finalizes
|
|
686
|
+
↓
|
|
687
|
+
recording_stopped
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
The recording belongs to the meeting/server rather than a particular browser. Recording permissions are enforced by the SFU. fileciteturn9file0L291-L340
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
# 17. Room management
|
|
695
|
+
|
|
696
|
+
Create:
|
|
697
|
+
|
|
698
|
+
```tsx
|
|
699
|
+
const room = await createRoom({
|
|
700
|
+
name: "Engineering",
|
|
701
|
+
capacity: 100,
|
|
702
|
+
is_private: true,
|
|
703
|
+
});
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
Get:
|
|
707
|
+
|
|
708
|
+
```tsx
|
|
709
|
+
const room = await getRoom(roomCode);
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Delete:
|
|
713
|
+
|
|
714
|
+
```tsx
|
|
715
|
+
await deleteRoom(roomCode);
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
These use the REST API. fileciteturn9file0L342-L355
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
# 18. Preview
|
|
723
|
+
|
|
724
|
+
The old mesh SDK used:
|
|
725
|
+
|
|
726
|
+
```text
|
|
727
|
+
/watch/:room
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
That endpoint is not part of the current Rust SFU.
|
|
731
|
+
|
|
732
|
+
`useMeetingPreview()` therefore uses the current REST room endpoint and does not fabricate live participant/media state. Live participant/media state begins after joining through `/ws`. fileciteturn9file0L358-L365
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
# 19. Current signaling protocol
|
|
737
|
+
|
|
738
|
+
### Client → SFU
|
|
739
|
+
|
|
740
|
+
```text
|
|
741
|
+
join
|
|
742
|
+
leave
|
|
743
|
+
publish_offer
|
|
744
|
+
subscribe
|
|
745
|
+
unsubscribe
|
|
746
|
+
subscribe_answer
|
|
747
|
+
ice_candidate
|
|
748
|
+
track_state
|
|
749
|
+
start_screen_share
|
|
750
|
+
stop_screen_share
|
|
751
|
+
start_recording
|
|
752
|
+
stop_recording
|
|
753
|
+
approve_join
|
|
754
|
+
reject_join
|
|
755
|
+
ping
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### SFU → Client
|
|
759
|
+
|
|
760
|
+
```text
|
|
761
|
+
joined
|
|
762
|
+
join_pending
|
|
763
|
+
join_requested
|
|
764
|
+
join_approved
|
|
765
|
+
join_rejected
|
|
766
|
+
publish_answer
|
|
767
|
+
subscribe_offer
|
|
768
|
+
ice_candidate
|
|
769
|
+
track_published
|
|
770
|
+
track_unpublished
|
|
771
|
+
track_state_changed
|
|
772
|
+
participant_joined
|
|
773
|
+
participant_left
|
|
774
|
+
participant_presence
|
|
775
|
+
screen_share_started
|
|
776
|
+
screen_share_stopped
|
|
777
|
+
subscription_updated
|
|
778
|
+
recording_started
|
|
779
|
+
recording_stopped
|
|
780
|
+
pong
|
|
781
|
+
left
|
|
782
|
+
error
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
fileciteturn9file0L22-L64
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
# 20. Recommended React structure
|
|
790
|
+
|
|
791
|
+
```text
|
|
792
|
+
MeetingPage
|
|
793
|
+
│
|
|
794
|
+
└── MeetingProvider
|
|
795
|
+
│
|
|
796
|
+
├── MeetingHeader
|
|
797
|
+
│ ├── Room name
|
|
798
|
+
│ ├── Connection state
|
|
799
|
+
│ └── Recording indicator
|
|
800
|
+
│
|
|
801
|
+
├── MeetingStage
|
|
802
|
+
│ ├── ScreenShareStage
|
|
803
|
+
│ └── ActiveSpeakerStage
|
|
804
|
+
│
|
|
805
|
+
├── ParticipantStrip
|
|
806
|
+
│ └── ParticipantTile[]
|
|
807
|
+
│ ├── Video
|
|
808
|
+
│ ├── Name
|
|
809
|
+
│ ├── Mic icon
|
|
810
|
+
│ ├── Camera icon
|
|
811
|
+
│ └── Reconnecting badge
|
|
812
|
+
│
|
|
813
|
+
└── MeetingControls
|
|
814
|
+
├── Mic
|
|
815
|
+
├── Camera
|
|
816
|
+
├── Screen share
|
|
817
|
+
├── Record
|
|
818
|
+
└── Leave
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
The SDK supplies media and meeting state. Your application owns the visual design.
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
# 21. Error handling
|
|
826
|
+
|
|
827
|
+
Catch operations that can fail:
|
|
828
|
+
|
|
829
|
+
```tsx
|
|
830
|
+
try {
|
|
831
|
+
await join();
|
|
832
|
+
} catch (error) {
|
|
833
|
+
console.error("Unable to join meeting", error);
|
|
834
|
+
}
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
Your UI should distinguish:
|
|
838
|
+
|
|
839
|
+
```text
|
|
840
|
+
Connecting
|
|
841
|
+
Reconnecting
|
|
842
|
+
Connected
|
|
843
|
+
Disconnected
|
|
844
|
+
Join rejected
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Do not show "left the meeting" for a temporary ICE/WebSocket interruption.
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
# 22. Do not reintroduce mesh logic
|
|
852
|
+
|
|
853
|
+
Never create a PeerConnection per remote participant.
|
|
854
|
+
|
|
855
|
+
Wrong:
|
|
856
|
+
|
|
857
|
+
```text
|
|
858
|
+
A ↔ B
|
|
859
|
+
A ↔ C
|
|
860
|
+
B ↔ C
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
Correct:
|
|
864
|
+
|
|
865
|
+
```text
|
|
866
|
+
A ──┐
|
|
867
|
+
B ──┼──► SFU ──► participants
|
|
868
|
+
C ──┘
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
The SFU owns:
|
|
872
|
+
|
|
873
|
+
- participant identity
|
|
874
|
+
- room membership
|
|
875
|
+
- admission
|
|
876
|
+
- SDP negotiation
|
|
877
|
+
- ICE
|
|
878
|
+
- track publication
|
|
879
|
+
- subscription
|
|
880
|
+
- RTP forwarding
|
|
881
|
+
- RTCP feedback
|
|
882
|
+
- mute state
|
|
883
|
+
- screen-share state
|
|
884
|
+
- server-side recording
|
|
885
|
+
|
|
886
|
+
The browser SDK owns:
|
|
887
|
+
|
|
888
|
+
- browser media permissions
|
|
889
|
+
- publisher/subscriber PeerConnections
|
|
890
|
+
- signaling transport
|
|
891
|
+
- media attachment
|
|
892
|
+
- local UI state
|
|
893
|
+
- reconnect/resume orchestration
|
|
894
|
+
- React bindings
|
|
895
|
+
|
|
896
|
+
fileciteturn9file0L423-L450
|
|
897
|
+
|
|
898
|
+
---
|
|
899
|
+
|
|
900
|
+
# 23. Build
|
|
901
|
+
|
|
902
|
+
```bash
|
|
903
|
+
npm install
|
|
904
|
+
npm run build
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
Expected output:
|
|
908
|
+
|
|
909
|
+
```text
|
|
910
|
+
dist/
|
|
911
|
+
├── index.js
|
|
912
|
+
├── index.mjs
|
|
913
|
+
├── index.d.ts
|
|
914
|
+
└── index.d.mts
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
The SDK consumes the SFU media path:
|
|
918
|
+
|
|
919
|
+
```text
|
|
920
|
+
Publisher
|
|
921
|
+
↓
|
|
922
|
+
SFU Publisher PC
|
|
923
|
+
↓
|
|
924
|
+
PublishedTrackRegistry
|
|
925
|
+
↓
|
|
926
|
+
MediaRouter
|
|
927
|
+
↓
|
|
928
|
+
TrackLocalStaticRTP
|
|
929
|
+
↓
|
|
930
|
+
Subscriber PC
|
|
931
|
+
↓
|
|
932
|
+
Browser ontrack
|
|
933
|
+
↓
|
|
934
|
+
SDK TrackInfo mapping
|
|
935
|
+
↓
|
|
936
|
+
Participant MediaStream
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
fileciteturn9file0L452-L474
|
|
940
|
+
|
|
941
|
+
---
|
|
942
|
+
|
|
943
|
+
# 24. Production checklist
|
|
944
|
+
|
|
945
|
+
Before shipping:
|
|
946
|
+
|
|
947
|
+
- [ ] Configure `apiBase` and `wsBase`.
|
|
948
|
+
- [ ] Use HTTPS/WSS in production.
|
|
949
|
+
- [ ] Render hooks inside `MeetingProvider`.
|
|
950
|
+
- [ ] Call `join()` from a user action.
|
|
951
|
+
- [ ] Handle camera/microphone permissions.
|
|
952
|
+
- [ ] Render remote video and audio separately.
|
|
953
|
+
- [ ] Keep video elements muted when remote audio is attached separately.
|
|
954
|
+
- [ ] Use SFU signaling for remote mic/camera icons.
|
|
955
|
+
- [ ] Render screen sharing separately from camera.
|
|
956
|
+
- [ ] Show `Reconnecting...` during transient network loss.
|
|
957
|
+
- [ ] Do not remove a participant immediately on temporary connection loss.
|
|
958
|
+
- [ ] Use `leave()` for intentional departure.
|
|
959
|
+
- [ ] Do not create mesh PeerConnections.
|
|
960
|
+
- [ ] Restrict recording controls to authorized users.
|
|
961
|
+
- [ ] Treat recording as server-side.
|
|
962
|
+
- [ ] Test reload, reconnect, leave, and rejoin.
|
|
963
|
+
- [ ] Run `npm run build` before publishing.
|
|
964
|
+
- [ ] Keep the SDK and SFU signaling protocol versions compatible.
|
|
965
|
+
|
|
966
|
+
---
|
|
967
|
+
|
|
968
|
+
# 25. Developer mental model
|
|
969
|
+
|
|
970
|
+
The simplest mental model is:
|
|
971
|
+
|
|
972
|
+
```text
|
|
973
|
+
YOUR APPLICATION
|
|
974
|
+
│
|
|
975
|
+
│ UI + meeting controls
|
|
976
|
+
▼
|
|
977
|
+
@afosecure/meetingsdk
|
|
978
|
+
│
|
|
979
|
+
├── React hooks
|
|
980
|
+
├── WebSocket signaling
|
|
981
|
+
├── Publisher PC
|
|
982
|
+
├── Subscriber PC
|
|
983
|
+
├── Media attachment
|
|
984
|
+
└── Reconnect/resume
|
|
985
|
+
│
|
|
986
|
+
▼
|
|
987
|
+
DEFCOMM RUST SFU
|
|
988
|
+
│
|
|
989
|
+
├── Rooms
|
|
990
|
+
├── Participants
|
|
991
|
+
├── Admission
|
|
992
|
+
├── SDP / ICE
|
|
993
|
+
├── Track registry
|
|
994
|
+
├── MediaRouter
|
|
995
|
+
├── RTCP feedback
|
|
996
|
+
├── Screen-share state
|
|
997
|
+
└── Recording
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
**Key rule:** the application owns presentation, the SDK owns browser WebRTC, and the SFU owns authoritative meeting/media state.
|