Header Logo

Why Your Realtime Avatar Works in P2P but Fails in an SFU: Common WebRTC Debugging Steps

Why Your Realtime Avatar Works in P2P but Fails in an SFU: Common WebRTC Debugging Steps

WebRTC SFU avatar debugging: track publication, subscription, RTP stats, frame pacing, lip sync, and rendering checks.

Introduction


If your realtime avatar looks fine in a peer-to-peer WebRTC call but falls apart once you put it behind an SFU, you are usually not dealing with “avatar quality” at all. You are debugging transport, timing, and media-state assumptions that happened to work in a simple topology and then stopped being true in a routed one.


By the end of this post, you should be able to identify the common failure modes, tell whether the problem is in signaling, media flow, or rendering, and apply a few concrete debugging steps that work for both browser clients and voice-agent pipelines.


Start with the topology, not the avatar


In P2P, each endpoint sends media directly to the other. There is one ICE session, one DTLS/SRTP path, and typically one sender/receiver pair to reason about. If the avatar’s video track is produced correctly, the remote peer gets it directly. Latency and packet loss are still real, but the graph is simple.


In an SFU setup, the sender publishes to the server, and the SFU forwards to subscribers. That changes a few assumptions:


  • Negotiation can succeed even when forwarding fails.

  • Media may be flowing upstream but not subscribed downstream.

  • Codec, resolution, or simulcast/SVC choices can be altered by the server path.

  • Timing jitter becomes more visible because the SFU adds another buffering and routing layer.


For an avatar, this usually shows up as one of three symptoms: black video, frozen lips while audio continues, or “works for a few seconds and then degrades.” Those are different bugs.


Failure mode 1: the track exists, but the subscriber never receives it


This is the most common SFU-specific mistake. The publisher believes it is sending a video track, but the subscriber is either not subscribed, subscribed to the wrong track, or receiving a track that the renderer never attaches.


Debug this in order:


  1. Confirm the publisher actually created a live video track.

  2. Confirm the SFU sees the publication.

  3. Confirm the remote side receives a subscription event.

  4. Confirm the renderer binds the incoming track to a visible element.


In browser logs, these tend to look like “published,” “subscribed,” and “track attached” events, but one of the middle steps is missing. In P2P, people often skip these distinctions because publication and attachment happen almost together. In SFU land, you cannot assume that.


A practical trick: if your avatar is sending video but the remote side shows no frames, inspect the inbound RTP stats rather than the UI first. If bytes and packets are increasing, the issue is probably in attachment or rendering. If they are flat, the issue is upstream publication or routing.


Failure mode 2: the track is live, but the video is frozen or badly delayed


Animated avatars are usually generated from a stream of synthesized frames, not a pre-rendered movie. That means timing matters more than with ordinary camera video. If the SFU adds buffering, the avatar can appear to “lag behind” the conversation or freeze during bursty network conditions.


Three things commonly bite developers:


  • Frame pacing: the generator produces frames irregularly, then the sender or SFU smooths them in a way that breaks lip-sync.

  • Keyframe cadence: the subscriber joins late and never gets a fresh keyframe quickly enough to display the stream.

  • Adaptive bitrate behavior: the connection downshifts aggressively, and the avatar looks stuck even though audio still works.


When this happens, check:


  • Outbound video FPS from the sender.

  • Remote jitter buffer delay.

  • Packet loss and retransmits.

  • Whether the stream is using a codec profile the SFU and browser both handle well.


For browser debugging, chrome://webrtc-internals is still the fastest place to start. Look at the peer connection stats before you start changing code. If outbound video frames are increasing but inbound decode frames are not, the transport path is the likely culprit. If decode frames are increasing but the element is frozen, the problem is downstream of WebRTC.


Failure mode 3: audio and video drift apart


Realtime avatars are usually judged by lip sync. If the video is technically “working” but the mouth movements trail the agent’s speech, users will still call it broken.


In P2P, sync drift can be masked by a straightforward path and lower buffering. In an SFU, drift becomes more visible because audio and video may traverse different buffering and scheduling paths. That is especially true if the avatar video is generated from text or phonemes and audio is coming from a separate TTS stream.


