Skip to main content

metered_realtime — the open-source Flutter WebRTC library

metered_realtime is a free, open-source (MIT) Flutter/Dart WebRTC library for video & voice calls. It's built on flutter_webrtc — the standard WebRTC plugin for Flutter — and adds the three things flutter_webrtc deliberately leaves to you: signalling, multi-peer orchestration, and TURN, plus auto-reconnect that survives Wi-Fi → cellular handoffs. flutter_webrtc gives you a single RTCPeerConnection; metered_realtime gives you multi-peer rooms with presence and discovery, the WebSocket signalling that connects them, and free TURN auto-injected — from a single Dart codebase that runs on Android, iOS, web, and desktop.

The Dart sibling of @metered-ca/realtime — same wire protocol, so a Flutter peer and a browser peer are in the same room.

flutter pub add metered_realtime
Read the SDK docsView on pub.dev

Flutter video & voice calls on every device​

The same model everywhere flutter_webrtc runs — Android, iOS, web, macOS, Windows, and Linux — from one Dart codebase:

  • 1:N video & voice calls — group rooms with presence, no signalling backend to build.
  • Chat + presence on the same connection — broadcast and direct messages ride alongside the media.
  • Screen sharing & multi-stream — camera + screen through one peer, each labelled.
  • Resilient by default — calls survive app backgrounding, network switches, and NAT rebinds.

Because it speaks the same wire protocol as the JavaScript SDK, a Flutter app and a browser tab are in the same room — your mobile and web users call each other directly, no gateway in between.

WebRTC in Flutter without the boilerplate​

With raw flutter_webrtc you create an RTCPeerConnection, then hand-build the signalling channel, the offer/answer exchange, ICE handling, the bookkeeping for who joined and left, reconnection, and TURN-credential fetching. With metered_realtime you join() a room and react to peers with streams:

import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:metered_realtime/metered_realtime.dart';

final peer = MeteredPeer(MeteredPeerOptions(apiKey: 'pk_live_...'));
await peer.join('room-42'); // presence + discovery; TURN auto-applied

peer.onPeerJoined.listen((remote) {
remote.onStreamAdded.listen((ev) {
// ev.stream is flutter_webrtc-backed under the hood:
renderer.srcObject = (ev.stream as FlutterWebrtcMediaStream).native;
});
});

final cam = await navigator.mediaDevices
.getUserMedia({'audio': true, 'video': true});
await peer.addStream(wrapMediaStream(cam), metadata: {'role': 'camera'});

Signalling runs over the free Metered Realtime server; TURN is Open Relay, auto-injected. There's nothing to host.

Rendering remote video​

flutter_webrtc provides the RTCVideoRenderer and RTCVideoView you draw with. Initialize a renderer per remote, and re-bind it on every onStreamAdded — after a transient drop the SDK surfaces a fresh stream object (same id), so a stale binding shows a frozen frame:

final renderer = RTCVideoRenderer();
await renderer.initialize();

remote.onStreamAdded.listen((ev) {
renderer.srcObject = (ev.stream as FlutterWebrtcMediaStream).native;
});

// in your widget tree:
// RTCVideoView(renderer,
// objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover)

Declare camera/microphone permissions per platform first (Android manifest + iOS Info.plist) — see Platform Setup.

Get your free key​

The SDK's apiKey is your signalling publishable key (pk_live_...) — the same key the JavaScript SDK uses, separate from the TURN REST API_KEY used to fetch Open Relay credentials directly. With metered_realtime the publishable key is all you need: TURN is auto-injected, so there's no second key to manage. Both are free. Sign up for a free Metered account, then:

  1. Go to Dashboard → Realtime Messaging → Keys → Create key and choose the Publishable type.
  2. Under "What can this key do?", make sure Send is checked. Subscribe, Publish, and Presence are on by default — leave them on.
  3. Copy the pk_live_… value and pass it as apiKey.
Send is off by default — turn it on

For publishable keys, Send starts unchecked. metered_realtime uses it to exchange SDP and ICE candidates between peers, so without it peer.join() and peer.addStream() succeed but the video never negotiates — peers never establish their RTCPeerConnection. This is the #1 reason a first call connects with no video.

For production, mint short-lived JWTs on your backend and pass a tokenProvider instead of the publishable key — the SDK refreshes them automatically across reconnects.

Full walkthrough: Flutter getting started →

Built on flutter_webrtc — batteries included​

metered_realtime doesn't replace flutter_webrtc — it wraps it. Every peer connection, track, and renderer under the hood is flutter_webrtc, and you keep full access to it (inbound streams expose .native). What the library adds is everything flutter_webrtc intentionally leaves to the application:

