livqenoDocs

RTCOverview

RTC Overview

How a Livqeno call works — rooms, participants, and tracks — and the basic code to join, listen, and leave.

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 backendholds the API key
Livqeno
Your appbrowser or phone · holds only a token
1asks for a token for alice in demo-room
grant: token + connection details
2sends the grant
3joins demo-room with it
the media server sends alice's video to bob, and bob's to alice
Two people in a call: what each side holds.
  • 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
WordMeaningExample
RoomOne call. Everyone with a token for the same room is in the same call.demo-room, or one room per support ticket
ParticipantOne person (or device) in the room. Their identity is the user id your backend put in the token.alice, bob
TrackOne stream of audio or video. A person usually has a camera track and a mic track.alice's camera
PublishSend one of your tracks to the room.alice turns on her camera
SubscribeReceive 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

  1. Your app asks your backend for a grant.
  2. Your app calls join(). The SDK opens a connection to Livqeno with the token.
  3. Livqeno checks the token: right project, right room, not expired.
  4. Livqeno connects the user to a media server (an SFU). The SFU receives each person's video once and forwards it to everyone else.
  5. 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, then turns: (TURN over TLS) when the server has a certificate — the last one is what gets through a network that only allows HTTPS-like traffic.
  6. 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) => {});                       // RTCError

To 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

MistakeWhat you'll seeFix
Building iceServers by hand, or dropping itCalls work on Wi-Fi but fail on cellular or office networksForward the grant from your backend unchanged.
Joining a different room than the token'sROOM_NOT_FOUNDJoin with resp.roomName or resp.roomId.
Only listening to trackSubscribedPeople already in the room are missingAlso loop over room.remoteParticipants after join().
Reusing an old tokenTOKEN_EXPIREDAsk your backend for a fresh grant on every join.

Next steps

A typical order to build a call screen:

StepGo to
1. Get a working call on Web, React, React Native, or FlutterQuickstart
2. Decide who can publish and for how longRTC authentication
3. Add camera and mic buttonsAudio & Video
4. Handle the camera/mic prompt on each platformPermissions
5. Show participants, pick devices, send dataRooms & Participants
6. Handle bad networksReconnection
7. Show call quality (RTT, jitter, packet loss, bitrate, codec)Diagnostics

Also: add chat to the call, or broadcast to many viewers instead.