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/peer

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://):

# 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:

  • 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.