Media Rooms
Media Rooms let you add video and audio calling to your application. Your server creates rooms and generates access tokens; clients connect directly to the media server using any WebRTC-compatible library.
import { getPlatform } from '@maravilla-labs/platform';
const platform = getPlatform();
// Create a room and generate a token for a participant
const room = await platform.media.createRoom('standup', { maxParticipants: 10 });
const { token, url } = await platform.media.generateToken('standup', {
identity: 'alice',
name: 'Alice',
});
How It Works
- Your server-side code uses
platform.media.generateToken()to create a token — this is where you check if the user is allowed to join - The browser receives the token from your server and connects directly to the SFU (Selective Forwarding Unit) — no manual WebRTC signaling needed
- The SFU handles all media routing: video, audio, and screen sharing between participants
Each project gets its own isolated set of rooms. Tokens are scoped to a specific room and participant identity.
Token generation always happens server-side so you control who can join which rooms.
| Endpoint | Method | Purpose |
|---|---|---|
/_rt/ws | GET | WebSocket for realtime channels |
/_rt/rooms | GET | List active media rooms |
Server-Side API
Access media features through platform.media in your server-side code.
createRoom(roomId, settings?)
Creates a new media room. If a room with the same ID already exists, returns the existing room.
Parameters:
roomId(string) — unique room identifier (e.g.standup,interview-42)settings(object, optional):maxParticipants(number) — maximum number of concurrent participants (default: 20)emptyTimeout(number) — seconds before an empty room is automatically deleted (default: 300)
Returns: { roomId, maxParticipants, emptyTimeout, createdAt }
// Create a room with defaults
const room = await platform.media.createRoom('team-standup');
// Create a room with limits
const room = await platform.media.createRoom('webinar', {
maxParticipants: 100,
emptyTimeout: 600,
});
deleteRoom(roomId)
Deletes a room and disconnects all participants. No error is thrown if the room does not exist.
Parameters:
roomId(string) — the room to delete
await platform.media.deleteRoom('team-standup');
listRooms()
Returns all active rooms for the current project.
Returns: Array of room objects with roomId, numParticipants, and createdAt.
const rooms = await platform.media.listRooms();
// [
// { roomId: 'standup', numParticipants: 3, createdAt: 1710000000 },
// { roomId: 'interview-42', numParticipants: 2, createdAt: 1710000500 }
// ]
generateToken(roomId, participant)
Generates an access token that allows a participant to join a specific room. Tokens are short-lived and scoped to one room.
Parameters:
roomId(string) — the room the participant will joinparticipant(object):identity(string) — unique participant identifier (e.g. user ID)name(string) — display name shown to other participantscanPublish(boolean, optional) — whether the participant can publish audio/video (default: true)canSubscribe(boolean, optional) — whether the participant can receive others’ media (default: true)
Returns: { token, url }
token— the access token to pass to the clienturl— the media server WebSocket URL
// Full participant (can send and receive)
const { token, url } = await platform.media.generateToken('standup', {
identity: 'alice',
name: 'Alice',
});
// View-only participant (can only receive)
const { token, url } = await platform.media.generateToken('webinar', {
identity: 'viewer-bob',
name: 'Bob',
canPublish: false,
});
mediaUrl()
Returns the media server URL for the current environment. Useful when you need the URL separately from token generation.
Returns: string — the media server WebSocket URL.
const url = await platform.media.mediaUrl();
// Development: "ws://localhost:7880"
// Production: "wss://media.yourapp.maravilla.page"
Client-Side Integration
Use MediaRoom from the platform package to connect to rooms in the browser:
import { MediaRoom, MediaRoomEvent, attachTrack } from '@maravilla-labs/platform';
const room = new MediaRoom();
room.on(MediaRoomEvent.TrackSubscribed, (track, participant) => {
// Attach remote audio/video to the DOM
const el = document.createElement(track.kind === 'video' ? 'video' : 'audio');
attachTrack(track, el);
document.getElementById('videos').appendChild(el);
});
room.on(MediaRoomEvent.ParticipantLeft, (participant) => {
console.log(`${participant.identity} left the room`);
});
// Connect with the token and URL from your server
await room.connect(url, token);
// Publish your camera and microphone
await room.localParticipant.enableCamera();
await room.localParticipant.enableMicrophone();
Device Selection
Let users pick their camera, microphone, or speaker:
// List available devices
const cameras = await MediaRoom.getCameras();
const mics = await MediaRoom.getMicrophones();
const speakers = await MediaRoom.getSpeakers();
// Switch to a specific device
await room.switchCamera(cameras[1].deviceId);
await room.switchMicrophone(mics[0].deviceId);
await room.switchSpeaker(speakers[0].deviceId);
// Enable camera with a specific device
await room.localParticipant.enableCamera({ deviceId: cameras[0].deviceId });
// Enable camera with custom resolution
await room.localParticipant.enableCamera({
resolution: { width: 1280, height: 720, frameRate: 30 },
});
Screen Sharing
// Start screen share (optionally with audio)
await room.localParticipant.enableScreenShare({ audio: true });
// Stop screen share
await room.localParticipant.disableScreenShare();
All Events
room.on(MediaRoomEvent.Connected, () => { });
room.on(MediaRoomEvent.Reconnecting, () => { });
room.on(MediaRoomEvent.Reconnected, () => { });
room.on(MediaRoomEvent.Disconnected, (reason) => { });
room.on(MediaRoomEvent.ParticipantJoined, (participant) => { });
room.on(MediaRoomEvent.ParticipantLeft, (participant) => { });
room.on(MediaRoomEvent.TrackSubscribed, (track, participant) => { });
room.on(MediaRoomEvent.TrackUnsubscribed, (track, participant) => { });
room.on(MediaRoomEvent.TrackMuted, (participant) => { });
room.on(MediaRoomEvent.TrackUnmuted, (participant) => { });
room.on(MediaRoomEvent.ActiveSpeakersChanged, (speakers) => { });
room.on(MediaRoomEvent.DataReceived, (data, participant) => { });
room.on(MediaRoomEvent.RecordingStatusChanged, (isRecording) => { });
room.on(MediaRoomEvent.MediaDevicesChanged, () => { });
Example: Video Call
A complete video call using SvelteKit. The server route handles auth and generates tokens; the client connects and manages media.
Server Route
// src/routes/api/rooms/[roomId]/join/+server.ts
import { json, error } from '@sveltejs/kit';
import { getPlatform } from '@maravilla-labs/platform';
const platform = getPlatform();
export async function POST({ params, request, locals }) {
// Your auth logic — control who can join
if (!locals.user) throw error(401, 'Not authenticated');
// Create room if needed + generate token
await platform.media.createRoom(params.roomId, { maxParticipants: 10 });
const { token, url } = await platform.media.generateToken(params.roomId, {
identity: locals.user.id,
name: locals.user.name,
});
return json({ token, url });
}
Client Page
// src/routes/call/[roomId]/+page.svelte (script section)
import { MediaRoom, MediaRoomEvent, attachTrack, detachTrack } from '@maravilla-labs/platform';
import { onMount, onDestroy } from 'svelte';
import { page } from '$app/stores';
const room = new MediaRoom();
onMount(async () => {
const roomId = $page.params.roomId;
// Get token from your server (which handles auth)
const { token, url } = await fetch(`/api/rooms/${roomId}/join`, {
method: 'POST',
}).then(r => r.json());
// Handle remote tracks
room.on(MediaRoomEvent.TrackSubscribed, (track, participant) => {
const el = document.createElement(track.kind === 'video' ? 'video' : 'audio');
el.dataset.participantId = participant.identity;
attachTrack(track, el);
document.getElementById('remote-videos').appendChild(el);
});
room.on(MediaRoomEvent.TrackUnsubscribed, (track, participant) => {
document
.querySelectorAll(`[data-participant-id="${participant.identity}"]`)
.forEach((el) => el.remove());
});
room.on(MediaRoomEvent.ParticipantLeft, (participant) => {
document
.querySelectorAll(`[data-participant-id="${participant.identity}"]`)
.forEach((el) => el.remove());
});
// Connect and publish local media
await room.connect(url, token);
await room.localParticipant.enableCamera();
await room.localParticipant.enableMicrophone();
});
onDestroy(() => {
room.disconnect();
});
Development vs. Production
| Development | Production | |
|---|---|---|
| Media server | ws://localhost:7880 | wss://media.yourapp.maravilla.page |
| Detection | Automatic (dev server routes to local media server) | Automatic (token includes production URL) |
| Setup | Works out of the box with maravilla dev | Works out of the box on Maravilla Cloud |
The generateToken response always includes the correct url for the current environment. Your code works the same in both.
Limits
| Free | Builder | Pro | Enterprise | |
|---|---|---|---|---|
| Max rooms | 5 | 20 | 100 | Unlimited |
| Max participants per room | 10 | 25 | 50 | Unlimited |
| Media feature | — | Included | Included | Included |
Next Steps
- Realtime Channels — pub/sub messaging, presence tracking, and WebSocket API
- KV Store — store participant metadata, room state, or chat history
- Database — persist call logs and room metadata