The useful distinction is this:


  • Media sync problem: the sender generated misaligned audio/video timestamps.

  • Transport sync problem: the sender was aligned, but the SFU or receiver introduced delay on one stream.

  • Application sync problem: the avatar is rendering frames on arrival instead of on presentation time.


To isolate this, compare timestamps at the source with timestamps at the receiver. If the source is already off, fix generation. If the source is aligned and the receiver drifts, inspect buffering, jitter, and render scheduling.


Failure mode 4: the browser renders nothing because the element is correct but the media pipeline is not


This one wastes a lot of time because it looks like a WebRTC failure when it is actually a DOM or autoplay issue. In an SFU flow, the remote track can be healthy while the <video> element is muted, hidden, zero-sized, or blocked by autoplay policy.


Check the boring things first:


  • The element is actually in the DOM and visible.

  • The stream is assigned to srcObject.

  • Autoplay is allowed or the stream starts after a user gesture.

  • The video element has nonzero dimensions and CSS is not collapsing it.


If you are rendering an avatar as a canvas or compositing it into another UI, verify that the incoming track is decoded before it is handed to your render layer. A surprising number of “avatar bugs” are just “the app never attached the track to the thing that paints pixels.”


WebRTC debugging steps that actually help


When an avatar works in P2P but not in an SFU, I usually debug in this order:


  1. Prove the source is valid. Publish a known-good video source first. If a static test stream works and the avatar stream does not, the issue is in generation or pacing.

  2. Check publication/subscription separately. Don’t assume “connected” means “receiving media.”

  3. Inspect stats, not just logs. RTP bytes, packets, jitter, frames decoded, and keyframe count are more useful than generic connection messages.

  4. Reduce variables. Test with one subscriber, one codec, and no extra transforms.

  5. Verify timestamps and frame rate. Avatar pipelines are sensitive to bad pacing and timestamp discontinuities.


Also, make sure you know where the problem lives. SFUs often get blamed for issues that are actually caused by sender-side backpressure, bad media timestamps, or renderer bugs. If the same avatar works in a direct call but fails in routed delivery, the SFU is usually exposing an assumption your client made about timing or subscriptions.


How Protoface fits into this


This is exactly the kind of integration where a reusable avatar layer helps. If you are using the Protoface LiveKit plugin, the avatar is added as a video surface inside an agent pipeline instead of being hand-wired as a separate media experiment. That does not remove WebRTC debugging, but it gives you a more predictable starting point: a known avatar publisher, a known agent runtime, and a narrower place to inspect track publication and subscription behavior.


For example, when you drop the avatar into a LiveKit voice agent, you can focus on whether the agent is publishing a valid video track and whether the room is subscribing cleanly, rather than first rebuilding your own media generator from scratch. The plugin and examples are a good reference if you want to compare your app against a known working setup: https://github.com/protoface-ai/protoface-plugin-pipecat and https://docs.pipecat.ai/api-reference/server/services/video/protoface.


If you are integrating outside that path, the same principle still applies: use the REST API to create and manage sessions, then validate the session lifecycle independently from the browser renderer. A simple request flow is often enough to prove that the control plane is healthy before you chase media bugs:


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


The exact fields depend on the endpoint and your setup, so use the docs for the current schema: https://docs.protoface.com.


A small checklist before you blame the SFU


When the avatar fails only in routed media, I would sanity-check these in sequence:


  • Does the sender have a real outbound video track?

  • Does the SFU show that track as published?

  • Does at least one subscriber receive it?

  • Are inbound RTP stats increasing on the remote side?

  • Is the video element attached, visible, and allowed to autoplay?

  • Are audio/video timestamps and frame pacing stable?


If the answer to any of those is “no,” you have narrowed the problem enough to stop guessing.


Conclusion


P2P hides a lot of media assumptions because the path is short. An SFU makes those assumptions visible. That is why a realtime avatar can look fine in a direct call and then fail as soon as you route it through a server: subscription state, buffering, timestamps, rendering, and adaptive bitrate all start to matter in ways that are easy to miss.


The practical approach is to debug the pipeline layer by layer: prove publication, prove subscription, inspect stats, then verify rendering and sync. If you are building with a managed avatar surface instead of assembling everything yourself, start from the docs and a known-good integration path, then move outward from there. For API and integration details, see https://docs.protoface.com.

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.