@truncatetechnologies/streamsdk
v1.0.9
Published
Drop-in video call and live streaming SDK for React
Readme
StreamSDK — React / Web
Drop-in React SDK for real-time video calls and live streaming.
Features
- ✓ Video calls — multi-participant SFU rooms via Mediasoup + Socket.IO
- ✓ Live streaming — WHIP publisher and WHEP viewer over WebRTC
- ✓ Spotlight layout — pinned participant > stable speaker lock (3s, 700ms debounce) > first participant
- ✓ Simulcast — 3 encoding layers at 100 k / 300 k / 900 k bps
- ✓ High-quality audio — Opus stereo, FEC, DTX, 48 kHz; echoCancellation + noiseSuppression + autoGainControl
- ✓ In-call chat — text messages, location sharing, image upload (auto-compressed to 800 px)
- ✓ Real-time stream interactions — floating emoji reactions (SSE), admin chat, stream settings
- ✓ Auto-reconnect — exponential backoff (3–10 s), up to 10 retries, 8 s track-arrival timeout
- ✓ Headless hooks — bring your own UI with
useVideoCall,usePublisher,useViewer,useStreamInteraction - ✓ Drop-in components —
VideoCallRoom,LivePublisher,LiveViewerwith full built-in UI - ✓ JWT-embedded config — server URL, media URL, and ICE servers can be embedded in your auth token
Installation
npm install @truncatetechnologies/streamsdkInstall peer dependencies based on which features you use:
# Video calls (VideoCallRoom / useVideoCall)
npm install react mediasoup-client socket.io-client
# Live streaming only (LivePublisher, LiveViewer, usePublisher, useViewer)
npm install react| Peer dependency | Version | Required for |
|---|---|---|
| react | >= 17.0.0 | All features |
| mediasoup-client | >= 3.0.0 | Video calls |
| socket.io-client | >= 4.0.0 | Video calls |
Quick Start
1. Video call room (drop-in)
import { StreamSDKClient, VideoCallRoom } from '@truncatetechnologies/streamsdk';
const client = new StreamSDKClient({
token: 'your-jwt-from-server',
serverUrl: 'https://api.streamsdk.io',
});
export default function CallPage() {
return (
<VideoCallRoom
client={client}
roomId="room-abc123"
displayName="Alice"
onLeave={() => window.location.href = '/'}
/>
);
}2. Live publisher (drop-in)
import { StreamSDKClient, LivePublisher } from '@truncatetechnologies/streamsdk';
const client = new StreamSDKClient({
token: 'your-jwt-from-server',
serverUrl: 'https://api.streamsdk.io',
});
export default function BroadcastPage() {
return (
<LivePublisher
client={client}
streamId="my-stream-id"
onEnd={() => window.location.href = '/dashboard'}
/>
);
}3. Live viewer (drop-in)
import { StreamSDKClient, LiveViewer } from '@truncatetechnologies/streamsdk';
const client = new StreamSDKClient({
token: 'your-jwt-from-server',
serverUrl: 'https://api.streamsdk.io',
});
export default function WatchPage() {
return (
<LiveViewer
client={client}
streamId="my-stream-id"
/>
);
}Headless Hooks
All drop-in components are built on top of headless hooks. Use the hooks directly when you need full control over the UI.
useVideoCall
import { useVideoCall } from '@truncatetechnologies/streamsdk';
function MyCallUI({ client, roomId }) {
const {
localRef, localStream, remoteStreams,
micOn, camOn, speaking, muteStatus,
toggleMic, toggleCam, leaveCall,
messages, unreadCount, markChatOpen,
sendTextMessage, sendLocation, sendImage,
connected, error,
} = useVideoCall(client, roomId, { displayName: 'Alice', enableChat: true });
return (
<div>
<video ref={localRef} autoPlay playsInline muted />
{Object.entries(remoteStreams).map(([id, stream]) => (
<RemoteVideo key={id} stream={stream} speaking={speaking[id]} />
))}
<button onClick={toggleMic}>{micOn ? 'Mute' : 'Unmute'}</button>
<button onClick={toggleCam}>{camOn ? 'Stop video' : 'Start video'}</button>
<button onClick={leaveCall}>Leave</button>
</div>
);
}usePublisher
import { usePublisher } from '@truncatetechnologies/streamsdk';
function MyPublisherUI({ client, streamId }) {
const {
videoRef, status, loading, elapsed,
micOn, camOn,
startStream, stopStream, toggleMic, toggleCam,
} = usePublisher(client, streamId);
return (
<div>
<video ref={videoRef} autoPlay playsInline muted />
<p>Status: {status} — {elapsed}s</p>
{status !== 'live'
? <button onClick={startStream} disabled={loading}>Go Live</button>
: <button onClick={stopStream}>End Stream</button>
}
</div>
);
}useViewer
import { useViewer } from '@truncatetechnologies/streamsdk';
function MyViewerUI({ client, streamId }) {
const {
videoRef, containerRef, status, elapsed,
muted, isFullscreen,
toggleMute, toggleFullscreen,
} = useViewer(client, streamId);
return (
<div ref={containerRef}>
<video ref={videoRef} autoPlay playsInline muted />
<p>Status: {status}</p>
<button onClick={toggleMute}>{muted ? 'Unmute' : 'Mute'}</button>
<button onClick={toggleFullscreen}>Fullscreen</button>
</div>
);
}useStreamInteraction
import { useStreamInteraction } from '@truncatetechnologies/streamsdk';
function StreamOverlay({ client, streamId }) {
const {
chatMessages, floatingReactions, settings,
sendReaction, sendChat, updateSettings,
} = useStreamInteraction(client, streamId);
return (
<div>
{floatingReactions.map(r => (
<span key={r.id} style={{ left: `${r.x}%` }}>{r.emoji}</span>
))}
<button onClick={() => sendReaction('👍')}>React</button>
<button onClick={() => sendChat('Hello viewers!')}>Send chat</button>
</div>
);
}StreamSDKClient
Configure once and pass to any hook or component.
import { StreamSDKClient } from '@truncatetechnologies/streamsdk';
const client = new StreamSDKClient({
token: 'your-jwt',
serverUrl: 'https://api.streamsdk.io',
mediaUrl: 'https://stream.streamsdk.io',
streamKey: 'optional-ingest-key',
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});Constructor options
| Prop | Type | Default | Description |
|---|---|---|---|
| token | string | '' | JWT auth token. Config embedded in the token payload takes the lowest precedence (see JWT token config). |
| serverUrl | string | 'https://api.streamsdk.io' | Base URL for the API server (signaling, WHIP, WHEP, SSE). Also accepted as apiUrl (alias). |
| mediaUrl | string | 'https://stream.streamsdk.io' | Base URL for the media server. Currently reserved for future use. |
| streamKey | string | '' | Optional ingest key sent as the x-stream-key header on WHIP requests. |
| iceServers | RTCIceServer[] | [{ urls: 'stun:stun.l.google.com:19302' }] | ICE server configuration passed to every RTCPeerConnection. |
API Reference
useVideoCall return values
| Value | Type | Description |
|---|---|---|
| localRef | React.RefObject<HTMLVideoElement> | Attach to the local <video> element. |
| localStream | MediaStream \| null | The local camera/microphone stream. |
| remoteStreams | { [peerId: string]: MediaStream } | Map of remote peer IDs to their streams. |
| micOn | boolean | Whether the local microphone is active. |
| camOn | boolean | Whether the local camera is active. |
| speaking | { [peerId: string]: boolean } | Real-time speaking state per peer. 700 ms on-threshold, 1800 ms off-threshold. |
| muteStatus | { [peerId: string]: boolean } | Mute state broadcast by each remote peer. |
| toggleMic | () => void | Toggle local microphone on/off. |
| toggleCam | () => void | Toggle local camera on/off. |
| leaveCall | () => void | Stop all tracks and disconnect from the room. |
| messages | Message[] | In-call chat history. Each message has type, text, senderName, timestamp, isMe. |
| unreadCount | number | Number of unread messages received while chat is closed. |
| markChatOpen | (open: boolean) => void | Notify the hook that the chat panel is open/closed (resets unread count). |
| sendTextMessage | (text: string) => void | Broadcast a text message to all participants. |
| sendLocation | () => void | Request the browser's geolocation and broadcast coordinates. |
| sendImage | (file: File) => void | Compress and broadcast an image (resized to max 800 px, JPEG 75%). |
| connected | boolean | Whether the SFU transport is fully established. |
| error | Error \| null | Set if the boot sequence fails (e.g. camera denied, SFU unreachable). |
Options passed as the third argument to useVideoCall(client, roomId, options):
| Option | Type | Default | Description |
|---|---|---|---|
| displayName | string | '' | Display name shown to other participants. |
| enableChat | boolean | true | Subscribe to and emit chat events. Set to false to disable all chat functionality. |
usePublisher return values
| Value | Type | Description |
|---|---|---|
| videoRef | React.RefObject<HTMLVideoElement> | Attach to the publisher's <video> element (local preview, mirrored). |
| status | 'idle' \| 'starting' \| 'live' \| 'error' | Current stream state. |
| loading | boolean | true while the WHIP handshake is in progress. |
| elapsed | number | Seconds elapsed since the stream went live. Resets to 0 when stopped. |
| micOn | boolean | Whether the microphone track is enabled. |
| camOn | boolean | Whether the camera track is enabled. |
| startStream | () => Promise<void> | Acquire media, negotiate WHIP, and set status to 'live'. |
| stopStream | () => Promise<void> | Send WHIP DELETE, stop tracks, and reset to 'idle'. |
| toggleMic | () => void | Enable/disable the audio track without renegotiating. |
| toggleCam | () => void | Enable/disable the video track without renegotiating. |
useViewer return values
| Value | Type | Description |
|---|---|---|
| videoRef | React.RefObject<HTMLVideoElement> | Attach to the viewer's <video> element. |
| containerRef | React.RefObject<HTMLDivElement> | Attach to the container element for fullscreen requests. |
| status | 'connecting' \| 'live' \| 'waiting' \| 'reconnecting' \| 'error' | Current playback state. |
| elapsed | number | Seconds elapsed since the stream became live. Resets on reconnect. |
| muted | boolean | Whether the video element is muted. Starts true (required for autoplay). |
| isFullscreen | boolean | Whether the container is currently in fullscreen mode. |
| toggleMute | () => void | Toggle audio mute on the video element. |
| toggleFullscreen | () => void | Enter or exit fullscreen using the Fullscreen API. |
Reconnect behavior: exponential backoff starting at 3 s, capped at 10 s, maximum 10 retries. A 8 s timer fires if the WebRTC connection opens but no media tracks arrive, triggering an automatic reconnect.
useStreamInteraction return values
| Value | Type | Description |
|---|---|---|
| chatMessages | ChatMessage[] | Admin chat messages received via SSE. Each has id, message, senderName, timestamp. Capped at the last 50. |
| floatingReactions | FloatingReaction[] | Active floating emoji animations. Each has id, emoji, x (CSS % from left). Auto-removed after 3.5 s. |
| settings | { reactionsEnabled: boolean } | Stream settings synced in real time from the server. |
| sendReaction | (emoji: string) => void | POST an emoji reaction. Visible to all viewers as a floating animation. |
| sendChat | (message: string) => void | POST a chat message (publisher/admin only). Appears to all viewers. |
| updateSettings | (patch: Partial<Settings>) => void | PATCH stream settings (publisher/admin only). Changes broadcast to all viewers immediately. |
Component Props
VideoCallRoom
<VideoCallRoom
client={client}
roomId="room-abc123"
displayName="Alice"
theme="dark"
chat={true}
onLeave={() => {}}
/>| Prop | Type | Default | Description |
|---|---|---|---|
| client | StreamSDKClient | required | Configured client instance. |
| roomId | string | required | Unique room identifier. All participants with the same ID join the same call. |
| displayName | string | '' | Name shown on your tile and sent in chat messages. |
| theme | 'dark' \| 'light' | 'dark' | Background color scheme. |
| chat | boolean | true | Show the in-call chat panel and controls. |
| onLeave | () => void | window.history.back() | Called after the local user leaves the call. |
Layout behavior:
- 1 participant: full-screen single tile.
- 2 participants: remote participant large, local in a picture-in-picture overlay (tap/click to swap).
- 3+ participants: spotlight tile (pinned > stable speaker > first) with a scrollable filmstrip.
LivePublisher
<LivePublisher
client={client}
streamId="my-stream-id"
onEnd={() => {}}
/>| Prop | Type | Default | Description |
|---|---|---|---|
| client | StreamSDKClient | required | Configured client instance. |
| streamId | string | required | Unique stream identifier. Sent to POST /stream/whip/{streamId}. |
| onEnd | () => void | undefined | Called after the stream is stopped and the WHIP session is deleted. |
Protocol details: H264 codec, max bitrate 1.5 Mbps, 30 fps. Sends x-stream-key header if client.streamKey is set.
LiveViewer
<LiveViewer
client={client}
streamId="my-stream-id"
/>| Prop | Type | Default | Description |
|---|---|---|---|
| client | StreamSDKClient | required | Configured client instance. |
| streamId | string | required | Stream to subscribe to. Sent to POST /stream/whep/{streamId}. |
Status overlays: Shows a spinner for connecting and reconnecting states, a waiting indicator when the stream is offline (404), and an error message if all retries are exhausted.
JWT Token Config
StreamSDKClient reads configuration from the JWT payload so you can provision tokens server-side without hardcoding URLs in your frontend.
Embed a config object in the token's first segment (before signing):
// Server-side token payload
{
"sub": "user-123",
"exp": 1800000000,
"config": {
"apiUrl": "https://api.your-domain.com",
"mediaUrl": "https://stream.your-domain.com",
"iceServers": [
{ "urls": "stun:stun.your-domain.com:3478" },
{
"urls": "turn:turn.your-domain.com:3478",
"username": "user",
"credential": "pass"
}
]
}
}Then initialize the client with only the token:
const client = new StreamSDKClient({ token: tokenFromYourServer });
// client.serverUrl → "https://api.your-domain.com"
// client.mediaUrl → "https://stream.your-domain.com"
// client.iceServers → [...] from tokenPrecedence (highest to lowest):
- Constructor props (
serverUrl,mediaUrl,iceServers) config.*fields inside the JWT payload- Built-in defaults (
https://api.streamsdk.io,https://stream.streamsdk.io, Google STUN)
TypeScript
The SDK ships as compiled JavaScript (ESM + CJS). TypeScript declaration files (.d.ts) are on the roadmap. In the meantime, you can add inline @ts-ignore comments or use JSDoc types where needed.
See https://streamsdk.io for the latest type information as it becomes available.
Links
- Documentation: https://streamsdk.io
- npm: https://www.npmjs.com/package/@truncatetechnologies/streamsdk
- Issues: https://github.com/truncatetechnologies/streamsdk/issues
MIT License. Copyright (c) Truncate Technologies.
