Skip to content
On this pageOPEN
  1. 01Goal
  2. 02Plain-language concept
  3. 03Interactive diagram
  4. 04Behind the scenes
  5. 05Code walkthrough
  6. 06Do & don't
  7. 07Practice
  8. 08Quick quiz
  9. 09Key takeaways
  10. 10Common errors
LESSON 14Production26 min read

Debugging real-time systems

A field checklist for stuck connections, missing tracks, bad audio, and browser-specific WebRTC failures.

#debugging#webrtc-internals#getStats#network
01

Goal

  • 01Turn a vague real-time complaint into a layer, a state, and a reproducible test.
  • 02Use browser WebRTC internals, signaling logs, getStats, and network captures without drowning in data.
  • 03Write diagnostics that survive a bug report and protect user privacy.
02

The concept, plainly

Start with the exact symptom and the layer. 'No video' can mean no camera track, no offer, no answer, no ICE pair, no DTLS, wrong transceiver direction, a muted element, or a decoder failure. Ask when it fails and whether it fails for both sides. A one-way failure narrows the search dramatically. A failure only on cellular points to NAT or TURN. A failure only in one browser points to codec, API, or autoplay policy.

Capture a timeline. Log the local signaling state, ICE state, gathering state, connection state, candidates, and data-channel state with monotonic timestamps. On the other peer, do the same. Put both timelines side by side. A missing offer is a signaling problem; an offer that never gets an answer is a routing or authorization problem; an answer with no candidates is a gathering problem; checking forever is an ICE or TURN problem; connected with no media is a track, direction, or rendering problem.

Use the tools that already exist. `chrome://webrtc-internals` shows the full offer/answer, candidate pairs, ICE events, and stats graphs. Firefox has `about:webrtc`. Safari has a WebRTC logging option in the Develop menu. Use the Network panel for signaling WebSocket frames and TURN/TCP behavior. `getStats()` is the portable source of truth for media health, while internals is best for protocol-level inspection.

Separate signaling health from media health. A WebSocket can be closed while an existing peer connection stays connected. A signaling server can be healthy while ICE fails. Never use one green badge for both. Show the socket status, peer connection state, data channel state, and selected pair independently. That alone prevents many misleading bug reports.

Check the boring causes first: permission denied, a muted element, video CSS hiding the element, autoplay policy, a track added after negotiation, a sender with a null track, a screen share that ended, and an audio output routed to the wrong device. Then move to codec and network. Many 'WebRTC bugs' are DOM, permissions, or state-management bugs. Reproduce with the synthetic test feed to remove hardware variables.

Write diagnostics for the next person. Include a session id, topology, browser and OS, selected path type, quality limitation, and a small set of anonymous aggregated stats. Do not ship raw SDP, full candidate addresses, or device labels by default. Provide a copyable diagnostic summary with clear redaction. A good bug report can be understood without watching the user's screen.

03

See it move

PLAY / PAUSE / STEP

Interactive diagram

The first broken checkpoint wins

capture?publish?candidatescheckstransportmedia pathframesthen optimizeBROWSERSymptomwho · when · whatPROCESSPermissiongetUserMediaSERVERSignalingjoin + offer + answerNETWORKGathercandidate typesNETWORKICE pairchecking → selectedPROCESSDTLSfingerprint matchPROCESSTrack statelive · enabled · directionBROWSERRendersrcObject · autoplaySTORAGEQualityloss · CPU · jitter
STEP 01 / 08triage

Classify the symptom

Ask who, when, where, and which direction. A symmetric failure across good networks starts higher than a one-way failure on cellular.

Walk the path in order. Do not debug codecs before a candidate pair exists, and do not blame ICE when the track is muted.

04

Behind the scenes

01

Browser internals show the exchange, not your application

webrtc-internals records the peer connection's view: SDP, candidates, ICE state, stats graphs, and events. It cannot tell you why your authorization server rejected a join or why your React state rendered the wrong component. Use internals for transport and stats, application logs for control flow, and the Network panel for signaling frames. Correlate timestamps.

02

Remote debugging changes the environment

Connecting devtools to a mobile device may force a different network, change the page lifecycle, or keep the screen awake. Reproduce without the debugger when possible, and note whether the bug only appears while attached. For media, a USB-tethered phone may bypass the cellular network you were trying to test.

03

Logs can leak more than they reveal

