Header Logo

Debugging WebRTC and WebSocket Issues in a React-Based AI NPC Avatar

Debugging WebRTC and WebSocket Issues in a React-Based AI NPC Avatar

Debug React WebRTC/WebSocket avatar issues by isolating lifecycle, signaling, ICE, and video stream rendering bugs.

Introduction


When a React app hosts a realtime AI avatar, the failure modes are rarely “the model is down.” More often, the browser can’t establish the media path cleanly, the signaling socket is unstable, or React is re-creating objects at exactly the wrong time. The result is familiar: black video, audio that starts and stops, a WebSocket that reconnects forever, or a session that works in one browser tab but not another.


This post focuses on debugging that stack end-to-end: browser signaling over WebSocket, media delivery over WebRTC, and the React integration patterns that commonly break both. By the end, you should be able to isolate whether the issue is in the UI layer, the signaling layer, or the media transport itself, and apply a practical fix instead of guessing.


Start with the three layers that can fail independently


A React-based avatar client usually has three separate concerns:


  1. Application state: component lifecycles, rerenders, hooks, and stale closures.

  2. Signaling: the WebSocket channel that coordinates session setup, SDP exchange, and control messages.

  3. Media transport: WebRTC peer connection, ICE, DTLS/SRTP, and the actual audio/video tracks.


These layers fail differently. If your WebSocket disconnects, the avatar may never start. If signaling succeeds but ICE fails, you might see a connected session with no media. If the media tracks arrive but your React component keeps swapping the video element or stream reference, the browser may have video data that never gets rendered.


Debugging is easier if you keep those boundaries explicit. The first question is not “why is the avatar broken?” but “which layer stopped doing its job?”


Diagnose the browser path first: React bugs look like network bugs


In React, the most common mistake is rebuilding connection objects on every render. If you create a WebSocket, peer connection, or event handler inline in a component body, React will happily rerun that code as state changes. The session then appears unstable because your app keeps tearing down and recreating the transport.


Use stable references for anything that represents a long-lived connection:


import { useEffect, useRef } from "react";

}
import { useEffect, useRef } from "react";

}
import { useEffect, useRef } from "react";

}


Two details matter here:


  • The connection is created in an effect, not during render.

  • The cleanup closes the socket so unmounts don’t leak sessions.


For media elements, apply the same principle. If you bind a remote stream to a video tag, make sure the element itself stays mounted while the stream is active. A common bug is rendering the video only after some async state flips, which can race with the arrival of the track. Prefer a persistent <video> ref and assign srcObject when the stream exists.


function RemoteVideo({ stream }) {

}
function RemoteVideo({ stream }) {

}
function RemoteVideo({ stream }) {

}


If the video is black but the track exists, inspect whether the element is present, whether autoplay is blocked, and whether the stream is actually attached. In practice, these are more common than codec issues.


WebSocket signaling: verify lifecycle and message order


WebSocket issues are usually about lifecycle timing, auth, or message ordering. In a realtime avatar flow, the socket often handles session bootstrap: authenticate, create or join a session, exchange metadata, and then coordinate WebRTC setup. If any of that happens out of order, the peer connection may never reach a stable state.


Start with the basics:


  • Check that the socket reaches OPEN before sending any message.

  • Log every outbound and inbound signaling message during debugging.

  • Confirm that reconnect logic does not duplicate session setup.

  • Make sure browser-origin restrictions, if any, match the actual host you are testing from.


A good debugging pattern is to instrument the socket with explicit state transitions rather than relying on implicit UI state:


ws.onopen = () => send({ type: "session.start" });

}
ws.onopen = () => send({ type: "session.start" });

}
ws.onopen = () => send({ type: "session.start" });

}


If you see messages leaving but no response, the problem may be auth or server-side rejection. If you see a response but no media, the problem likely moved down into WebRTC negotiation.


WebRTC failures: inspect ICE, not just connection state


WebRTC can look “connected” from the app’s perspective while still failing to move media. The useful browser-side indicators are:


  • iceConnectionState and connectionState

  • Candidate gathering progress

  • Whether a remote track actually arrives

  • Whether RTP stats show packets flowing


