@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/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. fileciteturn9file0L87-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. fileciteturn9file0L5-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. fileciteturn9file0L76-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. fileciteturn9file0L22-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. fileciteturn9file0L116-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. fileciteturn9file0L367-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. fileciteturn9file0L161-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. fileciteturn9file0L144-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. fileciteturn9file0L180-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. fileciteturn9file0L202-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
+ fileciteturn9file0L387-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. fileciteturn9file0L206-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
+ fileciteturn9file0L235-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. fileciteturn9file0L246-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. fileciteturn9file0L278-L289
641
+
642
+ ---
643
+
644
+ # 16. Server-side recording
645
+
646
+ Recording is **server-side**. The browser does not use `MediaRecorder`. fileciteturn9file0L291-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. fileciteturn9file0L291-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. fileciteturn9file0L342-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`. fileciteturn9file0L358-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
+ fileciteturn9file0L22-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
+ fileciteturn9file0L423-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
+ 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:
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.