Skip to main content

5-Minute Quickstart — WebRTC with @metered-ca/realtime

Two browser tabs talking over peer-to-peer WebRTC, no backend, no JWT minting, no manual TURN config. Save one HTML file, replace one key, open in two tabs.

1. Get a publishable key

Go to dashboard.metered.ca → Realtime Messaging → Keys → Create key, pick type Publishable.

Under "What can this key do?", make sure Send is checked. It's off by default for publishable keys (because it lets any browser holding the key direct-message every peer), but the WebRTC layer of @metered-ca/realtime uses it under the hood to exchange SDP and ICE candidates between peers. Without Send, peer.join() and peer.addStream() connect successfully but the video never negotiates — peers never establish their RTCPeerConnection.

The other three checkboxes (Subscribe, Publish, Presence) are on by default and should stay on.

Copy the pk_live_… value when the key is shown.

2. Save this HTML file

<!DOCTYPE html>
<html>
<body>
<video id="local" autoplay playsinline muted style="width:300px"></video>
<div id="remotes"></div>

<script src="https://unpkg.com/@metered-ca/realtime@1/dist/index.umd.js"></script>
<script>
const { MeteredPeer } = window.MeteredPeer;
const peer = new MeteredPeer({ apiKey: "pk_live_REPLACE_ME" });

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

remote.on("stream-added", ({ stream }) => {
tile.srcObject = stream;
});
});

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

(async () => {
const localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
document.getElementById("local").srcObject = localStream;
peer.addStream(localStream);
await peer.join("my-first-room");
})();
</script>
</body>
</html>

Replace pk_live_REPLACE_ME with your key.

3. Serve it locally + open two tabs

Any static server works. Browsers block getUserMedia on plain http:// from non-localhost, so use localhost or HTTPS:

npx serve .
# or
python3 -m http.server 3000

Open http://localhost:3000/ in two browser tabs. Allow camera + mic in both. Each tab shows its own video on top, the other tab's video underneath. That's a working WebRTC call.

What just happened

In ~25 lines of JavaScript:

  • new MeteredPeer({ apiKey }) connected to the Metered Realtime Messaging service over WebSocket.
  • peer.join("my-first-room") subscribed to a channel and asked the server who else was there.
  • Each tab discovered the other via presence → fired peer-joined.
  • peer.addStream(localStream) fanned the camera + mic out over peer-to-peer WebRTC. The SDK handled SDP exchange, ICE trickle, perfect negotiation, and applied auto-injected TURN credentials so it works behind symmetric NAT.
  • remote.on("stream-added", …) fired when the other tab's video arrived.

That's the whole 80%-case API. Add channels, peers, mute buttons, screen share — same shape.

Next steps

What you wantRead
Mute, camera-off, screen-share, per-user identityWebRTC Video Call guide (uses JWT path for per-user permissions)
Production reconnect handling (banners, stuck-timeout)Reconnect Best Practicesrequired reading before you ship
Stable per-user peerIds, custom peerMetadataAuthentication — pk_ vs JWT
Low-latency P2P data (gaming, large transfers)Data Channels & Low Latency
Migrating from simple-peer or PeerJSMigration from simple-peer · from PeerJS
Building chat / pub/sub without WebRTC5-Min Quickstart — Pub/Sub (raw WebSocket, any stack)

Or: try the live demo first

If you'd rather see it working before writing any code: live demo →. Same code, hosted. Open in two tabs.