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.
  • SignallingMetered Realtime, free up to 100 connections and 100,000 messages per month.
  • TURNOpen 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.