Skip to main content

Video Calls Technical Documentation

Complete technical documentation for implementing video calls in Spritz using Huddle01's WebRTC infrastructure.

Protocol Overview

Spritz uses Huddle01 for decentralized video calls with WebRTC.

ComponentTechnologyPurpose
SignalingHuddle01 API v2Room creation & coordination
Media TransportWebRTCP2P audio/video streams
TopologySFU (Selective Forwarding Unit)Multi-party calls
AuthJWT Access TokensPer-participant authorization

Architecture

┌─────────────────────────────────────────────────────────────┐
│ Video Call Architecture │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Client A │ │ Client B │ │
│ │ ┌────────┐ │ │ ┌────────┐ │ │
│ │ │ Camera │ │ │ │ Camera │ │ │
│ │ │ +Mic │ │ │ │ +Mic │ │ │
│ │ └────┬───┘ │ │ └────┬───┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌────▼───┐ │ │ ┌────▼───┐ │ │
│ │ │ WebRTC │ │ │ │ WebRTC │ │ │
│ │ │ Peer │ │ │ │ Peer │ │ │
│ │ └────┬───┘ │ │ └────┬───┘ │ │
│ └───────┼──────┘ └───────┼──────┘ │
│ │ │ │
│ └──────────┬─────────────┘ │
│ │ │
│ ┌─────▼─────┐ │
│ │ Huddle01 │ │
│ │ SFU │ │
│ │ Server │ │
│ └───────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Room Management

Create Room

// POST /api/huddle01/room
const HUDDLE01_API_KEY = process.env.HUDDLE01_API_KEY;

export async function POST(request: NextRequest) {
const { title, hostWallet } = await request.json();

const response = await fetch(
"https://api.huddle01.com/api/v2/sdk/rooms/create-room",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": HUDDLE01_API_KEY,
},
body: JSON.stringify({
roomLocked: false,
metadata: {
title: title || "Spritz Call",
hostWallets: hostWallet ? [hostWallet] : [],
},
}),
}
);

const data = await response.json();
return NextResponse.json({ roomId: data.data.roomId });
}

Room Response

interface RoomResponse {
data: {
roomId: string; // e.g., "abc-defg-hijk"
};
}

Token Generation

Access tokens authorize participants to join rooms with specific permissions.

Token Structure

import { AccessToken, Role } from "@huddle01/server-sdk/auth";

const accessToken = new AccessToken({
apiKey: HUDDLE01_API_KEY,
roomId: roomId,
role: Role.HOST, // or Role.GUEST, Role.CO_HOST
permissions: {
admin: true, // Can manage room
canConsume: true, // Can receive media
canProduce: true, // Can send media
canProduceSources: {
cam: true, // Camera permission
mic: true, // Microphone permission
screen: true, // Screen share permission
},
canRecvData: true, // Receive data channels
canSendData: true, // Send data channels
canUpdateMetadata: true,// Update participant metadata
},
options: {
metadata: {
displayName: displayName,
walletAddress: userAddress,
},
},
});

const jwt = await accessToken.toJwt();

Token API Endpoint

// POST /api/huddle01/token
export async function POST(request: NextRequest) {
const { roomId, userAddress, displayName } = await request.json();

if (!roomId || !userAddress) {
return NextResponse.json(
{ error: "roomId and userAddress are required" },
{ status: 400 }
);
}

const accessToken = new AccessToken({
apiKey: HUDDLE01_API_KEY,
roomId: roomId,
role: Role.HOST,
permissions: {
admin: true,
canConsume: true,
canProduce: true,
canProduceSources: { cam: true, mic: true, screen: true },
canRecvData: true,
canSendData: true,
canUpdateMetadata: true,
},
options: {
metadata: {
displayName: displayName || userAddress.slice(0, 10),
walletAddress: userAddress,
},
},
});

const token = await accessToken.toJwt();
return NextResponse.json({ token });
}

Client Integration

React Hook

// hooks/useHuddle01Call.ts
import { useRoom, useLocalVideo, useLocalAudio } from "@huddle01/react/hooks";

export function useHuddle01Call() {
const { joinRoom, leaveRoom, state: roomState } = useRoom();
const { stream: videoStream, enableVideo, disableVideo, isVideoOn } = useLocalVideo();
const { stream: audioStream, enableAudio, disableAudio, isAudioOn } = useLocalAudio();

const startCall = async (roomId: string, token: string) => {
await joinRoom({ roomId, token });
};

const endCall = async () => {
await leaveRoom();
};

const toggleVideo = () => {
isVideoOn ? disableVideo() : enableVideo();
};

const toggleAudio = () => {
isAudioOn ? disableAudio() : enableAudio();
};

return {
roomState,
videoStream,
audioStream,
isVideoOn,
isAudioOn,
startCall,
endCall,
toggleVideo,
toggleAudio,
};
}

Call Component

import { Video, Audio } from "@huddle01/react/components";

