@defcomm/meetingsdk
v0.6.1
Published
React/TypeScript meeting SDK for the DEFCOMM Rust SFU
Readme
DEFCOMM Meet SDK
@afosecure/meetingsdk is the React/TypeScript client SDK used by DEFCOMM Meet.
It provides the application layer for:
- joining and leaving meetings
- microphone and camera control
- remote participant media
- screen sharing
- active-speaker detection
- participant presence and reconnection state
- private-room admission
- server-side recording
- room management
- chat/PubSub integration
- WebRTC and SFU signaling orchestration
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.
Table of contents
- What the SDK does
- Architecture
- Installation
- SDK configuration
- Creating a meeting
- Joining a meeting
- MeetingProvider
- The
useMeetinghook - Participants
- Local participant
- Remote media
- Microphone and camera
- Active speaker detection
- Screen sharing
- Recording
- Private rooms and join approval
- Reconnection and participant presence
- Leaving a meeting
- Room management
- Meeting preview
- Chat
- Error handling
- Recommended DEFCOMM Meet structure
- Important implementation rules
- Build and publish
What the SDK does
DEFCOMM Meet is built around an SFU architecture.
The browser does not establish a separate peer connection with every participant.
Instead:
Participant A ──┐
Participant B ──┼──► DEFCOMM Rust SFU ──► Participants
Participant C ──┘The SDK sits between the React application and the SFU:
┌───────────────────────────────┐
│ DEFCOMM Meet App │
│ │
│ Meeting UI / Participant UI │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ @afosecure/meetingsdk │
│ │
│ React hooks │
│ Meeting state │
│ WebRTC │
│ WebSocket signaling │
│ Reconnection │
│ Active speaker detection │
│ Media attachment │
│ Recording controls │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
│ DEFCOMM Rust SFU │
│ │
│ Rooms │
│ Participants │
│ Admission │
│ SDP / ICE │
│ RTP forwarding │
│ Track state │
│ Recording │
└───────────────────────────────┘The responsibility split
Your application owns:
- layout
- buttons
- participant tiles
- navbar
- chat UI
- recording indicator
- active-speaker styling
- loading/error screens
The SDK owns:
- browser media access
- publisher PeerConnection
- subscriber PeerConnection
- WebSocket signaling
- media track mapping
- reconnect/resume behavior
- active-speaker detection
- SDK meeting state
The SFU owns:
- room state
- participant membership
- admission/approval
- signaling authority
- media forwarding
- track publication/subscription
- server-side recording
Installation
npm install @afosecure/meetingsdkThe SDK is intended for browser-based React + TypeScript applications.
Creating a meeting
The SDK exposes room management through VideoSDKCore and useMeeting().
const { createRoom } = useMeeting();
const room = await createRoom({
name: "Engineering Standup",
description: "Daily engineering meeting",
capacity: 50,
is_private: true,
});A room can also be retrieved:
const { getRoom } = useMeeting();
const room = await getRoom(roomCode);And deleted when appropriate:
const { deleteRoom } = useMeeting();
await deleteRoom(roomCode);Room creation and deletion should normally be restricted to the appropriate application role.
Joining a meeting
The normal React integration starts with MeetingProvider.
import { MeetingProvider, useMeeting } from "@afosecure/meetingsdk";
function MeetingRoom() {
const { join, leave } = useMeeting();
return (
<div>
<button onClick={() => join()}>Join meeting</button>
<button onClick={() => leave()}>Leave</button>
</div>
);
}
export function MeetingPage() {
return (
<MeetingProvider
config={{
roomId: "room-code",
name: "Afolabi",
audioMuted: false,
videoMuted: false,
}}
>
<MeetingRoom />
</MeetingProvider>
);
}MeetingProvider accepts:
type MeetingConfig = {
roomId: string;
name: string;
audioMuted?: boolean;
videoMuted?: boolean;
token?: string;
};Recommended join flow
Meeting page
↓
MeetingProvider
↓
User clicks Join
↓
join()
↓
Browser requests camera/microphone
↓
SDK connects to SFU
↓
Publisher PeerConnection
↓
Subscriber PeerConnection
↓
Remote tracks become availableCall join() from a user action whenever possible. This gives the browser a normal user-initiated context for requesting media permissions.
The useMeeting hook
useMeeting() is the main application API.
const {
join,
leave,
toggleMic,
toggleCam,
startScreenShare,
stopScreenShare,
startRecording,
stopRecording,
getRecordingStatus,
participants,
localParticipant,
activeSpeakerIds,
presenterId,
room,
approveJoinRequest,
rejectJoinRequest,
sendMessage,
} = useMeeting();The hook also supports event handlers:
const meeting = useMeeting({
onError: (error) => {
console.error(error);
},
onActiveSpeakersChanged: (participantIds) => {
console.log("Active speakers:", participantIds);
},
onRecordingStarted: (recording) => {
console.log("Recording started:", recording);
},
onRecordingStopped: (recording) => {
console.log("Recording stopped:", recording);
},
});For normal React rendering, prefer the state exposed by the hook:
const { activeSpeakerIds } = useMeeting();Use event callbacks when the application needs to perform an additional side effect.
Participants
Use useParticipants() when the component only needs the participant list.
import { useParticipants } from "@afosecure/meetingsdk";
function ParticipantList() {
const participants = useParticipants();
return (
<div>
{participants.map((participant) => (
<div key={participant.id}>{participant.name}</div>
))}
</div>
);
}A participant contains:
type Participant = {
id: string;
name?: string;
isHost?: boolean;
isLocal?: boolean;
isPresenter?: boolean;
connectionState?: "connected" | "reconnecting" | "disconnected";
media?: {
stream?: MediaStream | null;
screenStream?: MediaStream | null;
cameraTrack?: MediaStreamTrack;
screenTrack?: MediaStreamTrack;
audioTrack?: MediaStreamTrack;
micEnabled: boolean;
camEnabled: boolean;
isScreenSharing: boolean;
remoteScreenStreamId?: string;
cameraStreamId?: string;
};
};Local participant
Use useLocalParticipant() when you need the local participant and local video.
import { useLocalParticipant } from "@afosecure/meetingsdk";
function LocalVideo() {
const { participant, videoRef } = useLocalParticipant();
return <video ref={videoRef} autoPlay muted playsInline />;
}The local participant exposes:
participant?.media?.micEnabled;
participant?.media?.camEnabled;
participant?.media?.isScreenSharing;Use these values to render local microphone/camera state.
Remote media
Use useRemoteMedia(participantId) for a remote participant.
import { useRemoteMedia } from "@afosecure/meetingsdk";
function RemoteParticipant({ participantId }: { participantId: string }) {
const { videoRef, audioRef, isCamActive, isMicEnabled } =
useRemoteMedia(participantId);
return (
<div>
<video ref={videoRef} autoPlay playsInline muted />
<audio ref={audioRef} autoPlay />
{!isMicEnabled && <span>Muted</span>}
{!isCamActive && <span>Camera off</span>}
</div>
);
}Why are video and audio separate?
The SDK attaches the remote MediaStream to both elements:
<video>is muted<audio>handles remote audio
This makes media rendering explicit and avoids accidentally routing remote audio through the video element.
Microphone and camera
The SDK exposes:
const { toggleMic, toggleCam } = useMeeting();Use:
<button onClick={toggleMic}>
Microphone
</button>
<button onClick={toggleCam}>
Camera
</button>The SDK updates the local media track and sends the corresponding media state to the SFU.
Remote participants receive the authoritative track state through the meeting state.
For participant UI, use:
participant.media?.micEnabled;
participant.media?.camEnabled;rather than trying to infer another participant's mute state from RTP or MediaStreamTrack.muted.
Active speaker detection
DEFCOMM Meet performs active-speaker detection inside the SDK.
The application does not need to use Web Audio APIs, AnalyserNode, audio RMS calculations, timers, or WebRTC statistics.
The SDK exposes:
const { activeSpeakerIds } = useMeeting();activeSpeakerIds is:
string[]containing participant IDs currently detected as speaking.
Participant tile
function ParticipantTile({ participant }: { participant: Participant }) {
const { activeSpeakerIds } = useMeeting();
const isActiveSpeaker = activeSpeakerIds.includes(participant.id);
return (
<div className={isActiveSpeaker ? "ring-2 ring-green-400" : ""}>
{participant.name}
</div>
);
}Better pattern
For a participant list:
const { activeSpeakerIds } = useMeeting();
{
participants.map((participant) => (
<ParticipantTile
key={participant.id}
participant={participant}
isActiveSpeaker={activeSpeakerIds.includes(participant.id)}
/>
));
}The tile should only render the state. It should not implement speaker detection itself.
Active speaker event
The SDK also exposes:
useMeeting({
onActiveSpeakersChanged: (participantIds) => {
console.log("Active speakers:", participantIds);
},
});The event is useful for analytics or application-level side effects. For normal UI rendering, activeSpeakerIds is preferred.
Multiple speakers
The API intentionally supports multiple active speakers:
activeSpeakerIds: string[]rather than:
activeSpeakerId: string | null;This allows the UI to handle overlapping speech naturally.
Screen sharing
Start:
const { startScreenShare } = useMeeting();
await startScreenShare();Stop:
const { stopScreenShare } = useMeeting();
stopScreenShare();A participant's screen share is represented separately from their camera:
participant.media?.screenStream;
participant.media?.screenTrack;
participant.media?.isScreenSharing;Recommended UI
When someone shares their screen:
┌──────────────────────────────────────────┐
│ │
│ SCREEN SHARE │
│ │
│ │
└──────────────────────────────────────────┘
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Alice │ │ Bob │ │ Charlie │
└─────────┘ └─────────┘ └─────────┘Do not treat the screen track as the participant's camera track.
Recording
DEFCOMM Meet recording is server-side.
The browser does not use MediaRecorder to create the meeting recording.
Start recording:
const { startRecording } = useMeeting();
await startRecording();Stop recording:
const { stopRecording } = useMeeting();
await stopRecording();Get current recording status:
const { getRecordingStatus } = useMeeting();
const result = await getRecordingStatus();
console.log(result.recording);A recording has:
type RecordingInfo = {
recording_id: string;
started_at?: string;
stopped_at?: string;
active?: boolean;
};Recording events
The SDK exposes:
useMeeting({
onRecordingStarted: (recording) => {
console.log("Recording started", recording);
},
onRecordingStopped: (recording) => {
console.log("Recording stopped", recording);
},
});Recommended DEFCOMM Meet UI
The meeting application can place the recording control in the meeting navbar:
┌───────────────────────────────────────────────────────────────┐
│ Engineering 🔴 REC Chat People │
└───────────────────────────────────────────────────────────────┘Host before recording:
┌───────────────────────────────────────────────────────────────┐
│ Engineering [ Start Recording ] Chat People │
└───────────────────────────────────────────────────────────────┘Host while recording:
┌───────────────────────────────────────────────────────────────┐
│ Engineering 🔴 Recording [ Stop ] Chat People │
└───────────────────────────────────────────────────────────────┘Other participants should see the recording status but should not automatically receive the host's recording controls.
Important recording rule
Do not assume that clicking the button means recording has started.
The UI should use:
onRecordingStarted;as the authoritative start event and:
onRecordingStopped;as the authoritative stop event.
Recommended state:
const [isRecording, setIsRecording] = useState(false);
useMeeting({
onRecordingStarted: () => {
setIsRecording(true);
},
onRecordingStopped: () => {
setIsRecording(false);
},
});This prevents the UI from claiming that a recording is active when the SFU has not confirmed it.
Private rooms and join approval
Private rooms can require host approval.
A host can listen for entry requests:
useMeeting({
onEntryRequested: (request) => {
console.log("Join request:", request);
},
});Approve:
const { approveJoinRequest } = useMeeting();
approveJoinRequest(requestId);Reject:
const { rejectJoinRequest } = useMeeting();
rejectJoinRequest(requestId);The SDK exposes the approval flow, while the SFU remains authoritative for whether a participant is admitted.
Reconnection and participant presence
The SDK distinguishes a temporary connection problem from an intentional leave.
A participant can be:
"connected";
"reconnecting";
"disconnected";Recommended UI:
if (participant.connectionState === "reconnecting") {
return <ReconnectingBadge />;
}A temporary network interruption should not immediately remove a participant from the UI.
The SDK attempts to reconnect and resume the same logical participant session.
Conceptually:
Network interruption
↓
WebSocket disconnect
↓
reconnecting
↓
refresh/reconnect
↓
resume
↓
publisher/subscriber recreated
↓
media subscriptions restored
↓
connectedThis is why applications should render reconnecting as a state rather than treating every WebSocket close as a participant leaving.
Stable participant identity
The SDK stores a stable browser participant ID using:
defcomm:participant_idThis prevents a browser reload or temporary reconnect from unnecessarily generating a completely new participant identity.
Room resume state uses:
defcomm:sfu:resume:<roomCode>The SDK treats temporary network loss differently from an intentional leave().
Applications should use:
await leave();for an intentional departure instead of manually closing the WebSocket.
Leaving a meeting
Use:
const { leave } = useMeeting();
await leave();Do not implement the Leave button by doing:
websocket.close();or by directly destroying the SDK's PeerConnections.
The SDK owns cleanup and the SFU leave lifecycle.
Room management
Room management is available through useMeeting():
const { createRoom, getRoom, deleteRoom } = useMeeting();Create
await createRoom({
name: "Product Review",
description: "Weekly product review",
capacity: 100,
is_private: false,
});Get
await getRoom(roomCode);Delete
await deleteRoom(roomCode);Meeting preview
The SDK exports:
import { useMeetingPreview } from "@afosecure/meetingsdk";Use preview when the application needs a pre-join screen for room information or local setup.
The actual live meeting begins when join() connects the SDK to the SFU.
Chat
The SDK exposes the current chat interface through:
const { sendMessage } = useMeeting();Send:
sendMessage({
message: "Hello everyone",
});The input type is:
type ChatInput = {
message: string;
reply_to?: {
id: string;
name: string;
} | null;
target?: string | null;
};The SDK also exposes:
usePubSub("SECURE_CHAT");when an application wants the Pub/Sub style interface.
Example:
const chat = usePubSub("SECURE_CHAT");
chat.publish({
message: "Hello",
});Error handling
Use the SDK error event:
useMeeting({
onError: (error) => {
console.error(error);
},
});Errors contain:
type SDKError = {
code: string;
message: string;
roomId?: string | null;
participantId?: string;
raw?: any;
recoverable?: boolean;
};A useful UI rule is:
recoverable = true
↓
show retry/reconnecting UI
recoverable = false
↓
show a blocking error or return to the appropriate screenAlways wrap user-triggered asynchronous operations when appropriate:
async function handleJoin() {
try {
await join();
} catch (error) {
console.error("Unable to join meeting", error);
}
}Recommended DEFCOMM Meet structure
A production DEFCOMM Meet page can be structured like this:
MeetingPage
│
└── MeetingProvider
│
├── MeetingNav
│ ├── Meeting name
│ ├── Recording state/control
│ ├── Chat
│ └── Participants
│
├── MeetingStage
│ │
│ ├── ScreenShareStage
│ │
│ └── ParticipantGrid
│ └── ParticipantTile[]
│ ├── Video
│ ├── Name
│ ├── Mic state
│ ├── Camera state
│ ├── Active speaker state
│ └── Reconnecting state
│
└── MeetingControls
├── Microphone
├── Camera
├── Screen share
└── LeaveThe application should consume SDK state rather than recreating WebRTC state locally.
Recommended participant tile
A participant tile should combine the SDK's states:
function ParticipantTile({ participant }: { participant: Participant }) {
const { activeSpeakerIds } = useMeeting();
const isSpeaking = activeSpeakerIds.includes(participant.id);
const isReconnecting = participant.connectionState === "reconnecting";
const micEnabled = participant.media?.micEnabled ?? false;
const camEnabled = participant.media?.camEnabled ?? false;
const isScreenSharing = participant.media?.isScreenSharing ?? false;
return (
<div
className={
isSpeaking ? "participant-tile active-speaker" : "participant-tile"
}
>
{isReconnecting && <div className="reconnecting">Reconnecting...</div>}
{/* Remote/local media */}
<div className="participant-name">{participant.name}</div>
<div className="participant-status">
{!micEnabled && <span>Muted</span>}
{!camEnabled && <span>Camera off</span>}
{isScreenSharing && <span>Sharing</span>}
</div>
</div>
);
}The important design principle is:
SDK state → UInot:
UI → WebRTC internalsPublic exports
The SDK currently exports:
// Core
VideoSDKCore;
SFUClient;
MeetingState;
ActiveSpeakerDetector;
// React
MeetingProvider;
useMeetingContext;
useMeeting;
useParticipants;
useLocalParticipant;
useRemoteMedia;
useMeetingPreview;
// Types
Participant;
ChatInput;
RecordingInfo;
MeetingConfig;
SFUConnectionState;
TrackInfo;
ParticipantInfo;Most DEFCOMM Meet application developers should only need the React API:
import {
MeetingProvider,
useMeeting,
useParticipants,
useLocalParticipant,
useRemoteMedia,
useMeetingPreview,
} from "@afosecure/meetingsdk";Direct use of VideoSDKCore or SFUClient is intended for advanced integrations.
Important implementation rules
1. Do not build a mesh
Do not create:
Participant A ↔ Participant B
Participant A ↔ Participant C
Participant B ↔ Participant CThe SDK is designed for:
Participants
│
▼
DEFCOMM SFU
│
▼
Participants2. Do not manipulate the SDK's PeerConnections
Applications should not directly access:
RTCPeerConnection;
RTCSessionDescription;
RTCIceCandidate;
RTCRtpSender;for normal meeting functionality.
The SDK owns those objects.
3. Do not infer remote mute state from media silence
Use:
participant.media?.micEnabled;
participant.media?.camEnabled;The SFU signaling state is authoritative.
4. Do not implement speaker detection in the application
Use:
const { activeSpeakerIds } = useMeeting();The SDK owns audio analysis and active-speaker state.
5. Do not use browser recording for meeting recording
Use:
await startRecording();
await stopRecording();Meeting recording is server-side.
6. Do not manually close the WebSocket to leave
Use:
await leave();This allows the SDK to perform the intended cleanup and leave lifecycle.
7. Do not immediately remove reconnecting participants
Use:
participant.connectionState;and show:
Reconnecting...during transient failures.
8. Keep screen share separate from camera
Use:
participant.media.screenStream;
participant.media.screenTrack;
participant.media.isScreenSharing;instead of replacing the camera stream.
Build and publish
Install dependencies:
npm installBuild:
npm run buildBefore publishing a new SDK version, verify:
✓ TypeScript build succeeds
✓ React integration works
✓ Join/leave works
✓ Camera works
✓ Microphone works
✓ Remote audio/video works
✓ Screen sharing works
✓ Active speaker detection works
✓ Reconnection works
✓ Private-room admission works
✓ Recording start/stop works
✓ Recording callbacks fire
✓ Production WSS/API configuration worksDEFCOMM Meet integration principle
The purpose of this SDK is to make the DEFCOMM meeting experience feel like a single integrated platform.
A consumer should be able to think in terms of:
const {
join,
leave,
toggleMic,
toggleCam,
startScreenShare,
stopScreenShare,
startRecording,
stopRecording,
participants,
activeSpeakerIds,
} = useMeeting();rather than:
WebSocket messages
SDP offers/answers
ICE candidates
PeerConnections
RTP tracks
SSRCs
audio analysers
reconnect timers
resume tokensThose implementation details belong inside the SDK/SFU layer.
DEFCOMM Meet owns the meeting experience.
The SDK owns the browser media experience.
The SFU owns the authoritative meeting/media infrastructure.
