Skip to main content

Troubleshooting

The issues people actually hit, with the fix. Turn on logging first — pass a ConsoleLogger and most of these announce themselves:

MeteredPeer(MeteredPeerOptions(apiKey: 'pk_live_…', logger: const ConsoleLogger()));

Connection​

SymptomLikely causeFix
connect() / join() throws SignallingConnectError, closeCode == 4001JWT signature / kid / signing secret wrong, or a bad pk_ keyCheck the key; mint a fresh JWT with the right signing secret
closeCode == 4003The channel isn't in the JWT's channels claimAdd the channel pattern at mint time
closeCode == 4010At your plan's concurrent-connection capLook for leaked clients — call dispose()/close() when done; or raise the plan
closeCode == 4012Account suspended (billing)Resolve billing
connect() hangstokenProvider() never resolvesIt's capped by tokenProviderTimeoutMs (10 s default); check your mint endpoint
State flips to reconnecting immediately and repeatedlytokenProvider returning a stale JWT (4002 loop)Return a FRESH token each call (it runs on every reconnect)

"The call connects but I see no video"​

This is almost always one of four things. Work down the list:

  1. pk_ key without Send. If onPeerJoined fires (you see the peer) but onStreamAdded never does (the tile stays black), your publishable key is missing the Send permission — MeteredPeer needs it to exchange SDP/ICE. Re-create the key with Send ticked. See the pk_ gotcha. This is the #1 cause.
  2. Permissions not declared. getUserMedia throws or returns no frames. Re-check Platform Setup — the Android manifest entries, the iOS Info.plist usage strings.
  3. iOS Simulator. The iOS Simulator has no camera — getUserMedia({'video': true}) produces no frames there. Test video on a real device (Android emulators do have a virtual camera).
  4. Renderer not bound. Make sure you assign renderer.srcObject = (ev.stream as FlutterWebrtcMediaStream).native inside the onStreamAdded listener — and that you call RTCVideoView(renderer) with the initialized renderer.

"Video freezes after a network blip"​

You're not re-binding the renderer on reconnect. After a reconcile the SDK surfaces a fresh MediaStream object (same stream.id) — a renderer still holding the old one shows a frozen frame. Re-assign renderer.srcObject on every onStreamAdded, not just the first. The _RemoteView pattern does this by construction. See Reconnect Best Practices.

"Remote peers connect to each other but not to me" / "works on Wi-Fi, fails on cellular"​

A TURN-relay problem. Direct P2P works on permissive networks; restrictive ones (cellular, corporate firewalls) need TURN.

  • Confirm ConnectedEvent.iceServers is non-empty (pk_ keys rely on auto-injection; check it's enabled on the key).
  • On the JWT path, ensure auto-inject is on, or that you're supplying metadata.iceServers yourself.
  • Without working TURN, a meaningful fraction of real-world calls fail to connect.

"The camera light stays on after the call ends"​

peer.close() doesn't stop your capture tracks. In your cleanup, stop the tracks and dispose the stream first, then close:

_localRenderer.srcObject = null;
for (final t in _localStream?.getTracks() ?? const []) { await t.stop(); }
await _localStream?.dispose();
await _peer?.close();
await _localRenderer.dispose();

Platform / build​

SymptomFix
Android build fails on minSdkVersionflutter_webrtc needs API 23+ — set minSdk = maxOf(flutter.minSdkVersion, 23)
iOS build can't find WebRTC podscd ios && pod install; deployment target 12.0+
Web: navigator.mediaDevices is undefinedServe over HTTPS (or localhost) — getUserMedia requires a secure context
Black tile only on webBrowser blocked autoplay or camera — check the permission prompt and console

Messaging​

SymptomFix
send() throws MeteredPeerSendError(SendErrorCode.notJoined)Called before join() completed — await it, or queue behind onJoined
send() throws MeteredPeerOversizedErrorPayload over maxMessageSize — chunk it, or use a P2P DataChannel
onData never fires for a peer's messageThey're not in your channel (cross-channel directs are dropped on MeteredPeer — use SignallingClient for unscoped)
onMessage fires but m.fromMetadata is nullSubscribe with includeSenderMetadata: true, and ensure the sender's JWT has peerMetadata
Subscribed but receive nothingCheck the publisher's key/JWT allows publishing on that exact channel name

Leaks / lifecycle​

  • Streams leaking — cancel the StreamSubscriptions you got from .listen() in your widget's dispose(), and call client.dispose() / peer.close().
  • Renderers leaking native textures — await renderer.dispose() for every RTCVideoRenderer you created (local + per-remote), including in onPeerLeft.

Still stuck?​

Capture a ConsoleLogger transcript of a failing run and the close code (onDisconnected.code on a SignallingClient, or the onError message on MeteredPeer), then check Errors & Codes for that code's meaning.

See also​