Header Logo

Debugging WebRTC and WebSocket Issues in an Astro Realtime Avatar Sales Flow

Debugging WebRTC and WebSocket Issues in an Astro Realtime Avatar Sales Flow

Debug WebSocket, WebRTC, ICE, and Astro client hydration issues in a realtime avatar sales flow.

Introduction


When a realtime avatar sales flow breaks, the failure is usually not “the avatar” in the abstract. It is one of a few concrete transport problems: a WebSocket that never fully upgrades, a WebRTC session that connects but never produces media, or an application state machine that assumes signaling succeeded when it didn’t. In an Astro-based flow, those bugs can be especially annoying because you often have server-rendered pages, client-side hydration, and a browser-only media stack all interacting at once.


This post focuses on how to debug those issues systematically. By the end, you should be able to separate signaling failures from media failures, inspect the right browser and server logs, validate ICE/STUN/TURN behavior, and tighten the handoff between your Astro UI and the realtime avatar backend.


Start by identifying the layer that failed


For a realtime avatar flow, it helps to think in layers:


  • UI and app state: the user clicks “Start demo,” your Astro page renders a client component, and your app decides whether to connect.

  • Signaling: a WebSocket or HTTPS request creates or joins a session, exchanges session metadata, and sets up the peer connection.

  • Media transport: WebRTC establishes ICE connectivity and carries audio/video.

  • Avatar/session backend: the service generating the avatar stream may reject the session, time out, or fail to start a quality tier.


Most debugging time is wasted when those layers get conflated. For example, a UI can show “connected” because the WebSocket opened, while the actual video track never arrives because ICE failed. Or the peer connection can connect, but autoplay policy prevents the browser from rendering audio, making it look like the avatar is silent.


Your first job is to decide which of these happened. In practice:


  • If the WebSocket never opens, focus on auth, CORS, and endpoint reachability.

  • If the WebSocket opens but no media appears, inspect SDP, ICE candidates, and transceiver state.

  • If media exists but playback is silent or hidden, inspect browser autoplay, track attachment, and DOM timing.


Debug the WebSocket handshake before you touch WebRTC


A surprising number of “WebRTC issues” are actually signaling issues. If your client cannot establish a signaling channel, it never gets far enough to create an offer/answer exchange.


Use the browser’s Network panel and verify that the WebSocket upgrade completes with a 101 Switching Protocols response. Check that the request URL is correct, that any auth token is present, and that the response is not being blocked by origin policy or an intermediary proxy.


In Astro, one common trap is accidentally mixing server-only and client-only code. Anything that touches window, RTCPeerConnection, or WebSocket must run in a client component or inside a browser-only effect. If you instantiate those objects during SSR, you may not get a clean exception; you may get a component that silently renders but never connects after hydration.


When your app uses ephemeral session state, make sure the client has everything it needs before the connection attempt starts. A good pattern is:


  1. Render a static shell on the server.

  2. Fetch session metadata on the client.

  3. Open the WebSocket only after the metadata is present.

  4. Create the peer connection only after signaling is ready.


Short-lived auth mistakes can look like transport bugs. If your server returns a 401 or 403, do not keep retrying blindly; surface the failure so you can inspect the token, origin, or API key path. A clean failure is much easier to diagnose than a reconnect loop that hides the original error.


Separate WebRTC signaling from media delivery


Once the signaling channel is alive, the next failure mode is that the peer connection reaches “connected” or “checking” and then stalls. This usually means ICE cannot find a viable path between client and server, or the browser never attaches the remote stream to a visible element.


Three checks matter most:


  1. SDP exchange: confirm that offer and answer are actually exchanged and applied.

  2. ICE candidates: confirm that candidates are gathered and selected.

  3. Track attachment: confirm that the remote video track is bound to a <video> element with the right autoplay and muted properties.


Use chrome://webrtc-internals or the equivalent browser diagnostics to inspect the peer connection state, candidate pairs, jitter, packet loss, and track events. If you see media packets flowing but no visible video, the bug is usually in your UI rather than in WebRTC itself.


A minimal browser-side attachment pattern looks like this:


const pc = new RTCPeerConnection();

};
const pc = new RTCPeerConnection();

};
const pc = new RTCPeerConnection();

};


That snippet is intentionally simple. In a real app, the ordering matters: create the video element early, attach the stream when the track arrives, and then call play() only after the element exists and browser policy allows it. If the avatar includes audio, remember that most browsers block autoplay with sound unless the user has interacted with the page. A muted local preview can help distinguish “no track” from “autoplay blocked.”


Fix the common ICE and network pitfalls


