livqenoDocs

RTCSignaling Protocol

Signaling Protocol

The raw RTC WebSocket messages — for building a client in a language Livqeno has no SDK for.

Read this page if you are:

  • Writing a client in a language Livqeno has no SDK for, or
  • Reading WebSocket messages in your browser's devtools.

What signaling is: before audio and video can flow, the device and the media server must agree on how to connect: which formats to use and which network path works. They agree by exchanging small JSON messages over a WebSocket. That exchange is signaling. The audio and video themselves travel separately, over WebRTC, not over this socket.

Words used on this page:

WordMeaning
SDP offer / answerA text description of the media a side wants to send and receive. One side offers, the other answers.
ICE candidateOne possible network path (an address and port) to reach a device.
GlareBoth sides send an offer at the same time.

Endpoint

wss://api.livqeno.com/v1/rtc?token=<rtc token>
  • Use the endpoint from the token grant as the address. It already points at the right host and ends in /v1/rtc. Don't hard-code it.
  • The token goes in the URL, because browsers can't set headers on a WebSocket connection.
  • Always use wss:// in production. Over plain ws://, anyone could read the token.

You talk to the server, not to other users

An older version of this protocol sent messages between browsers, with a targetParticipantId field. Some message names are the same, but their meaning changed. If a document says the server forwards sdp.offer from another browser, it's out of date.

Messages

Every message is a JSON object with a type field. All fields sit at the top level.

Client → server

TypeFieldsWhat it does
room.joinroomId?, region?, resume?, autoSubscribe?Send this first. If you include roomId, it must match the token. After a dropped socket, send resume: { token } with the latest resume token to pick the old session back up. autoSubscribe: false starts every video you receive paused until you ask for a layer (audio always plays); leave it out for the normal behaviour.
token.refreshtokenHand the live session a new token (same room and identity) after token.expiring.
room.leave—Leave the room.
sdp.answersdpAnswer the server's offer. (The usual case: the server offers first.)
sdp.offersdpYour own offer, sent when you start publishing.
ice.candidatecandidate, sdpMid?, sdpMLineIndex?, usernameFragment?Send network candidates as you find them. The fields sit flat on the message.
track.mutemuted, sourceMute or unmute a track you're sending, without removing it.
track.publishsourceSay what a track is: camera, microphone, or screen share.
subscription.updatepublisherId, trackId, layerPick a video quality: low, medium, high, auto, or off to pause that video for you (a hidden tile) without renegotiating. Any other value resumes it.
ping—Keep the connection alive.

Why track.publish? WebRTC itself can't say whether a track is a camera or a screen. Without this message, the server has to guess.

Server → client

