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:
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.
getUserMediaWebRTC 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
- Sign up (skip if you already have an account).
- Dashboard → Realtime Messaging → Keys → Create key.
- Pick a key type:
| Key type | When to use | Where it goes |
|---|---|---|
pk_live_… publishable | Prototypes, no per-user scoping | Directly in client code |
Secret key (sk_id_… + sk_secret_…) | Production apps with per-user permissions, custom peerId, embedded TURN creds | Server-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.
Send permissionWhen 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:
- Outbound: wrap your
flutter_webrtcMediaStreamwithwrapMediaStream(...)before handing it to the SDK. - Inbound: the SDK gives you a
MediaStreamLike; unwrap it with.nativeand render it through anRTCVideoRenderer+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:
join('my-room')connects the WebSocket, subscribes you tomy-room, and asks the server who else is here.- For every peer the server reports,
onPeerJoinedfires and the SDK opens a peer-to-peerRTCPeerConnectionunder the hood. - The local stream you
addStream'd is sent to every peer; you receive theirs ononStreamAdded. 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 call | Guide: WebRTC Video Call |
| Anything before it ships | Platform Setup + Reconnect Best Practices |
| Chat / classroom / a lobby | Guide: Presence & Chat |
| Low-latency P2P data | Guide: Data Channels & Low Latency |
Per-user JWTs (custom peerId, channel permissions, TURN creds) | Guide: Authentication |
Coming from raw flutter_webrtc | Migration: from flutter_webrtc |