Build a video meeting

Set up a Zoom-style small-group call with real OpenRTC rooms and media

Try a real room

The call below uses OpenRTC({ apiKey }), rtc.rooms.join(), and connection.media.addTrack() against the configured OpenRTC service. It requests your camera and microphone only after you select Join. Open this page in a second browser or device, use the same room code and app key, and watch the remote tile appear. Peer tiles come only from admitted OpenRTC connections; there is no simulated local-tab fallback.

Enable JavaScript to join the interactive video room. Camera and microphone access is requested by the demo when you join.

The hosted docs demo uses its own public rooms key, configured for the docs origin. In a local or self-hosted build without that key, enter your public OpenRTC key. Register the exact page origin shown above in your app's Developer Portal Security settings, enable capability rooms with client-open room creation, and set a room capacity and credit budget that admit your test. A public API key is browser-safe; never put a server secret or product auth assertion in this page. The docs demo key is scoped to this demo app and cannot be reused as your product identity.

This is a small-group mesh meeting. The demo requests at most eight members, including the local participant; the portal manifest and server admission can set a lower ceiling. Each participant sends a microphone and camera track to each connected peer, so upload and encode work grow with the peer count. Use Live broadcasting for one-to-many viewing. A larger meeting with server-mixed audio/video, recording, moderation, or waiting rooms requires additional product services; changing maxPeers does not add those features or bypass admission limits.

Set up your own app

  1. Create an app in the Developer Portal and copy its public pk_test_… or pk_live_… key.
  2. Enable capability rooms and client-open room creation. Keep membership ephemeral for calls. Choose a reviewed room limit and budget for the expected simultaneous callers.
  3. Add each exact browser origin to the app allowlist, for example https://meet.example.com and http://localhost:5173 during development. The origin includes the scheme and port, but no path. A pk_test_… key does not select an internal OpenRTC staging service.
  4. Serve the app over HTTPS or localhost. Check getUserMedia, WebCodecs, browser track processing/rendering support, and device permissions before showing an enabled Join control. Test with two independent browsers or devices on different networks.

Install the published package in a standalone web app:

pnpm add openrtc

Keep the public key in a browser-public build variable such as VITE_OPENRTC_API_KEY. The default SDK target is the managed production OpenRTC platform for both development and deployed consumer apps.

Join and publish

The following is the central part of the live demo. It uses a separate MediaStream for each peer, so one peer's tracks cannot overwrite another's. The room code is a rendezvous name, not an access-control secret. Use access: 'authenticated' with your registered assertion provider when your product needs an invite list, roles, or a private meeting policy.

import { OpenRTC } from 'openrtc';

const rtc = OpenRTC({ apiKey: import.meta.env.VITE_OPENRTC_API_KEY });
const local = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
localVideo.srcObject = local; // HTMLVideoElement with autoplay, muted, playsinline

const room = await rtc.rooms.join(roomCode, {
  access: 'capability',
  membership: 'ephemeral',
  identity: 'session',
  architecture: 'mesh',
  payload: 'reliable',
  maxPeers: 8,
});

const remoteStreams = new Map<string, MediaStream>();
const stopConnections = room.onConnection(async (connection) => {
  const remote = new MediaStream();
  remoteStreams.set(connection.peerId, remote);
  addRemoteVideoTile(connection.peerId, remote); // autoplay, playsinline; not muted

  const stopTracks = connection.media.onTrack(({ track }) => {
    for (const old of remote.getTracks()) {
      if (old.kind === track.kind) remote.removeTrack(old);
    }
    remote.addTrack(track);
  });

  connection.onClose(() => {
    stopTracks();
    remoteStreams.delete(connection.peerId);
    removeRemoteVideoTile(connection.peerId);
  });

  for (const track of local.getTracks()) {
    await connection.media.addTrack(track);
  }
});

onConnection() includes current and future logical peers. Do not redial or republish on a carrier change: OpenRTC owns physical route recovery and reopens active publications. Render peer arrival, media, and removal as UI events. The onTrack() event gives an ordinary MediaStreamTrack; attach it to an HTML media element. A remote audio-only participant still needs an unmuted playback element.

Controls and cleanup

Set the local track's enabled property to mute or hide it for every peer. Keep the returned MediaSender if you need replaceTrack() for camera switching or screen share, stop() to end a publication, or getStats() for local media diagnostics. A screen-share track should also be stopped when the browser's sharing session ends. For example:

const microphone = local.getAudioTracks()[0];
microphone.enabled = false; // mute across this room's peer publications

// On Leave: stop observing, release hardware, retire room, close the client.
stopConnections();
local.getTracks().forEach((track) => track.stop());
await room.leave();
await rtc.close();

Do not use a BroadcastChannel, a second RTCPeerConnection, polling, or an app-level retry loop to make the room appear connected. Those would give the UI a different membership or media lifecycle from the admitted OpenRTC room. For API details and codec support, see Media, streams, and datagrams and Rooms API.