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

  1. Your server-side code uses platform.media.generateToken() to create a token — this is where you check if the user is allowed to join
  2. The browser receives the token from your server and connects directly to the SFU (Selective Forwarding Unit) — no manual WebRTC signaling needed
  3. 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.

EndpointMethodPurpose
/_rt/wsGETWebSocket for realtime channels
/_rt/roomsGETList 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 join
  • participant (object):
    • identity (string) — unique participant identifier (e.g. user ID)
    • name (string) — display name shown to other participants
    • canPublish (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 client
  • url — 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

DevelopmentProduction
Media serverws://localhost:7880wss://media.yourapp.maravilla.page
DetectionAutomatic (dev server routes to local media server)Automatic (token includes production URL)
SetupWorks out of the box with maravilla devWorks 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

FreeBuilderProEnterprise
Max rooms520100Unlimited
Max participants per room102550Unlimited
Media featureIncludedIncludedIncluded

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