WebRTC is very good at finding a path when the network is ordinary. It is also very good at exposing edge cases when corporate firewalls, VPNs, or aggressive NATs are involved. If your flow works at home but fails on office Wi-Fi, suspect network policy before you suspect the avatar backend.


Useful checks:


  • STUN/TURN reachability: if the environment blocks UDP, you may need TURN over TCP/TLS.

  • Candidate policy: make sure the browser is actually gathering candidates and not restricted to host candidates only.

  • Proxy interference: some reverse proxies handle WebSocket upgrades but not long-lived media connections cleanly.

  • Origin mismatch: a client served from localhost:4321 and an API at another origin can trigger different policy behavior than your production domain.


When debugging candidate issues, log the peer connection state transitions explicitly:


pc.oniceconnectionstatechange = () => {

};
pc.oniceconnectionstatechange = () => {

};
pc.oniceconnectionstatechange = () => {

};


The state machine is useful because it tells you where to look next. checking that never advances usually means connectivity or TURN problems. connected with no media usually means track attachment, autoplay, or rendering. failed often means candidate negotiation or network reachability.


Also watch for duplicate connection attempts. In component frameworks, a reconnect can be triggered twice if an effect reruns during hydration or a prop change. That can produce strange symptoms: two peer connections, one active signaling socket, and one set of tracks attached to a now-detached DOM node. Guard your connection lifecycle carefully and make teardown idempotent.


Make the Astro boundary explicit


Astro gives you a clean separation between server-rendered markup and client-side islands, which is good for performance and good for debugging if you respect the boundary. It becomes painful only when the realtime logic is split across both sides without a clear contract.


A few practical rules help:


  • Keep all media and signaling logic in a client component.

  • Pass only serializable session data from the server to the client.

  • Do not create WebSocket or peer connection objects during SSR.

  • Make teardown explicit on route change and component unmount.


If your sales flow is a multi-step experience, treat the avatar session as a finite-state machine. Example states: idle, requesting-session, signaling, connecting-media, live, failed. When a bug happens, log transitions with timestamps. That one habit makes it much easier to tell whether you are dealing with a startup delay, a network issue, or a UI race.


Also, keep in mind that some “realtime” failures are simply timeout mismatches. If your session creation expires in 30 seconds but your UI waits for a user to click through a modal first, the media connection may start with stale credentials. Capture the creation time, show remaining validity in logs, and refresh the session before connecting if the window is narrow.


Where Protoface fits in this flow


This is the part where the backend surface matters. If you are creating or managing sessions directly, the REST API at api.protoface.com is the place to verify auth, session creation, and server-side errors before you involve the browser. For example, a quick smoke test with curl can tell you whether your key is valid and whether the endpoint is returning the session payload you expect:


curl https://api.protoface.com/<relevant-endpoint> \
-H "Authorization: Bearer sk_live_..."
curl https://api.protoface.com/<relevant-endpoint> \
-H "Authorization: Bearer sk_live_..."
curl https://api.protoface.com/<relevant-endpoint> \
-H "Authorization: Bearer sk_live_..."


Exact paths and fields depend on the operation, so use the docs for the current schema. The useful debugging pattern is simple: confirm the API works in isolation, then move to the browser connection, then inspect media. If the server side is healthy but the browser still fails, the bug is almost always in signaling, ICE, or client lifecycle.


If you are integrating a voice agent, the LiveKit plugin is also a practical place to isolate issues because it collapses “agent speaks” and “avatar moves” into one runtime. The plugin is published as pipecat-protoface, and the accompanying integration docs are a good reference for how the avatar service is wired into the agent pipeline. When a voice agent works but the face does not, compare the audio track lifecycle with the video track lifecycle; mismatches there usually point to plugin configuration or session setup rather than browser transport.


If you prefer a higher-level implementation example, the Python SDK repository is useful for seeing session management patterns end to end: protoface-sdk-python. That is especially handy when you want to create sessions from a backend, log identifiers, and then hand the browser only the minimum connection data it needs.


Conclusion


Debugging a realtime avatar sales flow is mostly about reducing ambiguity. First prove the session API works. Then prove the WebSocket upgrades cleanly. Then prove WebRTC establishes ICE connectivity. Finally prove that the remote track is attached and allowed to play in the browser. If you keep those layers separate, most “mysterious” bugs become ordinary transport or lifecycle issues.


For implementation details, browser-side gotchas, and current API shapes, start with docs.protoface.com. If you want a working reference for your integration path, use the relevant SDK or plugin repo as a baseline, and instrument your Astro app so each state transition is visible in logs. That will save you far more time than chasing symptoms in the UI.

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.