function VideoCall({ roomId }: { roomId: string }) {
const {
roomState,
videoStream,
isVideoOn,
isAudioOn,
startCall,
endCall,
toggleVideo,
toggleAudio,
} = useHuddle01Call();

useEffect(() => {
const initCall = async () => {
// Get token from API
const res = await fetch("/api/huddle01/token", {
method: "POST",
body: JSON.stringify({ roomId, userAddress }),
});
const { token } = await res.json();

await startCall(roomId, token);
};

initCall();
return () => { endCall(); };
}, [roomId]);

return (
<div className="video-call">
{/* Local video preview */}
{videoStream && (
<Video stream={videoStream} className="local-video" />
)}

{/* Remote participants */}
<RemoteParticipants />

{/* Controls */}
<div className="controls">
<button onClick={toggleVideo}>
{isVideoOn ? "Camera Off" : "Camera On"}
</button>
<button onClick={toggleAudio}>
{isAudioOn ? "Mute" : "Unmute"}
</button>
<button onClick={endCall}>End Call</button>
</div>
</div>
);
}

WebRTC Configuration

ICE Configuration

Huddle01 manages ICE servers automatically. No manual STUN/TURN configuration required.

Codec Support

Media TypeCodecBitrate Range
VideoVP8, VP9500kbps - 2.5Mbps
AudioOpus32kbps - 128kbps

Network Adaptation

Huddle01's SFU automatically handles:

  • Bandwidth estimation
  • Resolution scaling (simulcast)
  • Packet loss recovery
  • Jitter buffering

Permissions

Role Hierarchy

RolePermissions
HOSTFull control, can manage room
CO_HOSTCan moderate, but not close room
GUESTCan participate, limited control
LISTENERCan only receive, no producing

Permission Matrix

PermissionHOSTCO_HOSTGUESTLISTENER
admin
canConsume
canProduce
canProduceSources.cam
canProduceSources.mic
canProduceSources.screen
canSendData
canRecvData
canUpdateMetadata

Call Types

1:1 Calls

// Direct call to a friend
async function startDirectCall(friendAddress: string) {
// Create room
const roomRes = await fetch("/api/huddle01/room", {
method: "POST",
body: JSON.stringify({
title: `Call with ${friendAddress}`,
hostWallet: myAddress,
}),
});
const { roomId } = await roomRes.json();

// Notify friend (via messaging)
await sendMessage(friendAddress, {
type: "call_invite",
roomId,
});

// Join room
await joinCall(roomId);
}

Group Calls

// Group call (channel or group chat)
async function startGroupCall(groupId: string, members: string[]) {
const roomRes = await fetch("/api/huddle01/room", {
method: "POST",
body: JSON.stringify({
title: `Group Call`,
hostWallet: myAddress,
}),
});
const { roomId } = await roomRes.json();

// Notify all members
for (const member of members) {
await sendMessage(member, {
type: "group_call_invite",
roomId,
groupId,
});
}

await joinCall(roomId);
}

Scheduled Calls

Room Persistence

Permanent rooms can be created for scheduled meetings:

// Create permanent room
const response = await fetch("/api/rooms/permanent", {
method: "POST",
body: JSON.stringify({
title: "Weekly Standup",
code: "weekly-standup-abc",
}),
});

// Store room code for later access
const { roomId, code } = await response.json();

Join via Code

// Join existing room by code
const joinUrl = `https://app.spritz.chat/room/${code}`;

Error Handling

Common Errors

ErrorCauseSolution
Token expiredJWT past expirationGenerate new token
Room not foundInvalid roomIdCreate new room
Permission deniedInsufficient roleCheck token permissions
Media access deniedBrowser blocked camera/micRequest user permission

Error Recovery

async function joinCallWithRetry(roomId: string, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const token = await getToken(roomId);
await joinRoom({ roomId, token });
return;
} catch (error) {
if (error.message.includes("Token expired")) {
// Retry with fresh token
continue;
}
throw error;
}
}
throw new Error("Failed to join call after retries");
}

Media Quality Settings

Video Quality Presets

const VIDEO_PRESETS = {
low: { width: 320, height: 180, frameRate: 15 },
medium: { width: 640, height: 360, frameRate: 24 },
high: { width: 1280, height: 720, frameRate: 30 },
};

// Request specific quality
await enableVideo({
constraints: VIDEO_PRESETS.medium,
});

Adaptive Quality

// Huddle01 automatically adjusts quality based on:
// - Network bandwidth
// - CPU usage
// - Number of participants

Security

Call Encryption

All WebRTC media is encrypted using:

  • DTLS: Key exchange
  • SRTP: Media encryption

Room Security

FeatureImplementation
Access ControlJWT tokens with expiration
Host VerificationWallet address in metadata
Room LockingroomLocked: true option

Best Practices

Performance

  1. Disable video when not needed (audio-only mode)
  2. Use simulcast for multi-party calls (automatic)
  3. Limit participants to 10-15 for optimal quality
  4. Monitor network and adjust quality dynamically

UX

  1. Show connection state (connecting, connected, reconnecting)
  2. Indicate mute status clearly
  3. Provide fallback for media access errors
  4. Test microphone/camera before joining

Integration

// Complete call flow
const callFlow = {
1: "Create room (server-side)",
2: "Generate tokens for participants",
3: "Join room with token",
4: "Enable media (camera/mic)",
5: "Handle participant events",
6: "Leave room on disconnect",
};