Skip to main content

Getting Started

Ten minutes from pub get to two devices talking. Run it on two emulators, two browser tabs (flutter run -d chrome), or a phone + an emulator.

Install

Add metered_realtime and flutter_webrtc to your pubspec.yaml:

pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_webrtc: ^1.4.1
metered_realtime: ^0.1.0
flutter pub get

The SDK needs Dart ^3.5.0 / Flutter >=3.3.0.

Configure platforms before you call getUserMedia

WebRTC needs camera/microphone permissions declared per platform — without them your first getUserMedia() throws. Do Platform Setup first (it's a 5-minute, one-time step per platform).

Get a key from the dashboard

  1. Sign up (skip if you already have an account).
  2. Dashboard → Realtime Messaging → Keys → Create key.
  3. Pick a key type:
Key typeWhen to useWhere it goes
pk_live_… publishablePrototypes, no per-user scopingDirectly in client code
Secret key (sk_id_… + sk_secret_…)Production apps with per-user permissions, custom peerId, embedded TURN credsServer-side only — your backend mints JWTs

For the rest of this page we'll use the pk_live_ path so there's no backend to run. See Authentication when you're ready to move to JWT minting.

pk_ + WebRTC needs the Send permission

When you create a publishable key, Subscribe / Publish / Presence are on by default but Send is OFF — because the key ships in your app and Send lets anyone holding it direct-message any peer. MeteredPeer's WebRTC layer carries SDP + ICE between peers over the wire protocol's send op, so for pk_ + WebRTC, tick Send when creating the key. Without it, join() succeeds and presence fires, but onPeerJoined → onStreamAdded never fires because no RTCPeerConnection ever negotiates. (Pub/sub-only SignallingClient use doesn't need Send unless you call client.send(...) directly.)

Your first connection

SignallingClient is the smaller surface — pub/sub, no WebRTC. Good for chat, telemetry, anywhere you don't need peer-to-peer media.

import 'package:metered_realtime/metered_realtime.dart';

final client = SignallingClient(
const SignallingClientOptions(apiKey: 'pk_live_…'),
);

client.onConnected.listen((e) {
print('connected as ${e.peerId}');
});

client.onMessage.listen((m) {
print('${m.from} → ${m.channel}: ${m.data}');
});

await client.connect();
await client.subscribe('room-42');
await client.publish('room-42', {'hello': 'world'});

Run this on two devices. Each gets a different peerId; one device's publish shows up as an onMessage event on the other.

That's the whole pub/sub model: subscribe to channels, publish to channels, receive onMessage events for everything you've subscribed to.

Add peers (WebRTC)

MeteredPeer is the same thing plus channel-driven peer discovery + per-peer RTCPeerConnection lifecycle + media fan-out. Use it when peers exchange media (video / audio / screen share) or want low-latency P2P data.

The Flutter media flow has two wrinkles vs the browser:

  1. Outbound: wrap your flutter_webrtc MediaStream with wrapMediaStream(...) before handing it to the SDK.
  2. Inbound: the SDK gives you a MediaStreamLike; unwrap it with .native and render it through an RTCVideoRenderer + RTCVideoView.
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:metered_realtime/metered_realtime.dart';

final peer = MeteredPeer(MeteredPeerOptions(apiKey: 'pk_live_…'));

// One renderer per remote peer.
final renderers = <String, RTCVideoRenderer>{};

peer.onPeerJoined.listen((remote) async {
final renderer = RTCVideoRenderer();
await renderer.initialize();
renderers[remote.id] = renderer;

remote.onStreamAdded.listen((ev) {
// Re-bind on EVERY event — a reconnect surfaces a fresh stream (same id).
renderer.srcObject = (ev.stream as FlutterWebrtcMediaStream).native;
setState(() {}); // your UI rebuild
});
});

peer.onPeerLeft.listen((remote) async {
await renderers.remove(remote.id)?.dispose();
setState(() {});
});

peer.onData.listen((m) {
print('${m.senderPeerId} said: ${m.data}');
});

await peer.join('my-room');

// Capture and publish the local camera + mic.
final localStream = await navigator.mediaDevices.getUserMedia({
'audio': true,
'video': true,
});
await peer.addStream(wrapMediaStream(localStream));

await peer.send({'hi': 'everyone'});

Render each RTCVideoRenderer with an RTCVideoView(renderer). The WebRTC Video Call guide has the full widget.

What this does:

  1. join('my-room') connects the WebSocket, subscribes you to my-room, and asks the server who else is here.
  2. For every peer the server reports, onPeerJoined fires and the SDK opens a peer-to-peer RTCPeerConnection under the hood.
  3. The local stream you addStream'd is sent to every peer; you receive theirs on onStreamAdded.
  4. peer.send(data) broadcasts to everyone in the channel; peer.sendTo(remoteId, data) targets one peer.

No SDP, no ICE, no manual TURN config — Metered TURN is auto-injected into the welcome message when the key has auto-inject enabled (default).

Messages are server-routed by default

peer.send(data) is server-routed, not P2P over a WebRTC DataChannel. It works before ICE completes, but it goes Device → Metered server → Device, so:

  • The same call broadcasts to the channel or directs to one peer
  • It works even before the peer connection finishes negotiating
  • Each message counts against your signalling-message quota

If you need low-latency P2P data (gaming, large transfers), open a real data channel via the remote.pc escape hatch and wrap it with DataChannel.

Next step depends on what you're building

You're building…Read
A WebRTC video callGuide: WebRTC Video Call
Anything before it shipsPlatform Setup + Reconnect Best Practices
Chat / classroom / a lobbyGuide: Presence & Chat
Low-latency P2P dataGuide: Data Channels & Low Latency
Per-user JWTs (custom peerId, channel permissions, TURN creds)Guide: Authentication
Coming from raw flutter_webrtcMigration: from flutter_webrtc