SDP contains IP addresses, DTLS fingerprints, and ICE credentials. Candidate lines contain addresses. Device labels can identify hardware. A diagnostics feature that uploads everything can create a privacy incident. Keep a local copy for debugging, send only an aggregated summary with consent and retention, and redact by default.

05

Code walkthrough

COPY-READY

Collect the facts that identify a layer without shipping raw SDP or full addresses. Pair it with a session id and timestamps.

lib/diagnostics.ts
export async function diagnosticSnapshot(
pc: RTCPeerConnection | null,
signaling: { status: string; room: string | null },
) {
const base = {
at: new Date().toISOString(),
browser: navigator.userAgent,
signaling,
secureContext: window.isSecureContext,
pc: pc
? {
connection: pc.connectionState,
ice: pc.iceConnectionState,
gathering: pc.iceGatheringState,
signaling: pc.signalingState,
senders: pc.getSenders().map((sender) => ({
kind: sender.track?.kind ?? null,
state: sender.track?.readyState ?? null,
enabled: sender.track?.enabled ?? null,
})),
receivers: pc.getReceivers().map((receiver) => ({
kind: receiver.track.kind,
state: receiver.track.readyState,
muted: receiver.track.muted,
})),
}
: null,
};
if (!pc) return base;
const report = await pc.getStats();
let pairId: string | undefined;
report.forEach((stat) => {
if (stat.type === "transport") pairId = stat.selectedCandidatePairId;
});
const pair = pairId ? report.get(pairId) : undefined;
const local = pair ? report.get(pair.localCandidateId) : undefined;
return {
...base,
selectedPath: local?.candidateType ?? "none",
rttMs: pair?.currentRoundTripTime ? Math.round(pair.currentRoundTripTime * 1000) : null,
};
}
06

Do and don't

Do

  • Get both timelines

    Compare caller and callee events with timestamps. A one-sided view hides routing, ordering, and authorization failures.

  • Use the synthetic feed for isolation

    It removes camera and permission variables without changing the peer connection, ICE, encryption, or data-channel path.

  • Check the selected pair and track state

    Connected is not enough. Confirm DTLS, a live sender, the negotiated direction, and a rendered element.

  • Keep diagnostics short and redacted

    A copyable summary with session context is more useful than a multi-megabyte dump, and safer.

Don't

  • Don't change five things at once

    Change one variable, reproduce, and record the result. Media bugs are often state-dependent.

  • Don't test only on localhost

    Secure context, permissions, NAT, TURN, and device behavior differ on real networks and phones.

  • Don't trust a green connection badge

    It may mean ICE/DTLS succeeded while no track, wrong direction, or a muted element produces a black pane.

  • Don't upload raw session data

    SDP and candidates are sensitive. Redact, aggregate, and explain what the diagnostics contain.

07

Practice

Hands-on lab

Debug a deliberately broken call

The 1:1 lab gives you state logs, candidate filters, permission failures, and live stats. Use the break-it controls as controlled incidents.

  1. 1Drop every candidate and write down the first state that stops advancing.
  2. 2Restore normal, connect, then kill signaling. Which health indicators change?
  3. 3Deny camera access and record the exact error path and recovery action.
  4. 4Copy the signaling and peer logs and identify the offer, answer, candidate, and connected events in order.
Open the diagnostic lab

Solo? Loopback plus candidate filters reproduces the state machine. The missing signaling socket is intentional; use the manual exchange to practice without it.

08

Quick quiz

Knowledge check

0 of 5 answered

0%
  1. 01

    A call is connected but remote video is black. What should you check first?

  2. 02

    What is webrtc-internals most useful for?

  3. 03

    ICE stays in checking on a mobile network but works on Wi-Fi. What is the likely category?

  4. 04

    Which diagnostic data should be redacted by default?

  5. 05

    What is the value of a session generation id in logs?

Pick one answer per question to see instant feedback.
09

Key takeaways

  1. Triage the first broken checkpoint: symptom, permission, signaling, gathering, pair, DTLS, track, render, quality.01
  2. Use browser internals for transport truth, application logs for control flow, and getStats for media health.02
  3. Connected with no media is usually a track, direction, or render problem.03
  4. Collect a compact, redacted timeline with both peers and a session generation.04
10

Common errors and fixes

Terms in this lesson

Done with this lesson?

Mark it complete, then keep the thread.