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 texturesawait 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