Direct WebRTC (LiveKit & Daily)
Publish the avatar straight into your LiveKit or Daily room with one extra argument to OjinSTVClient. No media relay through your backend, and the lowest latency for your viewers.
By default, OjinSTVClient streams the avatar's frames back to your process over the WebSocket. If your viewers are in a LiveKit or Daily room, Ojin can publish the avatar straight into that room instead. You add one argument and the rest of your program stays the same.
Requires ojin-client 0.11.0 or later (pipecat-ojin 0.1.5 or later for Pipecat).
How it works
Your backend creates the room and a token for the avatar with your LiveKit or Daily account.
You pass the room URL and token to
OjinSTVClientthroughWebRTCSettings.Ojin joins the room as a participant named
ojin-avatarand publishes the avatar's audio and video there.Your viewers (browser, widget, or app) watch the avatar in the room like any other participant.
Your backend still sends TTS audio and receives events over the same WebSocket, but it carries only a lightweight control channel. No audio or video flows through your backend. The SDK doesn't join the room itself, and it doesn't create rooms or tokens.
WebRTC or WebSocket?
Where the media goes
to your process, via output_stream()
straight into your LiveKit or Daily room
Latency to viewers
adds your relay hop
lowest, no relay
Backend cost
you decode and re-publish every frame
control messages only
Frames in your process
yes
no
Choose it when
you render, record, or post-process frames yourself
your viewers are already in a LiveKit or Daily room
Prerequisites
Everything from Build with the Python SDK: an API key, a
config_id, andojin-client[stv]installedA LiveKit or Daily account, a room, and a token for the avatar
Mint an access token for the avatar. It only needs to join the room and publish:
Mint the token with the identity ojin-avatar: on LiveKit a participant's identity comes from the token, so that claim is what your viewers use to tell the avatar apart from everyone else in the room.
WebRTCSettings field
Value
provider
WebRTCProvider.LIVEKIT
room_url
your LiveKit server URL, wss://<your-project>.livekit.cloud
token
the access token above
Create a room and a meeting token for it with the Daily REST API:
WebRTCSettings field
Value
provider
WebRTCProvider.DAILY
room_url
the room URL, https://<your-domain>.daily.co/<room>
token
a meeting token for that room
Keep room credentials on your backend and never send the avatar's token to a browser.
Switch to direct WebRTC
Pass webrtc=WebRTCSettings(...). Leave it out and the same code runs over the WebSocket.
provider also accepts the plain strings "livekit" and "daily", if you carry the value in config. Everything else works as before: start(), start_turn(), send_tts_audio(), say(), interrupt(), close(), async with, and every event.
output_stream() is empty in this mode. The audio and video go to the room, not to your process, so output_stream() and any custom STVOutput receive no frames. The stream ends when the session closes. Use events for speaking state and timing.
Events
SESSION_READY
the session is live and ready for audio
WEBRTC_CONNECTED
the avatar has joined the room (participant_id)
FIRST_FRAME
the first avatar frame is out (frame_type)
BOT_STARTED_SPEAKING
the avatar starts speaking a turn
BOT_STOPPED_SPEAKING
the avatar finishes a turn
INTERRUPTED
interrupt() took effect. In this mode it also fires when the barge-in only discarded input buffered before the room was open. Use interrupt()'s return value to tell the two apart
ERROR
an error occurred (message, fatal, plus code for errors that carry one)
CLOSED
the session has been torn down
FIRST_FRAME fires in WebSocket mode too.
Errors
The client never falls back to the WebSocket silently. If the avatar can't get into the room, or later drops out of it, you get a fatal ERROR and the session closes. Each cause has its own code, so you can branch on it without reading the message:
code
What happened
What to do
WEBRTC_AUTH_FAILED
the room rejected the avatar's token
check the token is for the room in room_url, hasn't expired, and allows publishing
WEBRTC_NETWORK_FAILED
Ojin couldn't reach the room
retry the session; if it persists, check your provider's status
WEBRTC_INVALID_SETTINGS
the room URL or provider was unusable
check room_url matches the provider (wss:// for LiveKit, the room URL for Daily)
WEBRTC_JOIN_TIMEOUT
the session wasn't ready within webrtc_join_timeout_s
raise the timeout. It covers the model's cold start as well as the room join
WEBRTC_ROOM_LOST
the avatar dropped out of the room mid-session
start a new session
WEBRTC_NOT_SUPPORTED
the server returned no room result, so this session can't be published into a room
connect without webrtc to receive frames over the WebSocket, or contact support
WEBRTC_JOIN_FAILED
the join failed for a reason this SDK doesn't recognise
read message for the server's own code, retry, then contact support
Invalid settings, such as an unknown provider or an empty room_url or token, raise ValueError when you construct WebRTCSettings.
Pipecat
Pass the same WebRTCSettings to OjinVideoService. The service pushes no media in this mode, so turn off your transport's audio and video output:
Lifecycle frames are unchanged. See the Pipecat integration for the full pipeline.
Where to go next
Last updated
Was this helpful?