RTC (real-time communication) is how you add live audio and video calls to your app: a one-to-one support call, a doctor's video visit, a team meeting, a classroom. This page explains how a call works and shows the basic code. Read it before the other RTC pages.
Before you start: you need a project, an API key, and a backend endpoint that makes tokens. If you don't have those yet, do the Quickstart first. It takes about 10 minutes.
Where RTC fits
A call always involves two pieces of your code:
- Your backend decides who may join which room, and with which rights. See RTC authentication.
- Your app joins with the grant, turns on the camera and mic, and shows the other people. That's what this page covers.
You never deal with low-level WebRTC (SDP, ICE, RTCPeerConnection).
WebRTC is the browser technology that carries live audio and video; the
SDK runs all of it for you.
The main parts
Room one call
├── localParticipant you: your identity and your tracks
└── remoteParticipants everyone else in the room
Track one stream of media
├── LocalTrack something you send (camera, mic, screen)
└── RemoteTrack something you receive from someone else
| Word | Meaning | Example |
|---|---|---|
| Room | One call. Everyone with a token for the same room is in the same call. | demo-room, or one room per support ticket |
| Participant | One person (or device) in the room. Their identity is the user id your backend put in the token. | alice, bob |
| Track | One stream of audio or video. A person usually has a camera track and a mic track. | alice's camera |
| Publish | Send one of your tracks to the room. | alice turns on her camera |
| Subscribe | Receive someone else's track. The SDK does this for you. | bob sees alice's camera |
Every track has a kind: 'camera' | 'microphone' | 'screenShare' | 'unknown'.
Use it to decide where to show the track, for example a screen share in a
big tile.
Data messages (small text or bytes sent to the room) are not tracks. See Rooms & Participants.
What happens when someone joins
- Your app asks your backend for a grant.
- Your app calls
join(). The SDK opens a connection to Livqeno with the token. - Livqeno checks the token: right project, right room, not expired.
- Livqeno connects the user to a media server (an SFU). The SFU receives each person's video once and forwards it to everyone else.
- If the user's network blocks direct connections (common on office Wi-Fi
and cellular), the media goes through a TURN relay. The relay
details are in the grant's
iceServers, which is why you must forward it unchanged. For each relay it lists UDP first, then TCP, thenturns:(TURN over TLS) when the server has a certificate — the last one is what gets through a network that only allows HTTPS-like traffic. join()resolves. The user can now see others and turn on their camera.
Join a room
In your browser app, after you get the grant (resp) from your backend:
import { createRTCClient } from '@livqeno/rtc';
const client = createRTCClient({
token: resp.token,
endpoint: resp.endpoint,
iceServers: resp.iceServers,
});
const room = await client.join(resp.roomName);The room you join must be the room the token was made for. If it isn't,
join() throws ROOM_NOT_FOUND right away, before trying to connect. The
easy way to get this right: pass resp.roomName or resp.roomId.
See who's already there
When join() finishes, everyone already in the room is ready to use,
along with their tracks. Show them first:
for (const participant of room.remoteParticipants) {
for (const track of participant.tracks) {
videoElement.appendChild(track.attach());
}
}participantJoined and trackSubscribed only fire for people and tracks
that arrive after this point. So a call screen needs both: the loop
above for people already there, and the events below for people who come
later.
Leave a room
await room.leave();This stops your camera and mic and closes the connection. Call it when the user clicks "Leave" or closes the page, so others see you leave right away. It's safe to call even if you might already be disconnected.
Events
The room tells your app what's happening through events. Your call screen reacts to them: add a tile when someone joins, show a "Reconnecting…" banner when the network drops, and so on.
room.on('connectionStateChanged', (state) => {}); // connecting|connected|reconnecting|disconnected|failed
room.on('connected', () => {});
room.on('disconnected', () => {});
room.on('reconnecting', () => {});
room.on('reconnected', () => {});
room.on('participantJoined', (participant) => {});
room.on('participantLeft', (participant) => {});
room.on('trackPublished', (kind, participant) => {});
room.on('trackUnpublished', (kind, participant) => {});
room.on('trackSubscribed', (track, participant) => {});
room.on('trackUnsubscribed', (track, participant) => {});
room.on('trackMuted', (kind, participant) => {});
room.on('trackUnmuted', (kind, participant) => {});
room.on('localTrackPublished', (track) => {});
room.on('localTrackUnpublished', (track) => {});
room.on('dataReceived', (payload, participant) => {}); // Uint8Array
room.on('error', (error) => {}); // RTCErrorTo stop listening, call room.off(event, handler). What each event sends
and when it fires: Events.
Errors
Every error is an RTCError with a code, a message, and a cause.
You never get a raw browser DOMException, so you can show the right
message by checking one field.
The possible codes:
INVALID_TOKEN | TOKEN_EXPIRED | ROOM_NOT_FOUND | CONNECTION_FAILED
PERMISSION_DENIED | CAMERA_PERMISSION_DENIED | MICROPHONE_PERMISSION_DENIED
DEVICE_NOT_FOUND | NETWORK_ERROR | SIGNALING_ERROR | MEDIA_ERROR | TIMEOUT
Check for them like this:
import { isRTCError } from '@livqeno/rtc';
try {
await client.join(resp.roomName);
} catch (error) {
if (isRTCError(error)) console.log(error.code, error.message);
}Common mistakes
| Mistake | What you'll see | Fix |
|---|---|---|
Building iceServers by hand, or dropping it | Calls work on Wi-Fi but fail on cellular or office networks | Forward the grant from your backend unchanged. |
| Joining a different room than the token's | ROOM_NOT_FOUND | Join with resp.roomName or resp.roomId. |
Only listening to trackSubscribed | People already in the room are missing | Also loop over room.remoteParticipants after join(). |
| Reusing an old token | TOKEN_EXPIRED | Ask your backend for a fresh grant on every join. |
Next steps
A typical order to build a call screen:
| Step | Go to |
|---|---|
| 1. Get a working call on Web, React, React Native, or Flutter | Quickstart |
| 2. Decide who can publish and for how long | RTC authentication |
| 3. Add camera and mic buttons | Audio & Video |
| 4. Handle the camera/mic prompt on each platform | Permissions |
| 5. Show participants, pick devices, send data | Rooms & Participants |
| 6. Handle bad networks | Reconnection |
| 7. Show call quality (RTT, jitter, packet loss, bitrate, codec) | Diagnostics |
Also: add chat to the call, or broadcast to many viewers instead.