Skip to main content

Migrating from raw flutter_webrtc

If you've built WebRTC in Flutter directly on flutter_webrtc, you already wrote — or copied — a signalling server, an SDP/ICE exchange, peer-discovery logic, reconnection handling, and TURN credential plumbing. metered_realtime keeps your media + rendering code and replaces all of that plumbing with peer.join(channel).

This isn't a rip-and-replace: metered_realtime depends on flutter_webrtc. You keep getUserMedia, RTCVideoRenderer, RTCVideoView, and your widget tree. You delete the negotiation + signalling layer.

What you keep

Your existing codeStill used
navigator.mediaDevices.getUserMedia(...)Yes — unchanged
RTCVideoRenderer + RTCVideoViewYes — unchanged
track.enabled toggles for muteYes — unchanged
getDisplayMedia for screen shareYes — unchanged
Android manifest / iOS Info.plist permissionsYes — see Platform Setup

What you delete

Your existing codeReplaced by
Your signalling WebSocket + message protocolThe SDK's connection (auto-reconnect, ack, presence)
RTCPeerConnection construction + lifecycle, one per peerMeteredPeer manages one per remote
createOffer / createAnswer / setLocalDescription / setRemoteDescriptionHandled (perfect negotiation, including native rollback)
onIceCandidate → send → addCandidate routingHandled (ICE trickle over signalling)
Glare handling / "who offers first" tie-breakHandled (politeness from peer ids)
ICE restart on network changeHandled (9-attempt restart ladder)
Reconnect + re-negotiation after a dropHandled (channel reconcile; your RemotePeer refs survive)
Manual TURN credential fetch + RTCConfigurationAuto-injected into the welcome

Before / after

Adding your camera

// Before — you manage the PC and add tracks yourself, per peer:
final pc = await createPeerConnection(config);
final stream = await navigator.mediaDevices.getUserMedia({'video': true, 'audio': true});
for (final track in stream.getTracks()) {
await pc.addTrack(track, stream);
}
// ...then createOffer, setLocalDescription, send the SDP over your socket, etc.

// After — one call fans out to every current and future peer:
final stream = await navigator.mediaDevices.getUserMedia({'video': true, 'audio': true});
await peer.addStream(wrapMediaStream(stream));

The one new step: wrap the flutter_webrtc MediaStream with wrapMediaStream(...) before handing it to the SDK. (And wrapMediaStreamTrack(...) for a single track via addTrack.)

Receiving a remote track

// Before — your onTrack handler on each PC you created:
pc.onTrack = (RTCTrackEvent event) {
if (event.streams.isNotEmpty) {
remoteRenderer.srcObject = event.streams[0];
}
};

// After — one stream per remote, surfaced by the SDK:
remote.onStreamAdded.listen((ev) {
remoteRenderer.srcObject = (ev.stream as FlutterWebrtcMediaStream).native;
});

Inbound streams are flutter_webrtc-backed; unwrap with .native to get the same MediaStream you'd assign to a renderer.

Discovering peers

// Before — your own signalling messages drive this:
socket.onMessage = (msg) {
if (msg.type == 'peer-joined') createPeerConnectionFor(msg.peerId);
if (msg.type == 'peer-left') teardownPeerConnection(msg.peerId);
};

// After — presence-driven, with a ready-made RemotePeer:
peer.onPeerJoined.listen((remote) { /* wire renderers */ });
peer.onPeerLeft.listen((remote) { /* dispose renderer */ });

The mental-model shifts

  1. One MeteredPeer = one channel, N connections. You don't think in individual RTCPeerConnections anymore; you think in peers in a channel. The PC is still there at remote.pc if you need it (e.g. getStats, data channels).
  2. Media survives reconnects; you re-bind renderers. Your RemotePeer references and added streams persist across a network drop — but the MediaStream object is fresh after a reconcile, so re-assign renderer.srcObject on every onStreamAdded. This is the one habit to build. See Reconnect Best Practices.
  3. createDataChannel is still yours. If you used raw data channels, keep them — open via remote.pc.createDataChannel(...) (now async) and optionally wrap with DataChannel for backpressure. See Data Channels & Low Latency.
  4. Signalling and media are independent. peer.send / sendTo are server-routed and work before (and regardless of) ICE — so chat/control messages don't depend on the P2P connection succeeding.

Keeping a custom WebRTC stack

If your peer topology doesn't fit MeteredPeer's 1:N channel model (an SFU client, a mesh with custom rules), you can still use the SDK purely as the signalling layer: drive SignallingClient directly for pub/sub + directed messages, and keep your own RTCPeerConnection management. You get auto-reconnect, ack correlation, presence, and TURN injection without the per-peer abstraction.

See also