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
MeteredPeerconstructor withapiKey(publishable key, no backend)peer.addStream(localStream)fans your camera/mic to every peerpeer-joined/peer-leftevent handling — creating + removing video tilesremote.on("track", …)attaching incoming media to a<video>elementpeer.state-changefor 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://).
Send when creating the publishable keySend 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:
addStreamis called beforejoin. 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.mutedso we don't echo our own audio back at us;playsinlineso 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-roomtriggerspeer-joinedon 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:
| Feature | Where it's documented |
|---|---|
| Mute, camera-off, screen-share | WebRTC Video Call guide |
| Reconnect UI (banner, stuck-timeout) | Reconnect Best Practices |
| Chat / control messages | Presence & Chat |
Per-user identity (peerMetadata, peerId) | Authentication |
| Embedded TURN credentials | WebRTC 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.