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/peer
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://):
# 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/peer/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.