If you only watch the top-level connection state, you can miss useful detail. For example, connected can happen briefly and then degrade to failed because the browser chose a candidate pair that cannot sustain media. Likewise, checking may stick if STUN/TURN connectivity is blocked by a restrictive network.


When debugging in Chrome, open chrome://webrtc-internals and watch:


  • Selected candidate pair

  • ICE state transitions

  • Outbound and inbound RTP packet counters

  • Audio level and jitter


If packets are flowing but the avatar still looks frozen, the issue is probably not transport. It may be a rendering problem, an element lifecycle issue, or a track-selection mistake. If no candidate pair is selected, focus on network traversal and server configuration.


One subtle failure mode in avatar apps is mixing up local and remote tracks. In a voice-agent UI, you may have microphone capture, synthesized speech, and a remote avatar video track all at once. Make sure the video you are inspecting is actually the avatar’s remote track, not your own camera preview or a muted placeholder stream.


Common React integration mistakes that masquerade as WebRTC problems


There are a few patterns I see repeatedly in frontend code:


  • Effect dependency loops: a changing callback identity reopens the socket on each render.

  • Stale closures: event handlers capture old session state and dispatch messages to the wrong instance.

  • Unmount/remount churn: route transitions or conditional rendering destroy the video element while the session is live.

  • Double initialization in development: React Strict Mode intentionally mounts effects twice in dev, which can expose code that is not idempotent.


For debugging, make session setup idempotent. Store the current session token, connection object, and peer connection in refs or a dedicated state machine. Then guard against accidental re-entry:


const startedRef = useRef(false);

}, []);
const startedRef = useRef(false);

}, []);
const startedRef = useRef(false);

}, []);


This does not replace proper cleanup; it just prevents duplicate startup while you are tightening the lifecycle.


Check the backend contract before blaming the browser


When the frontend looks reasonable, verify that the backend contract is actually being honored. For a realtime avatar service, that means checking authentication, session creation, and the shape of the session configuration. A browser client should never hold a long-lived secret API key. If you need to create sessions from your own backend, do it server-side and pass only the ephemeral result to the browser.


A basic REST call to create or inspect a session will usually look like this, with the exact fields defined in the docs:


curl -X POST "https://api.protoface.com/..." \
-d '{"avatar_id":"...","voice":"..."}'
curl -X POST "https://api.protoface.com/..." \
-d '{"avatar_id":"...","voice":"..."}'
curl -X POST "https://api.protoface.com/..." \
-d '{"avatar_id":"...","voice":"..."}'


Two practical checks here:


  • If the API request fails, fix auth or payload shape before debugging the client.

  • If the API succeeds but the browser cannot connect, compare the session identifiers and origin rules end to end.


For teams that prefer Python, the SDK is useful when you want to script session creation or reproduce a failing case outside the browser. Keep the reproduction minimal so you can separate API problems from client problems.


from protoface import ProtofaceClient

print(session)
from protoface import ProtofaceClient

print(session)
from protoface import ProtofaceClient

print(session)


How Protoface fits into this debugging model


The cleanest way to reduce browser-side complexity is to keep session setup and media orchestration behind a stable integration point. If you are building a voice agent in Python, the LiveKit plugin from the plugin repository or the Pipecat integration guide can drop in a synchronized talking face without making your React app own the full media stack. If you are debugging the browser experience directly, the docs at docs.protoface.com are the place to confirm the expected session and transport behavior.


That separation matters. Your React app should manage presentation and user interaction; the avatar session should be a well-defined realtime dependency. When the integration is clean, your debugging surface shrinks to a few questions: did the session get created, did the socket connect, did WebRTC negotiate, and did the remote track reach the video element?


Conclusion


Most WebRTC and WebSocket issues in a React avatar app are lifecycle bugs, not mysterious realtime failures. The fastest path to a fix is to isolate the layer that is failing, instrument it directly, and keep connection objects stable across renders. Check WebSocket state and message order first, then inspect ICE and RTP stats, and finally verify that your React component is not destroying the very element that should be rendering the avatar.


If you want to compare your implementation against a known-good setup, start with the quickstarts linked from the Protoface docs and then narrow the scope until the failure becomes obvious. When in doubt, reproduce with minimal state, log aggressively, and keep transport ownership out of the component tree as much as possible.

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.