TypeWhat it carries
room.joinedroomId, current participants and their tracks, and rtcServer / region (the media server's name and region, for support). clientRegion is the region you were placed in (see Regions). e2ee: true appears only in an end-to-end encrypted room: don't publish unencrypted media into it. Also resumeToken, and on a join that asked to resume, resumed: true if the old session was kept (keep your PeerConnection) or false (build a new one)
room.resume_tokenA fresh resumeToken, about once a minute. Keep the latest; each lasts about two minutes
room.leftroomId, and reason (REMOVED, ROOM_CLOSED, TOKEN_EXPIRED, TOKEN_REVOKED) when the server ended the session. Don't reconnect after REMOVED or ROOM_CLOSED; after TOKEN_EXPIRED or TOKEN_REVOKED, only with a new token
token.expiringexpiresAt. Sent a minute before the token expires. Reply with token.refresh
token.refreshedexpiresAt. The new token was accepted. A refused one gets an error with TOKEN_INVALID instead, and the session keeps its old token
participant.permissions_updatedpermissions. What you may now do. Stop publishing anything you lost the right to; the server has already stopped forwarding it
participant.joinedThe person who joined
participant.leftThe person who left
track.publishedSomeone started sending a track
track.unpublishedSomeone stopped
track.muted / track.unmutedSomeone muted or unmuted a track. byServer: true on the copy sent to you about your own track means the server muted it: mute it locally
sdp.offerThe server's offer. Sent on join, when the room's tracks change, and to restart ICE after your connection has been disconnected for a few seconds. Answer them all the same way
sdp.answerThe server's answer to your offer
ice.candidateNetwork candidates from the server
connection.stateThe real connection progress (ICE and DTLS) as the server sees it
speakers.changedspeakers: who is talking, loudest first, up to 8, each with a level from 0 to 1. The whole list each time; empty means nobody. At most every 300 ms
quality.changedupdates: participantId and quality (excellent, good, poor, lost) for the people whose connection quality changed. Anyone not listed keeps their last score
subscription.statepublisherId, trackId, state, layer: what you are really receiving of one video. state is active, paused_bandwidth (your bandwidth can't fit it right now) or paused_client (you asked for off). Show quality badges from this, not from the layer you asked for
publisher.layerstrackId, layers (rid, active): which quality layers of your own video anyone is watching. Turn the others off on your sender to save upload. Ignoring it is safe: you just keep sending every layer
errorcode, message
pongReply to ping

speakers.changed and quality.changed are about the whole room. subscription.state and publisher.layers are about you alone. After a resumed session, you get anything you missed after room.joined, in order. Older servers never send these four; that's fine, nothing else depends on them.

The connection flow

Here's a full join, as messages. → is you sending, ← is the server.

→  (upgrade with ?token=…)
→  {"type":"room.join"}
←  {"type":"sdp.offer","sdp":"v=0…"}
←  {"type":"ice.candidate","candidate":"candidate:… typ host …","sdpMid":"0","sdpMLineIndex":0}
←  {"type":"room.joined","roomId":"…","participants":[…],"rtcServer":"sfu-01","region":"ap-south"}
→  {"type":"sdp.answer","sdp":"v=0…"}
→  {"type":"ice.candidate","candidate":"candidate:…","sdpMid":"0","sdpMLineIndex":0}
←  {"type":"connection.state","state":"connected"}

Heartbeat

A WebSocket can look open even after the network is gone. Pings let both sides notice a dead connection quickly.

Send ping every30 seconds
Connection closes after no traffic for60 seconds (one missed ping)

Errors

The error message can have one of 20 codes:

INVALID_TOKEN, TOKEN_EXPIRED, TOKEN_REVOKED, TOKEN_INVALID, UNAUTHORIZED, ROOM_NOT_FOUND, ROOM_FULL, ROOM_CLOSED, INVALID_MESSAGE, INVALID_MESSAGE_TYPE, PARTICIPANT_NOT_FOUND, NOT_IN_ROOM, PERMISSION_DENIED, ORIGIN_NOT_ALLOWED, RATE_LIMITED, USAGE_LIMIT_EXCEEDED, NO_RTC_CAPACITY, RTC_SERVER_UNREACHABLE, NEGOTIATION_FAILED, NEGOTIATION_GLARE.

When the server closes the socket, it uses a 4xxx close code:

CodeMeaning
1012Server restarting. Reconnect within 0–2 s (random), with resume
4001Authentication failed, or the token expired or was revoked mid-session. Get a new token, then reconnect
4002Replaced by a newer session
4003Removed from the room, or the room closed. Don't reconnect
4004Media server lost (reconnect)
4029Rate limited

More detail: Close codes. Which errors you can retry: Errors.

These codes are separate from the RAVEN_* HTTP error codes. The socket protocol has its own versions and its own codes.

Limits

LimitDefaultVariable
Frame size16 KBSIGNALING_MAX_MESSAGE_BYTES
Messages per connection, per window100 / 10sSIGNALING_MAX_MESSAGES_PER_WINDOW
Connection attempts per IP, per window20SIGNALING_MAX_CONNECTIONS_PER_WINDOW
Participants per room50SIGNALING_MAX_PARTICIPANTS_PER_ROOM
  • The message limit is per connection.
  • The connection limit is per IP address. It's checked before the token is checked.

You never get a media server address

room.joined tells you the server's name and region, so you can share them with support. But you always connect to the same endpoint, and Livqeno picks the media server for you. There's no server address to set up.

Next steps