What production WebRTC needsRaw flutter_webrtcmetered_realtime
Peer connection, SRTP, codecs, renderers✅ (this is flutter_webrtc)✅ (uses flutter_webrtc)
Signalling transportbuild it yourself✅ free, managed (WebSocket)
Offer/answer + ICE exchangewire it yourself✅ automatic (perfect negotiation)
Multi-peer rooms + presencetrack it yourself✅ join/leave events
Auto-reconnect on network dropbuild it yourself✅ WebSocket + ICE-restart
TURN credentialsfetch + embed yourself✅ auto-injected (20 GB/mo free)
Shares rooms with browser/JS peersDIY protocol✅ same wire protocol
PlatformsAndroid/iOS/web/desktopAndroid/iOS/web/desktop
LicenseMITMIT

If you're already using flutter_webrtc directly, metered_realtime is the layer you'd otherwise write by hand — adopt it incrementally and drop down to flutter_webrtc objects whenever you need to.

Auto-reconnect & perfect negotiation​

Mobile calls drop because the network moves underneath them — Wi-Fi to cellular, a tunnel, a backgrounded app. metered_realtime recovers automatically on three layers:

  • WebSocket reconnect with exponential backoff.
  • ICE-restart ladder — rebuilds the media path on Wi-Fi → cellular roams and NAT rebinds.
  • Identity-preserving reconcile — your RemotePeer references stay valid through a transient drop; the underlying connection is swapped out silently. Remote streams re-announce via onStreamAdded with a fresh stream object (same id) — re-bind your renderer on every event.

It also uses perfect negotiation, so you never hit glare or "who's the initiator?" race conditions.

Data messages​

Send structured messages to the whole room or to one peer:

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

await peer.send({'type': 'chat', 'text': 'hi everyone'}); // broadcast to the room
await peer.sendTo(otherPeerId, {'ping': 1}); // direct to one peer

For high-frequency game state or large transfers, open a peer-to-peer DataChannel over the documented wrapper.

Screen sharing & multi-stream​

Route camera and screen through a single peer, each labelled so the receiver can tell them apart:

final screen = await navigator.mediaDevices.getDisplayMedia({'video': true});
await peer.addStream(wrapMediaStream(screen), metadata: {'role': 'screen'});

remote.onStreamAdded.listen((ev) {
final native = (ev.stream as FlutterWebrtcMediaStream).native;
if (ev.metadata?['role'] == 'screen') screenRenderer.srcObject = native;
else cameraRenderer.srcObject = native;
});

The whole stack is free​

  • SDK — free, open source (MIT), built on flutter_webrtc.
  • Signalling — Metered Realtime, free up to 100 connections and 100,000 messages per month.
  • TURN — Open Relay, 20 GB/month free.

No credit card to get started.

Get a free key

One SDK family, every platform​

Same wire protocol, same free signalling + TURN behind each SDK — a Flutter app, a browser tab, and a Python agent interoperate across the same rooms without custom integration code.

PlatformStatus
Flutter (Android/iOS/web/desktop, on flutter_webrtc)✅ Available now
JavaScript / TypeScript (browser + Node 18+)✅ Available now
React Native✅ Available now
Python (server / headless, on aiortc)✅ Available now
iOS (Swift) / Android (Kotlin) native🔜 Coming soon

FAQ​

Is it production-ready? metered_realtime is at 0.1.0 — the newest SDK in the family — but it's built on the mature flutter_webrtc plugin and the same managed signalling + free TURN that back Metered's production browser SDK. It ships with a full unit-test suite and a runnable example app.

Which platforms does it run on? Everywhere flutter_webrtc runs — Android, iOS, web, macOS, Windows, and Linux — from a single Dart codebase.

Do I need a backend? No — a pk_live_ publishable key runs entirely in the app. Add backend-minted JWTs via tokenProvider when you want per-user permissions and stable peer IDs.

Can a Flutter app join the same room as my browser users? Yes. The Flutter and JavaScript SDKs speak the same wire protocol, so a Flutter app and a browser tab are peers in the same room — ideal for cross-platform calls between mobile and web.

Can I still use flutter_webrtc directly? Yes — by design. Inbound streams are flutter_webrtc objects via (stream as FlutterWebrtcMediaStream).native, and you render with flutter_webrtc's RTCVideoRenderer / RTCVideoView. metered_realtime adds a layer; it doesn't lock the lower one away.

Do I need to run a signalling server or a TURN server? No. Signalling is the managed, free Metered Realtime service, and Open Relay TURN credentials are auto-injected at connect time (20 GB/month free). There's nothing to host.


metered_realtime is built on flutter_webrtc, the free Metered Realtime signalling server, and the Open Relay TURN server — a complete, free, open-source-first WebRTC stack for Flutter.