Skip to main content

Example — Basic Video Call

The simplest possible browser video call. Open two tabs to the same HTML file — they discover each other via presence and exchange video.

npm: @metered-ca/realtime

What it demonstrates

  • MeteredPeer constructor with apiKey (publishable key, no backend)
  • peer.addStream(localStream) fans your camera/mic to every peer
  • peer-joined / peer-left event handling — creating + removing video tiles
  • remote.on("track", …) attaching incoming media to a <video> element
  • peer.state-change for a "connecting…" / "connected" indicator

Running it locally

Save the HTML + JS from the walkthrough below into index.html, replace pk_live_… with your own publishable key, then serve it over HTTPS or localhost (browsers block getUserMedia on plain http://).

Tick Send when creating the publishable key

Send is off by default for publishable keys, but the WebRTC layer of @metered-ca/realtime uses it under the hood to exchange SDP and ICE candidates between peers. Without it, peer.join() succeeds but no RTCPeerConnection ever negotiates and video never appears. Under "What can this key do?" leave Subscribe, Publish, Presence checked AND tick Send before saving.

# Any static server works
npx serve .
# or:
python3 -m http.server 3000

Open http://localhost:3000/ in two browser tabs. Allow camera access in both. Each tab shows its own local video + a tile for the other tab.

Source walkthrough

The example is a single HTML file with inline JS. Key parts:

Loading the SDK from a CDN

<script src="https://unpkg.com/@metered-ca/realtime/dist/index.umd.js"></script>
<script>
const { MeteredPeer } = window.MeteredPeer;
</script>

For a real app, install the npm package and bundle. The CDN form is for quick demos.

Constructing the peer

const peer = new MeteredPeer({
apiKey: "pk_live_…",
// url: "wss://rms.metered.ca" // default
});

pk_live_… keys work for demos. For production, switch to tokenProvider so per-user permissions and TURN credentials come from your backend — see the Authentication guide and the main WebRTC guide.

Getting local media and attaching

const local = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
document.querySelector("#local").srcObject = local;
peer.addStream(local);

Two important details:

  • addStream is called before join. This way the SDK can include the local tracks in the very first SDP offer to each peer — one round trip instead of two.
  • <video autoplay playsinline muted> is required. muted so we don't echo our own audio back at us; playsinline so it works on mobile Safari.

Wiring up remote peers

peer.on("peer-joined", ({ peer: remote }) => {
const tile = document.createElement("video");
tile.autoplay = true;
tile.playsInline = true;
tile.dataset.peerId = remote.id;
document.querySelector("#remotes").appendChild(tile);

remote.on("track", ({ streams }) => {
tile.srcObject = streams[0];
});
});

peer.on("peer-left", ({ peer: remote }) => {
document.querySelector(`[data-peer-id="${remote.id}"]`)?.remove();
});

The remote.on("track", …) callback fires once per inbound track (one for video, one for audio). The streams[0] is the MediaStream the remote attached — using it directly preserves track grouping.

Joining the channel

await peer.join("demo-room");

That's it. After join resolves:

  • Anyone already in demo-room triggers peer-joined on your peer
  • Anyone joining later triggers it too
  • Your camera + mic stream is sent to all of them

What this example doesn't cover

For a production app, layer in:

FeatureWhere it's documented
Mute, camera-off, screen-shareWebRTC Video Call guide
Reconnect UI (banner, stuck-timeout)Reconnect Best Practices
Chat / control messagesPresence & Chat
Per-user identity (peerMetadata, peerId)Authentication
Embedded TURN credentialsWebRTC Video Call guide

The point of this example is to show that two tabs and ~30 lines of JS get you a working call. Layer the rest on once that works.