Best Practices
The key things to get right for a smooth, low-latency avatar pipeline with OjinSTVClient, whether you're prototyping straight to a browser, relaying media over WebRTC, or publishing directly into a Li
Essentials (every setup)
Run it server-side. The SDK connects to Ojin over a server-to-server WebSocket. Run it in a backend process, never on an end-user device, and keep your API key off the client. In production, run it in US East, close to Ojin's inference, for the lowest latency.
Feed audio as your TTS produces it. Call
start_turn()once per utterance, thensend_tts_audio()for each chunk as it arrives, even tiny 40 ms fragments. The SDK handles batching. Don't collect several seconds or the whole utterance yourself. See Optimizing Performance for the recommended initial audio amounts and tuning.Present frames as they arrive. Don't re-sync.
output_stream()is already paced to realtime 25 fps, with audio and video in sync and a small jitter buffer. Forward each frame the moment it arrives; never hold audio to wait for a video frame, and don't add your own A/V sync layer.Handle the lifecycle. Wait for
SESSION_READYbefore relying on the stream; useBOT_STARTED_SPEAKING/BOT_STOPPED_SPEAKINGfor UI state; onERROR, retry with backoff (e.g.NO_BACKEND_SERVER_AVAILABLE).Barge-in with
interrupt(). Stop the current TTS producer and callinterrupt()when the user starts talking. Callstart_turn()before feeding the next utterance.
Setup A. Local, output to a browser (prototyping)
Run the SDK in a local Python process and stream frames to a browser you control (e.g. over a WebSocket to a <canvas>). Great for demos and development; not for production over real networks.
Forward the JPEG straight to the browser. Keep the default decoder and read the raw JPEG from
STVVideoFrame.source_bytes, then draw it to a<canvas>/<img>as it arrives. It is empty (b"") on a held tick — the tick where no new server frame was ready — so skip those and keep showing the last image. Do not use aPassthroughDecoderto skip the decode: it makes the client emit no video frames at all, so the loop below never runs. (Need raw RGB in Python instead? The default decoder gives youframe.rgbas well.)Play audio at the rate you fed. Run the browser
AudioContextat the rate you pass tosend_tts_audio().STVAudioFrame.sample_ratereports it per frame, but frames emitted before your first TTS audio default to 16 kHz — don't configure the context from the first frame you see. You can feed higher-quality TTS (e.g. 24 kHz) for better sound; the SDK plays back your original audio while lip-sync uses a 16 kHz copy.Present as they come. Draw each video frame and queue each audio chunk the instant it arrives, no re-sync; the stream is already aligned.
client = OjinSTVClient(
api_key=..., config_id=...,
)
async for frame in client.output_stream():
if isinstance(frame, STVVideoFrame):
if frame.source_bytes: # empty on a held tick
await ws.send_bytes(frame.source_bytes) # raw JPEG -> draw on a canvas
elif isinstance(frame, STVAudioFrame):
await ws.send_bytes(frame.pcm) # play at frame.sample_rateSetup B. Backend, relay to a WebRTC service (production)
Run the SDK on a backend and relay the media to your users over a WebRTC service (LiveKit, Daily, mediasoup, …), which absorbs packet loss, jitter, and varying networks that a raw WebSocket can't.
Deploy in US East, close to Ojin's inference.
Push frames into the transport with their
pts. EachSTVAudioFrame/STVVideoFramecarries aptstimestamp in monotonic nanoseconds, so convert for your transport (frame.pts // 1000for a microsecond API,frame.pts / 1e9for seconds). Hand it to your WebRTC tracks and let the transport sync them. Don't re-sync yourself.Push, don't poll, when you can. Inject a custom
STVOutputsink to write frames straight into your transport instead of drainingoutput_stream(), for lower latency and less glue. (This is exactly whatpipecat-ojindoes.)Match the video track to the model's frame size: read
STVVideoFrame.width/heightand configure your outgoing track to match.Deliver to end users over WebRTC, not the raw WebSocket. Keep the Ojin WebSocket strictly server-to-server.
Setup C. Direct WebRTC into your LiveKit or Daily room (recommended for rooms)
If your viewers are in a LiveKit or Daily room, let Ojin publish the avatar straight into it. This gives viewers the lowest latency, and your backend relays no media: it only sends TTS audio and handles events.
Pass
webrtc=WebRTCSettings(...)with the room URL and a token for theojin-avatarparticipant. Mint the token on your backend and keep it there.Set
audio_sample_rateto your TTS output rate so audio reaches the room without resampling.Give the join room to breathe. Set
webrtc_join_timeout_sto about 90 s in production; it covers the room join and the model's cold start, which alone can take more than 30 s.Drive UI state from events, not frames.
output_stream()receives no frames in this mode; useWEBRTC_CONNECTED,BOT_STARTED_SPEAKING, andBOT_STOPPED_SPEAKING.Treat
ERRORwithfatal=Trueas the end of the session. There is no fallback to the WebSocket; start a new session.Keep your own bot from hearing the avatar. If your agent is also in the room, unsubscribe it from the avatar's microphone.
See Direct WebRTC (LiveKit & Daily) for credentials, events, errors, and Pipecat.
See also
Build with the Python SDK, install, auth, events, full quickstart
Direct WebRTC (LiveKit & Daily), publish the avatar straight into your room
Optimizing Performance, the audio-feeding contract and tuning
Troubleshooting, symptoms, causes, and fixes
API Reference (advanced), the raw WebSocket protocol
Last updated
Was this helpful?