@defcomm/meetingsdk 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -1,281 +1,355 @@
1
- # @afosecure/meetingsdk
1
+ # DEFCOMM Meet SDK
2
2
 
3
- React/TypeScript SDK for the **DEFCOMM Rust SFU**.
3
+ `@afosecure/meetingsdk` is the React/TypeScript client SDK used by **DEFCOMM Meet**.
4
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.
5
+ It provides the application layer for:
6
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();
7
+ - joining and leaving meetings
8
+ - microphone and camera control
9
+ - remote participant media
10
+ - screen sharing
11
+ - active-speaker detection
12
+ - participant presence and reconnection state
13
+ - private-room admission
14
+ - server-side recording
15
+ - room management
16
+ - chat/PubSub integration
17
+ - WebRTC and SFU signaling orchestration
46
18
 
47
- const participants = useParticipants();
19
+ The SDK is designed so that a DEFCOMM Meet consumer should **not need to manage WebRTC peer connections, SDP, ICE candidates, RTP tracks, reconnect logic, or browser media plumbing directly**.
48
20
 
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>
21
+ ---
59
22
 
60
- {participants.map((participant) => (
61
- <div key={participant.id}>{participant.name}</div>
62
- ))}
63
- </div>
64
- );
65
- }
23
+ ## Table of contents
24
+
25
+ 1. [What the SDK does](#what-the-sdk-does)
26
+ 2. [Architecture](#architecture)
27
+ 3. [Installation](#installation)
28
+ 4. [SDK configuration](#sdk-configuration)
29
+ 5. [Creating a meeting](#creating-a-meeting)
30
+ 6. [Joining a meeting](#joining-a-meeting)
31
+ 7. [MeetingProvider](#meetingprovider)
32
+ 8. [The `useMeeting` hook](#the-usemeeting-hook)
33
+ 9. [Participants](#participants)
34
+ 10. [Local participant](#local-participant)
35
+ 11. [Remote media](#remote-media)
36
+ 12. [Microphone and camera](#microphone-and-camera)
37
+ 13. [Active speaker detection](#active-speaker-detection)
38
+ 14. [Screen sharing](#screen-sharing)
39
+ 15. [Recording](#recording)
40
+ 16. [Private rooms and join approval](#private-rooms-and-join-approval)
41
+ 17. [Reconnection and participant presence](#reconnection-and-participant-presence)
42
+ 18. [Leaving a meeting](#leaving-a-meeting)
43
+ 19. [Room management](#room-management)
44
+ 20. [Meeting preview](#meeting-preview)
45
+ 21. [Chat](#chat)
46
+ 22. [Error handling](#error-handling)
47
+ 23. [Recommended DEFCOMM Meet structure](#recommended-defcomm-meet-structure)
48
+ 24. [Important implementation rules](#important-implementation-rules)
49
+ 25. [Build and publish](#build-and-publish)
66
50
 
67
- export function MeetingPage({ roomCode }: { roomCode: string }) {
68
- return (
69
- <MeetingProvider roomCode={roomCode} displayName="Afolabi">
70
- <MeetingRoom />
71
- </MeetingProvider>
72
- );
73
- }
74
- ```
51
+ ---
75
52
 
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. fileciteturn9file0L87-L113
53
+ # What the SDK does
77
54
 
78
- ---
55
+ DEFCOMM Meet is built around an **SFU architecture**.
79
56
 
80
- # 1. Architecture
57
+ The browser does not establish a separate peer connection with every participant.
81
58
 
82
- The SDK uses one publisher PeerConnection and one subscriber PeerConnection per participant:
59
+ Instead:
83
60
 
84
61
  ```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
62
+ Participant A ──┐
63
+ Participant B ──┼──► DEFCOMM Rust SFU ──► Participants
64
+ Participant C ──┘
93
65
  ```
94
66
 
95
- The SFU-generated track ID is the authoritative bridge between signaling metadata and browser `MediaStreamTrack` objects. fileciteturn9file0L5-L20
96
-
97
- Think of the system as:
67
+ The SDK sits between the React application and the SFU:
98
68
 
99
69
  ```text
100
- Your React application
101
- │
102
- ▼
103
- @afosecure/meetingsdk
104
- │
105
- ├── WebSocket signaling
106
- └── WebRTC media
70
+ ┌───────────────────────────────┐
71
+ │ DEFCOMM Meet App │
72
+ │ │
73
+ │ Meeting UI / Participant UI │
74
+ └───────────────┬───────────────┘
107
75
  │
108
76
  ▼
109
- DEFCOMM Rust SFU
77
+ ┌───────────────────────────────┐
78
+ │ @afosecure/meetingsdk │
79
+ │ │
80
+ │ React hooks │
81
+ │ Meeting state │
82
+ │ WebRTC │
83
+ │ WebSocket signaling │
84
+ │ Reconnection │
85
+ │ Active speaker detection │
86
+ │ Media attachment │
87
+ │ Recording controls │
88
+ └───────────────┬───────────────┘
110
89
  │
111
- ┌───────┴────────┐
112
- │ │
113
- RTP/RTCP Recording
114
- │
115
- MediaRouter
90
+ ▼
91
+ ┌───────────────────────────────┐
92
+ │ DEFCOMM Rust SFU │
93
+ │ │
94
+ │ Rooms │
95
+ │ Participants │
96
+ │ Admission │
97
+ │ SDP / ICE │
98
+ │ RTP forwarding │
99
+ │ Track state │
100
+ │ Recording │
101
+ └───────────────────────────────┘
116
102
  ```
117
103
 
118
- The application owns the UI. The SDK owns browser WebRTC/signaling orchestration. The SFU owns authoritative room and media state.
104
+ ### The responsibility split
105
+
106
+ **Your application owns:**
107
+
108
+ - layout
109
+ - buttons
110
+ - participant tiles
111
+ - navbar
112
+ - chat UI
113
+ - recording indicator
114
+ - active-speaker styling
115
+ - loading/error screens
116
+
117
+ **The SDK owns:**
118
+
119
+ - browser media access
120
+ - publisher PeerConnection
121
+ - subscriber PeerConnection
122
+ - WebSocket signaling
123
+ - media track mapping
124
+ - reconnect/resume behavior
125
+ - active-speaker detection
126
+ - SDK meeting state
127
+
128
+ **The SFU owns:**
129
+
130
+ - room state
131
+ - participant membership
132
+ - admission/approval
133
+ - signaling authority
134
+ - media forwarding
135
+ - track publication/subscription
136
+ - server-side recording
119
137
 
120
138
  ---
121
139
 
122
- # 2. Installation
140
+ # Installation
123
141
 
124
142
  ```bash
125
143
  npm install @afosecure/meetingsdk
126
144
  ```
127
145
 
128
- The package is intended for React + TypeScript applications.
146
+ The SDK is intended for browser-based React + TypeScript applications.
129
147
 
130
148
  ---
131
149
 
132
- # 3. Configuration
150
+ # Creating a meeting
133
151
 
134
- ```ts
135
- import { updateSDKConfig } from "@afosecure/meetingsdk";
152
+ The SDK exposes room management through `VideoSDKCore` and `useMeeting()`.
136
153
 
137
- updateSDKConfig({
138
- apiBase: "https://your-sfu.example.com/api",
139
- wsBase: "wss://your-sfu.example.com/ws",
154
+ ```tsx
155
+ const { createRoom } = useMeeting();
156
+
157
+ const room = await createRoom({
158
+ name: "Engineering Standup",
159
+ description: "Daily engineering meeting",
160
+ capacity: 50,
161
+ is_private: true,
140
162
  });
141
163
  ```
142
164
 
143
- Use:
144
-
145
- ```text
146
- apiBase → REST API base
147
- wsBase → WebSocket signaling endpoint
148
- ```
165
+ A room can also be retrieved:
149
166
 
150
- For local development:
167
+ ```tsx
168
+ const { getRoom } = useMeeting();
151
169
 
152
- ```ts
153
- updateSDKConfig({
154
- apiBase: "http://localhost:8080/api",
155
- wsBase: "ws://localhost:8080/ws",
156
- });
170
+ const room = await getRoom(roomCode);
157
171
  ```
158
172
 
159
- For production:
173
+ And deleted when appropriate:
160
174
 
161
- ```ts
162
- updateSDKConfig({
163
- apiBase: "https://sfu.example.com/api",
164
- wsBase: "wss://sfu.example.com/ws",
165
- });
175
+ ```tsx
176
+ const { deleteRoom } = useMeeting();
177
+
178
+ await deleteRoom(roomCode);
166
179
  ```
167
180
 
168
- Do **not** use the old mesh `/watch/:room` endpoint. The current SFU uses `/ws` plus REST room/token APIs. fileciteturn9file0L76-L85
181
+ Room creation and deletion should normally be restricted to the appropriate application role.
169
182
 
170
183
  ---
171
184
 
172
- # 4. Meeting lifecycle
185
+ # Joining a meeting
186
+
187
+ The normal React integration starts with `MeetingProvider`.
173
188
 
174
- A normal meeting follows:
189
+ ```tsx
190
+ import { MeetingProvider, useMeeting } from "@afosecure/meetingsdk";
191
+
192
+ function MeetingRoom() {
193
+ const { join, leave } = useMeeting();
194
+
195
+ return (
196
+ <div>
197
+ <button onClick={() => join()}>Join meeting</button>
198
+
199
+ <button onClick={() => leave()}>Leave</button>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ export function MeetingPage() {
205
+ return (
206
+ <MeetingProvider
207
+ config={{
208
+ roomId: "room-code",
209
+ name: "Afolabi",
210
+ audioMuted: false,
211
+ videoMuted: false,
212
+ }}
213
+ >
214
+ <MeetingRoom />
215
+ </MeetingProvider>
216
+ );
217
+ }
218
+ ```
219
+
220
+ `MeetingProvider` accepts:
221
+
222
+ ```ts
223
+ type MeetingConfig = {
224
+ roomId: string;
225
+ name: string;
226
+ audioMuted?: boolean;
227
+ videoMuted?: boolean;
228
+ token?: string;
229
+ };
230
+ ```
231
+
232
+ ### Recommended join flow
175
233
 
176
234
  ```text
177
- Configure SDK
235
+ Meeting page
178
236
  ↓
179
237
  MeetingProvider
180
238
  ↓
181
239
  User clicks Join
182
240
  ↓
183
- Get SFU access token
184
- ↓
185
- Open WebSocket
241
+ join()
186
242
  ↓
187
- JOIN
243
+ Browser requests camera/microphone
188
244
  ↓
189
- Create publisher/subscriber PCs
245
+ SDK connects to SFU
190
246
  ↓
191
- Publish microphone + camera
247
+ Publisher PeerConnection
192
248
  ↓
193
- Subscribe to remote tracks
249
+ Subscriber PeerConnection
194
250
  ↓
195
- Attach media to UI
251
+ Remote tracks become available
196
252
  ```
197
253
 
198
- The current protocol includes joining/leaving, SDP negotiation, subscriptions, ICE, track state, screen sharing, recording, admission control, and related events. fileciteturn9file0L22-L64
254
+ Call `join()` from a user action whenever possible. This gives the browser a normal user-initiated context for requesting media permissions.
199
255
 
200
256
  ---
201
257
 
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
- ```
258
+ # The `useMeeting` hook
220
259
 
221
- The room-specific resume token is stored as:
260
+ `useMeeting()` is the main application API.
222
261
 
223
- ```text
224
- defcomm:sfu:resume:<roomCode>
225
- ```
226
-
227
- A transient network failure does not clear the resume token. An intentional `leave()` does. fileciteturn9file0L116-L132
262
+ ```tsx
263
+ const {
264
+ join,
265
+ leave,
228
266
 
229
- This distinction is critical:
267
+ toggleMic,
268
+ toggleCam,
230
269
 
231
- ```text
232
- Network loss
233
- → reconnect/resume
270
+ startScreenShare,
271
+ stopScreenShare,
234
272
 
235
- User clicked Leave
236
- → actual departure
237
- ```
273
+ startRecording,
274
+ stopRecording,
275
+ getRecordingStatus,
238
276
 
239
- ---
277
+ participants,
278
+ localParticipant,
279
+ activeSpeakerIds,
280
+ presenterId,
281
+ room,
240
282
 
241
- # 6. Joining a meeting
283
+ approveJoinRequest,
284
+ rejectJoinRequest,
242
285
 
243
- ```tsx
244
- const {
245
- join,
246
- leave,
286
+ sendMessage,
247
287
  } = useMeeting();
248
288
  ```
249
289
 
250
- Join from a user interaction:
290
+ The hook also supports event handlers:
251
291
 
252
292
  ```tsx
253
- <button onClick={join}>Join meeting</button>
293
+ const meeting = useMeeting({
294
+ onError: (error) => {
295
+ console.error(error);
296
+ },
297
+
298
+ onActiveSpeakersChanged: (participantIds) => {
299
+ console.log("Active speakers:", participantIds);
300
+ },
301
+
302
+ onRecordingStarted: (recording) => {
303
+ console.log("Recording started:", recording);
304
+ },
305
+
306
+ onRecordingStopped: (recording) => {
307
+ console.log("Recording stopped:", recording);
308
+ },
309
+ });
254
310
  ```
255
311
 
256
- Leave intentionally:
312
+ For normal React rendering, prefer the state exposed by the hook:
257
313
 
258
314
  ```tsx
259
- <button onClick={leave}>Leave</button>
315
+ const { activeSpeakerIds } = useMeeting();
260
316
  ```
261
317
 
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.
318
+ Use event callbacks when the application needs to perform an additional side effect.
263
319
 
264
320
  ---
265
321
 
266
- # 7. Participant model
322
+ # Participants
267
323
 
268
- Conceptually:
324
+ Use `useParticipants()` when the component only needs the participant list.
325
+
326
+ ```tsx
327
+ import { useParticipants } from "@afosecure/meetingsdk";
328
+
329
+ function ParticipantList() {
330
+ const participants = useParticipants();
331
+
332
+ return (
333
+ <div>
334
+ {participants.map((participant) => (
335
+ <div key={participant.id}>{participant.name}</div>
336
+ ))}
337
+ </div>
338
+ );
339
+ }
340
+ ```
341
+
342
+ A participant contains:
269
343
 
270
344
  ```ts
271
345
  type Participant = {
272
346
  id: string;
273
347
  name?: string;
348
+ isHost?: boolean;
349
+ isLocal?: boolean;
350
+ isPresenter?: boolean;
274
351
 
275
- connectionState?:
276
- | "connected"
277
- | "reconnecting"
278
- | "disconnected";
352
+ connectionState?: "connected" | "reconnecting" | "disconnected";
279
353
 
280
354
  media?: {
281
355
  stream?: MediaStream | null;
@@ -288,713 +362,986 @@ type Participant = {
288
362
  micEnabled: boolean;
289
363
  camEnabled: boolean;
290
364
  isScreenSharing: boolean;
365
+
366
+ remoteScreenStreamId?: string;
367
+ cameraStreamId?: string;
291
368
  };
292
369
  };
293
370
  ```
294
371
 
295
- The SDK exposes these connection and media states for meeting UI. fileciteturn9file0L367-L385
296
-
297
372
  ---
298
373
 
299
- # 8. Remote audio/video
374
+ # Local participant
300
375
 
301
- Use `useRemoteMedia(participantId)`:
376
+ Use `useLocalParticipant()` when you need the local participant and local video.
302
377
 
303
378
  ```tsx
304
- import { useRemoteMedia } from "@afosecure/meetingsdk";
379
+ import { useLocalParticipant } from "@afosecure/meetingsdk";
305
380
 
306
- function RemoteMedia({ participantId }: { participantId: string }) {
307
- const {
308
- videoRef,
309
- audioRef,
310
- isCamActive,
311
- isMicEnabled,
312
- } = useRemoteMedia(participantId);
381
+ function LocalVideo() {
382
+ const { participant, videoRef } = useLocalParticipant();
313
383
 
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
- );
384
+ return <video ref={videoRef} autoPlay muted playsInline />;
329
385
  }
330
386
  ```
331
387
 
332
- The video element is muted because remote audio is attached separately through the audio element. fileciteturn9file0L161-L178
388
+ The local participant exposes:
389
+
390
+ ```tsx
391
+ participant?.media?.micEnabled;
392
+ participant?.media?.camEnabled;
393
+ participant?.media?.isScreenSharing;
394
+ ```
395
+
396
+ Use these values to render local microphone/camera state.
333
397
 
334
398
  ---
335
399
 
336
- # 9. Track mapping
400
+ # Remote media
337
401
 
338
- The SDK maps:
402
+ Use `useRemoteMedia(participantId)` for a remote participant.
339
403
 
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
- ```
404
+ ```tsx
405
+ import { useRemoteMedia } from "@afosecure/meetingsdk";
353
406
 
354
- Do not rely on:
407
+ function RemoteParticipant({ participantId }: { participantId: string }) {
408
+ const { videoRef, audioRef, isCamActive, isMicEnabled } =
409
+ useRemoteMedia(participantId);
355
410
 
356
- ```ts
357
- track.participant_id
411
+ return (
412
+ <div>
413
+ <video ref={videoRef} autoPlay playsInline muted />
414
+
415
+ <audio ref={audioRef} autoPlay />
416
+
417
+ {!isMicEnabled && <span>Muted</span>}
418
+ {!isCamActive && <span>Camera off</span>}
419
+ </div>
420
+ );
421
+ }
358
422
  ```
359
423
 
360
- Browsers do not provide `participant_id` as a standard `MediaStreamTrack` property. fileciteturn9file0L144-L159
424
+ ### Why are video and audio separate?
425
+
426
+ The SDK attaches the remote `MediaStream` to both elements:
427
+
428
+ - `<video>` is muted
429
+ - `<audio>` handles remote audio
430
+
431
+ This makes media rendering explicit and avoids accidentally routing remote audio through the video element.
361
432
 
362
433
  ---
363
434
 
364
- # 10. Microphone and camera
435
+ # Microphone and camera
436
+
437
+ The SDK exposes:
365
438
 
366
439
  ```tsx
367
- const {
368
- toggleMic,
369
- toggleCam,
370
- } = useMeeting();
440
+ const { toggleMic, toggleCam } = useMeeting();
371
441
  ```
372
442
 
373
443
  Use:
374
444
 
375
445
  ```tsx
376
- <button onClick={toggleMic}>Mic</button>
377
- <button onClick={toggleCam}>Camera</button>
378
- ```
446
+ <button onClick={toggleMic}>
447
+ Microphone
448
+ </button>
379
449
 
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
450
+ <button onClick={toggleCam}>
451
+ Camera
452
+ </button>
394
453
  ```
395
454
 
396
- The same mechanism is used for camera state. fileciteturn9file0L180-L204
455
+ The SDK updates the local media track and sends the corresponding media state to the SFU.
397
456
 
398
- ### Important
457
+ Remote participants receive the authoritative track state through the meeting state.
399
458
 
400
- Remote mute/camera icons should be **signaling authoritative**.
459
+ For participant UI, use:
401
460
 
402
- Do not infer a remote participant's state only from:
403
-
404
- ```ts
405
- remoteTrack.muted
461
+ ```tsx
462
+ participant.media?.micEnabled;
463
+ participant.media?.camEnabled;
406
464
  ```
407
465
 
408
- or missing RTP.
409
-
410
- Use:
466
+ rather than trying to infer another participant's mute state from RTP or `MediaStreamTrack.muted`.
411
467
 
412
- ```ts
413
- participant.media?.micEnabled
414
- participant.media?.camEnabled
415
- ```
468
+ ---
416
469
 
417
- The SFU is authoritative for these states. fileciteturn9file0L202-L204
470
+ # Active speaker detection
418
471
 
419
- ---
472
+ DEFCOMM Meet performs active-speaker detection **inside the SDK**.
420
473
 
421
- # 11. Professional participant tile
474
+ The application does not need to use Web Audio APIs, `AnalyserNode`, audio RMS calculations, timers, or WebRTC statistics.
422
475
 
423
- A participant tile should combine media and connection state:
476
+ The SDK exposes:
424
477
 
425
478
  ```tsx
426
- function ParticipantTile({ participant }: { participant: Participant }) {
427
- const reconnecting =
428
- participant.connectionState === "reconnecting";
479
+ const { activeSpeakerIds } = useMeeting();
480
+ ```
429
481
 
430
- const micEnabled =
431
- participant.media?.micEnabled ?? false;
482
+ `activeSpeakerIds` is:
432
483
 
433
- const camEnabled =
434
- participant.media?.camEnabled ?? false;
484
+ ```ts
485
+ string[]
486
+ ```
435
487
 
436
- const sharing =
437
- participant.media?.isScreenSharing ?? false;
488
+ containing participant IDs currently detected as speaking.
438
489
 
439
- return (
440
- <div className="participant-tile">
441
- {reconnecting && (
442
- <span className="reconnecting">
443
- Reconnecting...
444
- </span>
445
- )}
490
+ ### Participant tile
446
491
 
447
- <video autoPlay playsInline muted />
448
-
449
- <div>{participant.name}</div>
492
+ ```tsx
493
+ function ParticipantTile({ participant }: { participant: Participant }) {
494
+ const { activeSpeakerIds } = useMeeting();
450
495
 
451
- <MicIcon muted={!micEnabled} />
452
- <CameraIcon off={!camEnabled} />
496
+ const isActiveSpeaker = activeSpeakerIds.includes(participant.id);
453
497
 
454
- {sharing && <ScreenShareIcon />}
498
+ return (
499
+ <div className={isActiveSpeaker ? "ring-2 ring-green-400" : ""}>
500
+ {participant.name}
455
501
  </div>
456
502
  );
457
503
  }
458
504
  ```
459
505
 
460
- Recommended mapping:
506
+ ### Better pattern
461
507
 
462
- ```text
463
- connectionState
464
- ↓
465
- reconnecting badge
508
+ For a participant list:
466
509
 
467
- micEnabled
468
- ↓
469
- microphone icon
510
+ ```tsx
511
+ const { activeSpeakerIds } = useMeeting();
512
+
513
+ {
514
+ participants.map((participant) => (
515
+ <ParticipantTile
516
+ key={participant.id}
517
+ participant={participant}
518
+ isActiveSpeaker={activeSpeakerIds.includes(participant.id)}
519
+ />
520
+ ));
521
+ }
522
+ ```
470
523
 
471
- camEnabled
472
- ↓
473
- camera icon
524
+ The tile should only render the state. It should not implement speaker detection itself.
474
525
 
475
- isScreenSharing
476
- ↓
477
- screen-share layout
526
+ ### Active speaker event
527
+
528
+ The SDK also exposes:
529
+
530
+ ```tsx
531
+ useMeeting({
532
+ onActiveSpeakersChanged: (participantIds) => {
533
+ console.log("Active speakers:", participantIds);
534
+ },
535
+ });
478
536
  ```
479
537
 
480
- fileciteturn9file0L387-L405
538
+ The event is useful for analytics or application-level side effects. For normal UI rendering, `activeSpeakerIds` is preferred.
481
539
 
482
- ---
540
+ ### Multiple speakers
483
541
 
484
- # 12. Screen sharing
542
+ The API intentionally supports multiple active speakers:
543
+
544
+ ```ts
545
+ activeSpeakerIds: string[]
546
+ ```
485
547
 
486
- Screen sharing is a separate publication with:
548
+ rather than:
487
549
 
488
- ```text
489
- source = "screen"
550
+ ```ts
551
+ activeSpeakerId: string | null;
490
552
  ```
491
553
 
492
- It is not the participant's camera. fileciteturn9file0L206-L233
554
+ This allows the UI to handle overlapping speech naturally.
555
+
556
+ ---
557
+
558
+ # Screen sharing
493
559
 
494
560
  Start:
495
561
 
496
562
  ```tsx
563
+ const { startScreenShare } = useMeeting();
564
+
497
565
  await startScreenShare();
498
566
  ```
499
567
 
500
568
  Stop:
501
569
 
502
570
  ```tsx
503
- await stopScreenShare();
571
+ const { stopScreenShare } = useMeeting();
572
+
573
+ stopScreenShare();
504
574
  ```
505
575
 
506
- Remote state:
576
+ A participant's screen share is represented separately from their camera:
507
577
 
508
578
  ```ts
509
- participant.media.screenStream
510
- participant.media.screenTrack
511
- participant.media.isScreenSharing
579
+ participant.media?.screenStream;
580
+ participant.media?.screenTrack;
581
+ participant.media?.isScreenSharing;
512
582
  ```
513
583
 
514
- fileciteturn9file0L235-L244
584
+ ### Recommended UI
515
585
 
516
- Recommended layout:
586
+ When someone shares their screen:
517
587
 
518
588
  ```text
519
- ┌───────────────────────────────────────────────┐
520
- │ │
521
- │ SCREEN SHARE │
522
- │ │
523
- │ │
524
- └───────────────────────────────────────────────┘
589
+ ┌──────────────────────────────────────────┐
590
+ │ │
591
+ │ SCREEN SHARE │
592
+ │ │
593
+ │ │
594
+ └──────────────────────────────────────────┘
595
+
596
+ ┌─────────┐ ┌─────────┐ ┌─────────┐
597
+ │ Alice │ │ Bob │ │ Charlie │
598
+ └─────────┘ └─────────┘ └─────────┘
599
+ ```
600
+
601
+ Do not treat the screen track as the participant's camera track.
602
+
603
+ ---
525
604
 
526
- ┌────────┐ ┌────────┐ ┌────────┐
527
- │ Afolabi│ │ Mega │ │ John │
528
- └────────┘ └────────┘ └────────┘
605
+ # Recording
606
+
607
+ DEFCOMM Meet recording is **server-side**.
608
+
609
+ The browser does not use `MediaRecorder` to create the meeting recording.
610
+
611
+ Start recording:
612
+
613
+ ```tsx
614
+ const { startRecording } = useMeeting();
615
+
616
+ await startRecording();
529
617
  ```
530
618
 
531
- The screen should be the primary stage. Participant cameras should become a secondary strip.
619
+ Stop recording:
532
620
 
533
- Do not attach the screen track to the participant camera element.
621
+ ```tsx
622
+ const { stopRecording } = useMeeting();
534
623
 
535
- ---
624
+ await stopRecording();
625
+ ```
626
+
627
+ Get current recording status:
536
628
 
537
- # 13. Reconnection and resume
629
+ ```tsx
630
+ const { getRecordingStatus } = useMeeting();
538
631
 
539
- The SDK is designed to resume the same logical participant after a temporary network/signaling failure:
632
+ const result = await getRecordingStatus();
540
633
 
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
634
+ console.log(result.recording);
563
635
  ```
564
636
 
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. fileciteturn9file0L246-L275
637
+ A recording has:
566
638
 
567
- ### UI rule
639
+ ```ts
640
+ type RecordingInfo = {
641
+ recording_id: string;
642
+ started_at?: string;
643
+ stopped_at?: string;
644
+ active?: boolean;
645
+ };
646
+ ```
647
+
648
+ ## Recording events
568
649
 
569
- During a transient outage:
650
+ The SDK exposes:
651
+
652
+ ```tsx
653
+ useMeeting({
654
+ onRecordingStarted: (recording) => {
655
+ console.log("Recording started", recording);
656
+ },
657
+
658
+ onRecordingStopped: (recording) => {
659
+ console.log("Recording stopped", recording);
660
+ },
661
+ });
662
+ ```
663
+
664
+ ### Recommended DEFCOMM Meet UI
665
+
666
+ The meeting application can place the recording control in the meeting navbar:
570
667
 
571
668
  ```text
572
- Reconnecting...
669
+ ┌───────────────────────────────────────────────────────────────┐
670
+ │ Engineering 🔴 REC Chat People │
671
+ └───────────────────────────────────────────────────────────────┘
573
672
  ```
574
673
 
575
- should be shown rather than immediately removing the participant.
674
+ Host before recording:
576
675
 
577
- Only treat the participant as gone after the SFU reports an actual departure/disconnection.
676
+ ```text
677
+ ┌───────────────────────────────────────────────────────────────┐
678
+ │ Engineering [ Start Recording ] Chat People │
679
+ └───────────────────────────────────────────────────────────────┘
680
+ ```
578
681
 
579
- ---
682
+ Host while recording:
580
683
 
581
- # 14. Leave correctly
684
+ ```text
685
+ ┌───────────────────────────────────────────────────────────────┐
686
+ │ Engineering 🔴 Recording [ Stop ] Chat People │
687
+ └───────────────────────────────────────────────────────────────┘
688
+ ```
582
689
 
583
- Intentional departure:
690
+ Other participants should see the recording status but should not automatically receive the host's recording controls.
584
691
 
585
- ```tsx
586
- await leave();
692
+ ### Important recording rule
693
+
694
+ Do not assume that clicking the button means recording has started.
695
+
696
+ The UI should use:
697
+
698
+ ```ts
699
+ onRecordingStarted;
587
700
  ```
588
701
 
589
- Do not manually close:
702
+ as the authoritative start event and:
590
703
 
591
704
  ```ts
592
- websocket.close();
705
+ onRecordingStopped;
593
706
  ```
594
707
 
595
- to implement the Leave button.
708
+ as the authoritative stop event.
596
709
 
597
- The intended lifecycle is:
710
+ Recommended state:
598
711
 
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
712
+ ```tsx
713
+ const [isRecording, setIsRecording] = useState(false);
714
+
715
+ useMeeting({
716
+ onRecordingStarted: () => {
717
+ setIsRecording(true);
718
+ },
719
+
720
+ onRecordingStopped: () => {
721
+ setIsRecording(false);
722
+ },
723
+ });
611
724
  ```
612
725
 
613
- A temporary network failure is different and should use reconnect/resume.
726
+ This prevents the UI from claiming that a recording is active when the SFU has not confirmed it.
614
727
 
615
728
  ---
616
729
 
617
- # 15. Private-room admission
730
+ # Private rooms and join approval
618
731
 
619
- For private rooms:
732
+ Private rooms can require host approval.
733
+
734
+ A host can listen for entry requests:
620
735
 
621
736
  ```tsx
622
- const {
623
- approveJoinRequest,
624
- rejectJoinRequest,
625
- } = useMeeting();
737
+ useMeeting({
738
+ onEntryRequested: (request) => {
739
+ console.log("Join request:", request);
740
+ },
741
+ });
626
742
  ```
627
743
 
628
744
  Approve:
629
745
 
630
746
  ```tsx
631
- await approveJoinRequest(participantId);
747
+ const { approveJoinRequest } = useMeeting();
748
+
749
+ approveJoinRequest(requestId);
632
750
  ```
633
751
 
634
752
  Reject:
635
753
 
636
754
  ```tsx
637
- await rejectJoinRequest(participantId);
755
+ const { rejectJoinRequest } = useMeeting();
756
+
757
+ rejectJoinRequest(requestId);
638
758
  ```
639
759
 
640
- The SFU remains authoritative. The SDK does not fabricate approval state locally. fileciteturn9file0L278-L289
760
+ The SDK exposes the approval flow, while the SFU remains authoritative for whether a participant is admitted.
641
761
 
642
762
  ---
643
763
 
644
- # 16. Server-side recording
764
+ # Reconnection and participant presence
645
765
 
646
- Recording is **server-side**. The browser does not use `MediaRecorder`. fileciteturn9file0L291-L340
766
+ The SDK distinguishes a temporary connection problem from an intentional leave.
647
767
 
648
- Start:
768
+ A participant can be:
649
769
 
650
- ```tsx
651
- await startRecording();
770
+ ```ts
771
+ "connected";
772
+ "reconnecting";
773
+ "disconnected";
652
774
  ```
653
775
 
654
- Stop:
776
+ Recommended UI:
655
777
 
656
778
  ```tsx
657
- await stopRecording();
779
+ if (participant.connectionState === "reconnecting") {
780
+ return <ReconnectingBadge />;
781
+ }
658
782
  ```
659
783
 
660
- Status:
784
+ A temporary network interruption should not immediately remove a participant from the UI.
661
785
 
662
- ```tsx
663
- const { recording } = await getRecordingStatus();
786
+ The SDK attempts to reconnect and resume the same logical participant session.
787
+
788
+ Conceptually:
789
+
790
+ ```text
791
+ Network interruption
792
+ ↓
793
+ WebSocket disconnect
794
+ ↓
795
+ reconnecting
796
+ ↓
797
+ refresh/reconnect
798
+ ↓
799
+ resume
800
+ ↓
801
+ publisher/subscriber recreated
802
+ ↓
803
+ media subscriptions restored
804
+ ↓
805
+ connected
664
806
  ```
665
807
 
666
- Lifecycle:
808
+ This is why applications should render `reconnecting` as a state rather than treating every WebSocket close as a participant leaving.
809
+
810
+ ---
811
+
812
+ # Stable participant identity
813
+
814
+ The SDK stores a stable browser participant ID using:
667
815
 
668
816
  ```text
669
- startRecording()
670
- ↓
671
- start_recording
672
- ↓
673
- Rust SFU RecordingManager
674
- ↓
675
- recording_started
817
+ defcomm:participant_id
676
818
  ```
677
819
 
678
- Stop:
820
+ This prevents a browser reload or temporary reconnect from unnecessarily generating a completely new participant identity.
821
+
822
+ Room resume state uses:
679
823
 
680
824
  ```text
681
- stopRecording()
682
- ↓
683
- stop_recording
684
- ↓
685
- RecordingManager finalizes
686
- ↓
687
- recording_stopped
825
+ defcomm:sfu:resume:<roomCode>
688
826
  ```
689
827
 
690
- The recording belongs to the meeting/server rather than a particular browser. Recording permissions are enforced by the SFU. fileciteturn9file0L291-L340
828
+ The SDK treats temporary network loss differently from an intentional `leave()`.
829
+
830
+ Applications should use:
831
+
832
+ ```tsx
833
+ await leave();
834
+ ```
835
+
836
+ for an intentional departure instead of manually closing the WebSocket.
691
837
 
692
838
  ---
693
839
 
694
- # 17. Room management
840
+ # Leaving a meeting
695
841
 
696
- Create:
842
+ Use:
697
843
 
698
844
  ```tsx
699
- const room = await createRoom({
700
- name: "Engineering",
845
+ const { leave } = useMeeting();
846
+
847
+ await leave();
848
+ ```
849
+
850
+ Do not implement the Leave button by doing:
851
+
852
+ ```ts
853
+ websocket.close();
854
+ ```
855
+
856
+ or by directly destroying the SDK's PeerConnections.
857
+
858
+ The SDK owns cleanup and the SFU leave lifecycle.
859
+
860
+ ---
861
+
862
+ # Room management
863
+
864
+ Room management is available through `useMeeting()`:
865
+
866
+ ```tsx
867
+ const { createRoom, getRoom, deleteRoom } = useMeeting();
868
+ ```
869
+
870
+ ### Create
871
+
872
+ ```tsx
873
+ await createRoom({
874
+ name: "Product Review",
875
+ description: "Weekly product review",
701
876
  capacity: 100,
702
- is_private: true,
877
+ is_private: false,
703
878
  });
704
879
  ```
705
880
 
706
- Get:
881
+ ### Get
707
882
 
708
883
  ```tsx
709
- const room = await getRoom(roomCode);
884
+ await getRoom(roomCode);
710
885
  ```
711
886
 
712
- Delete:
887
+ ### Delete
713
888
 
714
889
  ```tsx
715
890
  await deleteRoom(roomCode);
716
891
  ```
717
892
 
718
- These use the REST API. fileciteturn9file0L342-L355
893
+ ---
894
+
895
+ # Meeting preview
896
+
897
+ The SDK exports:
898
+
899
+ ```tsx
900
+ import { useMeetingPreview } from "@afosecure/meetingsdk";
901
+ ```
902
+
903
+ Use preview when the application needs a pre-join screen for room information or local setup.
904
+
905
+ The actual live meeting begins when `join()` connects the SDK to the SFU.
719
906
 
720
907
  ---
721
908
 
722
- # 18. Preview
909
+ # Chat
723
910
 
724
- The old mesh SDK used:
911
+ The SDK exposes the current chat interface through:
725
912
 
726
- ```text
727
- /watch/:room
913
+ ```tsx
914
+ const { sendMessage } = useMeeting();
728
915
  ```
729
916
 
730
- That endpoint is not part of the current Rust SFU.
917
+ Send:
731
918
 
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`. fileciteturn9file0L358-L365
919
+ ```tsx
920
+ sendMessage({
921
+ message: "Hello everyone",
922
+ });
923
+ ```
924
+
925
+ The input type is:
926
+
927
+ ```ts
928
+ type ChatInput = {
929
+ message: string;
930
+ reply_to?: {
931
+ id: string;
932
+ name: string;
933
+ } | null;
934
+ target?: string | null;
935
+ };
936
+ ```
937
+
938
+ The SDK also exposes:
939
+
940
+ ```tsx
941
+ usePubSub("SECURE_CHAT");
942
+ ```
943
+
944
+ when an application wants the Pub/Sub style interface.
945
+
946
+ Example:
947
+
948
+ ```tsx
949
+ const chat = usePubSub("SECURE_CHAT");
950
+
951
+ chat.publish({
952
+ message: "Hello",
953
+ });
954
+ ```
733
955
 
734
956
  ---
735
957
 
736
- # 19. Current signaling protocol
958
+ # Error handling
737
959
 
738
- ### Client → SFU
960
+ Use the SDK error event:
739
961
 
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
962
+ ```tsx
963
+ useMeeting({
964
+ onError: (error) => {
965
+ console.error(error);
966
+ },
967
+ });
968
+ ```
969
+
970
+ Errors contain:
971
+
972
+ ```ts
973
+ type SDKError = {
974
+ code: string;
975
+ message: string;
976
+ roomId?: string | null;
977
+ userId?: string;
978
+ raw?: any;
979
+ recoverable?: boolean;
980
+ };
981
+ ```
982
+
983
+ A useful UI rule is:
759
984
 
760
985
  ```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
- fileciteturn9file0L22-L64
786
-
787
- ---
788
-
789
- # 20. Recommended React structure
986
+ recoverable = true
987
+ ↓
988
+ show retry/reconnecting UI
989
+
990
+ recoverable = false
991
+ ↓
992
+ show a blocking error or return to the appropriate screen
993
+ ```
994
+
995
+ Always wrap user-triggered asynchronous operations when appropriate:
996
+
997
+ ```tsx
998
+ async function handleJoin() {
999
+ try {
1000
+ await join();
1001
+ } catch (error) {
1002
+ console.error("Unable to join meeting", error);
1003
+ }
1004
+ }
1005
+ ```
1006
+
1007
+ ---
1008
+
1009
+ # Recommended DEFCOMM Meet structure
1010
+
1011
+ A production DEFCOMM Meet page can be structured like this:
790
1012
 
791
1013
  ```text
792
1014
  MeetingPage
793
1015
  │
794
1016
  └── MeetingProvider
795
1017
  │
796
- ├── MeetingHeader
797
- │ ├── Room name
798
- │ ├── Connection state
799
- │ └── Recording indicator
1018
+ ├── MeetingNav
1019
+ │ ├── Meeting name
1020
+ │ ├── Recording state/control
1021
+ │ ├── Chat
1022
+ │ └── Participants
800
1023
  │
801
1024
  ├── MeetingStage
1025
+ │ │
802
1026
  │ ├── ScreenShareStage
803
- │ └── ActiveSpeakerStage
804
- │
805
- ├── ParticipantStrip
806
- │ └── ParticipantTile[]
807
- │ ├── Video
808
- │ ├── Name
809
- │ ├── Mic icon
810
- │ ├── Camera icon
811
- │ └── Reconnecting badge
1027
+ │ │
1028
+ │ └── ParticipantGrid
1029
+ │ └── ParticipantTile[]
1030
+ │ ├── Video
1031
+ │ ├── Name
1032
+ │ ├── Mic state
1033
+ │ ├── Camera state
1034
+ │ ├── Active speaker state
1035
+ │ └── Reconnecting state
812
1036
  │
813
1037
  └── MeetingControls
814
- ├── Mic
1038
+ ├── Microphone
815
1039
  ├── Camera
816
1040
  ├── Screen share
817
- ├── Record
818
1041
  └── Leave
819
1042
  ```
820
1043
 
821
- The SDK supplies media and meeting state. Your application owns the visual design.
1044
+ The application should consume SDK state rather than recreating WebRTC state locally.
822
1045
 
823
1046
  ---
824
1047
 
825
- # 21. Error handling
1048
+ # Recommended participant tile
826
1049
 
827
- Catch operations that can fail:
1050
+ A participant tile should combine the SDK's states:
828
1051
 
829
1052
  ```tsx
830
- try {
831
- await join();
832
- } catch (error) {
833
- console.error("Unable to join meeting", error);
1053
+ function ParticipantTile({ participant }: { participant: Participant }) {
1054
+ const { activeSpeakerIds } = useMeeting();
1055
+
1056
+ const isSpeaking = activeSpeakerIds.includes(participant.id);
1057
+
1058
+ const isReconnecting = participant.connectionState === "reconnecting";
1059
+
1060
+ const micEnabled = participant.media?.micEnabled ?? false;
1061
+
1062
+ const camEnabled = participant.media?.camEnabled ?? false;
1063
+
1064
+ const isScreenSharing = participant.media?.isScreenSharing ?? false;
1065
+
1066
+ return (
1067
+ <div
1068
+ className={
1069
+ isSpeaking ? "participant-tile active-speaker" : "participant-tile"
1070
+ }
1071
+ >
1072
+ {isReconnecting && <div className="reconnecting">Reconnecting...</div>}
1073
+
1074
+ {/* Remote/local media */}
1075
+
1076
+ <div className="participant-name">{participant.name}</div>
1077
+
1078
+ <div className="participant-status">
1079
+ {!micEnabled && <span>Muted</span>}
1080
+ {!camEnabled && <span>Camera off</span>}
1081
+ {isScreenSharing && <span>Sharing</span>}
1082
+ </div>
1083
+ </div>
1084
+ );
834
1085
  }
835
1086
  ```
836
1087
 
837
- Your UI should distinguish:
1088
+ The important design principle is:
838
1089
 
839
1090
  ```text
840
- Connecting
841
- Reconnecting
842
- Connected
843
- Disconnected
844
- Join rejected
1091
+ SDK state → UI
845
1092
  ```
846
1093
 
847
- Do not show "left the meeting" for a temporary ICE/WebSocket interruption.
1094
+ not:
1095
+
1096
+ ```text
1097
+ UI → WebRTC internals
1098
+ ```
848
1099
 
849
1100
  ---
850
1101
 
851
- # 22. Do not reintroduce mesh logic
1102
+ # Public exports
1103
+
1104
+ The SDK currently exports:
1105
+
1106
+ ```ts
1107
+ // Core
1108
+ VideoSDKCore;
1109
+ SFUClient;
1110
+ MeetingState;
1111
+ ActiveSpeakerDetector;
1112
+
1113
+ // React
1114
+ MeetingProvider;
1115
+ useMeetingContext;
1116
+ useMeeting;
1117
+ useParticipants;
1118
+ useLocalParticipant;
1119
+ useRemoteMedia;
1120
+ useMeetingPreview;
1121
+
1122
+ // Types
1123
+ Participant;
1124
+ ChatInput;
1125
+ RecordingInfo;
1126
+ MeetingConfig;
1127
+ SFUConnectionState;
1128
+ TrackInfo;
1129
+ ParticipantInfo;
1130
+ ```
1131
+
1132
+ Most DEFCOMM Meet application developers should only need the React API:
1133
+
1134
+ ```tsx
1135
+ import {
1136
+ MeetingProvider,
1137
+ useMeeting,
1138
+ useParticipants,
1139
+ useLocalParticipant,
1140
+ useRemoteMedia,
1141
+ useMeetingPreview,
1142
+ } from "@afosecure/meetingsdk";
1143
+ ```
1144
+
1145
+ Direct use of `VideoSDKCore` or `SFUClient` is intended for advanced integrations.
852
1146
 
853
- Never create a PeerConnection per remote participant.
1147
+ ---
854
1148
 
855
- Wrong:
1149
+ # Important implementation rules
1150
+
1151
+ ## 1. Do not build a mesh
1152
+
1153
+ Do not create:
856
1154
 
857
1155
  ```text
858
- A ↔ B
859
- A ↔ C
860
- B ↔ C
1156
+ Participant A ↔ Participant B
1157
+ Participant A ↔ Participant C
1158
+ Participant B ↔ Participant C
861
1159
  ```
862
1160
 
863
- Correct:
1161
+ The SDK is designed for:
864
1162
 
865
1163
  ```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
1164
+ Participants
1165
+ │
1166
+ ▼
1167
+ DEFCOMM SFU
1168
+ │
1169
+ ▼
1170
+ Participants
1171
+ ```
1172
+
1173
+ ---
1174
+
1175
+ ## 2. Do not manipulate the SDK's PeerConnections
1176
+
1177
+ Applications should not directly access:
1178
+
1179
+ ```ts
1180
+ RTCPeerConnection;
1181
+ RTCSessionDescription;
1182
+ RTCIceCandidate;
1183
+ RTCRtpSender;
1184
+ ```
1185
+
1186
+ for normal meeting functionality.
1187
+
1188
+ The SDK owns those objects.
1189
+
1190
+ ---
1191
+
1192
+ ## 3. Do not infer remote mute state from media silence
1193
+
1194
+ Use:
1195
+
1196
+ ```ts
1197
+ participant.media?.micEnabled;
1198
+ participant.media?.camEnabled;
1199
+ ```
1200
+
1201
+ The SFU signaling state is authoritative.
1202
+
1203
+ ---
1204
+
1205
+ ## 4. Do not implement speaker detection in the application
1206
+
1207
+ Use:
1208
+
1209
+ ```tsx
1210
+ const { activeSpeakerIds } = useMeeting();
1211
+ ```
1212
+
1213
+ The SDK owns audio analysis and active-speaker state.
885
1214
 
886
- The browser SDK owns:
1215
+ ---
1216
+
1217
+ ## 5. Do not use browser recording for meeting recording
1218
+
1219
+ Use:
1220
+
1221
+ ```tsx
1222
+ await startRecording();
1223
+ await stopRecording();
1224
+ ```
1225
+
1226
+ Meeting recording is server-side.
1227
+
1228
+ ---
1229
+
1230
+ ## 6. Do not manually close the WebSocket to leave
1231
+
1232
+ Use:
1233
+
1234
+ ```tsx
1235
+ await leave();
1236
+ ```
1237
+
1238
+ This allows the SDK to perform the intended cleanup and leave lifecycle.
1239
+
1240
+ ---
1241
+
1242
+ ## 7. Do not immediately remove reconnecting participants
1243
+
1244
+ Use:
1245
+
1246
+ ```ts
1247
+ participant.connectionState;
1248
+ ```
887
1249
 
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
1250
+ and show:
1251
+
1252
+ ```text
1253
+ Reconnecting...
1254
+ ```
895
1255
 
896
- fileciteturn9file0L423-L450
1256
+ during transient failures.
897
1257
 
898
1258
  ---
899
1259
 
900
- # 23. Build
1260
+ ## 8. Keep screen share separate from camera
1261
+
1262
+ Use:
1263
+
1264
+ ```ts
1265
+ participant.media.screenStream;
1266
+ participant.media.screenTrack;
1267
+ participant.media.isScreenSharing;
1268
+ ```
1269
+
1270
+ instead of replacing the camera stream.
1271
+
1272
+ ---
1273
+
1274
+ # Build and publish
1275
+
1276
+ Install dependencies:
901
1277
 
902
1278
  ```bash
903
1279
  npm install
1280
+ ```
1281
+
1282
+ Build:
1283
+
1284
+ ```bash
904
1285
  npm run build
905
1286
  ```
906
1287
 
907
- Expected output:
1288
+ Before publishing a new SDK version, verify:
908
1289
 
909
1290
  ```text
910
- dist/
911
- ├── index.js
912
- ├── index.mjs
913
- ├── index.d.ts
914
- └── index.d.mts
1291
+ ✓ TypeScript build succeeds
1292
+ ✓ React integration works
1293
+ ✓ Join/leave works
1294
+ ✓ Camera works
1295
+ ✓ Microphone works
1296
+ ✓ Remote audio/video works
1297
+ ✓ Screen sharing works
1298
+ ✓ Active speaker detection works
1299
+ ✓ Reconnection works
1300
+ ✓ Private-room admission works
1301
+ ✓ Recording start/stop works
1302
+ ✓ Recording callbacks fire
1303
+ ✓ Production WSS/API configuration works
915
1304
  ```
916
1305
 
917
- The SDK consumes the SFU media path:
1306
+ ---
918
1307
 
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
- fileciteturn9file0L452-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:
1308
+ # DEFCOMM Meet integration principle
1309
+
1310
+ The purpose of this SDK is to make the DEFCOMM meeting experience feel like a single integrated platform.
1311
+
1312
+ A consumer should be able to think in terms of:
1313
+
1314
+ ```tsx
1315
+ const {
1316
+ join,
1317
+ leave,
1318
+ toggleMic,
1319
+ toggleCam,
1320
+ startScreenShare,
1321
+ stopScreenShare,
1322
+ startRecording,
1323
+ stopRecording,
1324
+ participants,
1325
+ activeSpeakerIds,
1326
+ } = useMeeting();
1327
+ ```
1328
+
1329
+ rather than:
971
1330
 
972
1331
  ```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.
1332
+ WebSocket messages
1333
+ SDP offers/answers
1334
+ ICE candidates
1335
+ PeerConnections
1336
+ RTP tracks
1337
+ SSRCs
1338
+ audio analysers
1339
+ reconnect timers
1340
+ resume tokens
1341
+ ```
1342
+
1343
+ Those implementation details belong inside the SDK/SFU layer.
1344
+
1345
+ **DEFCOMM Meet owns the meeting experience.
1346
+ The SDK owns the browser media experience.
1347
+ The SFU owns the authoritative meeting/media infrastructure.**