Header Logo

Protoface Node SDK Guide: Debugging WebRTC ICE Connection Failures in Avatar Streaming

Protoface Node SDK Guide: Debugging WebRTC ICE Connection Failures in Avatar Streaming

Debug Protoface WebRTC ICE failures: log states, inspect candidates, and separate signaling, STUN/TURN, and rendering issues.

Introduction


When a WebRTC avatar session fails to connect, the symptom is often unhelpful: a spinner that never resolves, a video element that stays black, or an ICE state that gets stuck in checking and then falls back to failed. The underlying issue is usually not “video” itself. It is connectivity between two peers that need to discover a routable path through NAT, firewalls, and sometimes restrictive enterprise networks.


This post is for developers integrating realtime avatars into voice agents or web apps. By the end, you should be able to identify where ICE failures happen, distinguish signaling problems from media path problems, and apply a practical debug checklist that gets you from “it doesn’t connect” to a specific network or implementation issue.


I’ll use Protoface as the concrete example because it sits on top of a WebRTC-style realtime avatar flow: a session is created, signaling happens, ICE candidates are gathered and exchanged, and then the avatar’s video/audio stream becomes available to your client or agent runtime.


What ICE is actually doing


ICE, the Interactive Connectivity Establishment layer in WebRTC, is the mechanism that finds a working network route between peers. In practice, it coordinates three things:


  • STUN to discover the public-facing address seen from the internet.

  • TURN to relay media when direct peer-to-peer paths are blocked.

  • Candidate exchange over signaling so each side can test possible routes.


An ICE failure does not always mean the media server is down. Common failure modes include:


  • Signaling never completed, so candidates were never exchanged.

  • ICE candidates were exchanged, but all candidate pairs failed connectivity checks.

  • A network allowed HTTPS but blocked UDP, which kills the preferred media path.

  • WebSocket signaling succeeded, but the browser could not reach the TURN server.

  • Firewall or browser policy prevented a camera/microphone/device permission flow, causing the session setup to abort before ICE even started.


The useful mental model is to split the problem into three layers: signaling, candidate gathering, and connectivity checks. If you can tell which layer failed, your debugging gets much faster.


Start with the browser or client state machine


The first thing to inspect is the ICE state transitions. In browser JavaScript, every RTCPeerConnection exposes state changes you can log directly. Do this before changing network settings or rewriting session code.


pc.onicegatheringstatechange = () => {

};
pc.onicegatheringstatechange = () => {

};
pc.onicegatheringstatechange = () => {

};


Interpretation matters:


  • new means ICE has not started meaningfully yet.

  • checking means candidates are being tested.

  • connected or completed means a viable route was found.

  • failed usually means all candidate pairs failed or no usable candidates were gathered.


If the state never leaves new, the issue is often signaling or session setup, not ICE pathing. If it reaches checking and stalls, you are usually in a network reachability problem or TURN misconfiguration. If it briefly connects and then drops, look at NAT timeouts, aggressive proxies, or unstable mobile networks.


Differentiate signaling failures from media failures


It is easy to misclassify a bad session initialization as an ICE problem. The quickest way to avoid that is to verify signaling independently from media. For a Protoface-backed session, that means checking whether the avatar session was created successfully and whether the client actually received the session data it needs to begin WebRTC negotiation.


If you are creating sessions through the REST API, confirm the API call returns normally and that the response includes whatever session metadata your client expects. Keep the auth layer separate from the media layer: a 401 or 403 is not an ICE failure, even if the symptom in the browser is “avatar never loads.”


curl -X POST https://api.protoface.com/v1/sessions \
}'
curl -X POST https://api.protoface.com/v1/sessions \
}'
curl -X POST https://api.protoface.com/v1/sessions \
}'


The exact request and response fields depend on the endpoint and SDK version, so use the public docs for the canonical shape. The key point is to validate the session creation path first. If that succeeds, log the values handed to the browser or agent process and confirm they are passed through unchanged.


In a browser-based integration, check three things in order:


  1. Did the app receive a valid session payload?

  2. Did signaling exchange complete without auth or serialization errors?

  3. Did the peer connection reach checking and then connected?


If step 1 fails, stop debugging ICE. If step 2 fails, inspect your signaling transport and schema. Only after those are stable should you focus on TURN/STUN and network routing.


Inspect candidate gathering and relay fallback


When a network blocks direct UDP paths, WebRTC should fall back to a relay candidate via TURN. If that fallback never happens, you often end up with a session that looks healthy in code but can’t establish a media path in production environments like corporate VPNs, school networks, or heavily filtered mobile carriers.


From a debugging perspective, the most useful thing is to print the gathered candidates and confirm that relay candidates are present when direct paths are unavailable. In browser JavaScript, you can inspect the candidates as they arrive:


pc.onicecandidate = (event) => {
};
pc.onicecandidate = (event) => {
};
pc.onicecandidate = (event) => {
};


Useful clues in the candidate string:


  • typ host means a local interface candidate.

  • typ srflx means a server-reflexive candidate discovered through STUN.

  • typ relay means a TURN relay candidate.


If you only see host candidates in an environment where direct connectivity should be impossible, STUN may not be reachable. If you see srflx but no relay, TURN may be missing or blocked. If relay candidates exist but still never connect, the TURN endpoint itself may be unreachable from the client network.


One subtle failure mode is asymmetric policy: outbound HTTPS is allowed, but UDP and nonstandard ports are blocked. In that case, TCP-based TURN is often the difference between success and failure. If your stack supports it, make sure your relay configuration includes a transport path that survives constrained networks.


Use logs and packet-level evidence, not guesswork


WebRTC debugging gets much easier when you gather a small set of logs from both the application and the network layer. I usually want the following:


  • Browser console logs for ICE state changes and candidate errors.

  • Session creation logs from your backend or agent runtime.

  • The exact timestamp when the session was created and when the peer connection started.

  • Whether the user is on corporate Wi-Fi, VPN, mobile, or a home network.


If you can reproduce the issue locally, browser internals are extremely useful. Chrome’s WebRTC diagnostics can show whether candidates were gathered, which pair was selected, and whether connectivity checks completed. In practice, I look for evidence that a pair was nominated. If there is no nomination, the failure is before media transport. If there is nomination followed by immediate teardown, you may have a mid-session network change or server-side rejection.


There are also patterns that point away from ICE entirely:


  • Audio works but video is black: often track negotiation, autoplay policy, or renderer issues.

  • Session connects only when users disable VPN: network policy or blocked relay traffic.

  • It works in one browser but not another: permission, codec, or implementation differences.

  • It works on desktop but not mobile: NAT behavior, power management, or intermittent network switching.


The goal is not to memorize every WebRTC nuance. It is to stop treating “failed to connect” as a single bucket. Most production bugs are either a signaling bug, a relay reachability issue, or a client-state bug that merely looks like networking.


How Protoface fits into the debugging workflow


For a real integration, the practical way to debug is to isolate the surface that owns each step. If you are using the LiveKit Agents plugin, the avatar portion is handled by the plugin layer while your agent handles voice logic. That means you can debug the WebRTC connection path independently from your model or TTS pipeline. The plugin repo and examples are a good place to compare your setup against a known-good integration: Pipecat integration example is especially useful if your stack already uses Pipecat, and the package docs at PyPI show the service entrypoint for that integration.


If you are orchestrating sessions directly, the Python SDK or REST API lets you verify creation and inspect session-level errors without involving the browser at all. That separation is what you want during incident response: prove that the server-side session exists, then prove that the client can reach the media path, then prove the tracks are actually rendered.


In short, Protoface gives you multiple choke points to test. Use them. A clean session created through the API but a failed browser connection tells you the bug is in client network reachability. A failed API request tells you to stay out of WebRTC and fix auth, payload shape, or backend routing first.


Practical checklist for ICE failures


  1. Verify session creation succeeds before debugging media.

  2. Log iceGatheringState, iceConnectionState, and candidate errors.

  3. Confirm you see srflx and, when needed, relay candidates.

  4. Test on a hostile network: VPN, corporate Wi-Fi, or mobile hotspot.

  5. Separate signaling, TURN reachability, and rendering into different checks.

  6. Keep auth, payload schema, and browser permissions out of the ICE bucket.


Conclusion


ICE failures are usually diagnosable once you stop treating them as opaque WebRTC magic. The core questions are simple: did signaling complete, were viable candidates gathered, and did any candidate pair survive connectivity checks? If you instrument those steps, you can usually narrow a “connection failed” report to a specific network path or implementation mistake in a few minutes.


For the exact session shapes, SDK usage, and integration details, refer to the public docs at docs.protoface.com. If you are wiring this into an agent stack, start with a known-good quickstart or plugin example, reproduce the failure with logging enabled, and work backward from the ICE state machine rather than forward from the black video tile.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.