@livqeno/live is for browser apps that use calls and chat together,
like a meeting app with a chat panel, or a live stream with a chat
sidebar. It puts both behind one object, so you set up, join, and clean up
in one place. It also has LiveStream for live streaming.
It runs in the browser and holds only tokens: an RTC token and a chat token, both made by your backend with the TypeScript, Python, Go, Java or PHP SDK. It never sees your API key.
The API key can't live in a browser, so one small route on your server makes the tokens. Build your token endpoint has it in TypeScript, Python, Go, Java, PHP and Dart.
- Use it when your app needs both calls and chat.
- Use
@livqeno/rtcor@livqeno/chatalone when you need only one. It's a smaller download, and you skip code you don't use.
It's a thin wrapper. livqeno.rtc is a real RTCClient from @livqeno/rtc,
and livqeno.chat is a real ChatClient from @livqeno/chat. Everything
documented for those packages works here.
Install
pnpm add @livqeno/livenpm install @livqeno/liveyarn add @livqeno/livebun add @livqeno/liveSet up
First, your backend makes the tokens your page needs: an RTC token
(POST /v1/rooms/{roomId}/rtc-tokens), a chat token (POST /v1/chat/tokens),
or both. Your page fetches them from your own endpoint.
Then, in your browser app, pass only the credentials for what you use:
import { Livqeno } from '@livqeno/live';
const livqeno = new Livqeno({ token, endpoint }); // calls only
const livqeno = new Livqeno({ chatToken, chatApiUrl }); // messaging only
const livqeno = new Livqeno({ token, endpoint, chatToken, chatApiUrl }); // both| Option | For | Notes |
|---|---|---|
token | RTC | From your backend. Never make one in the browser. |
endpoint | RTC | The endpoint from the same token response. Required with token. |
iceServers | RTC | Pass it as-is. Never build your own. |
telemetry | RTC | Default true. Best-effort. Never blocks a call. |
telemetryUrl | RTC | From the same token response. |
autoReconnect | RTC | Default true. |
chatToken | Chat | From POST /v1/chat/tokens. |
chatApiUrl | Chat | The apiUrl from the chat-token response. |
chatUrl | Chat | Worked out from chatApiUrl if you leave it out. |
onChatTokenExpiring | Chat | Return a new token and the connection reconnects itself. |
logLevel | Both |
If you set chatToken, you also need chatApiUrl or telemetryUrl (they're
usually the same host). Without either, the SDK throws right away.
Basic use
A typical flow: the user joins the call, turns on camera and mic, opens the
chat, and later leaves. leave() exits the call but keeps chat open (useful
if people keep chatting after the call ends). dispose() closes everything,
for example when the user closes the page.
const room = await livqeno.join('room_123');
await room.enableCamera();
await room.enableMicrophone();
await livqeno.chat!.connect({ room: 'room_123' });
await livqeno.chat!.sendMessage({ text: 'Hello' });
await livqeno.leave();
await livqeno.dispose();For anything else, use the real clients directly:
livqeno.rtc?.getDevices('videoinput');
livqeno.chat?.messages.list({ limit: 50 });livqeno.chat is undefined if you passed no chat credentials. That's why the
examples use !. Check it in your code.
Events
Listen on the room and the chat client directly. There's no combined event emitter:
const room = await livqeno.join('room_123');
room.on('participantJoined', (p) => console.log(p.identity));
livqeno.chat?.on('message', (m) => console.log(m.text));| Call | Returns |
|---|---|
room.on() | The room, so you can chain calls |
chat.on() | A function that unsubscribes |
See RTC events and Chat events.
Live streaming
A live stream is a call and a chat working together: the host's video plus
a chat for viewers. LiveStream joins both in one step.
Your backend decides each person's role before they join. It calls
addHost() for a host, or createViewerToken() for a viewer, and sends the
result to the page. In your browser app:
import { LiveStream } from '@livqeno/live';
const live = await LiveStream.join(credentials); // from addHost() or createViewerToken()
if (live.isHost) {
await live.room.enableCamera();
await live.room.enableMicrophone();
}
live.chat?.on('message', (m) => appendChat(m));
await live.react('❤️');
await live.leave();| Member | Type / what it does |
|---|---|
streamId | string |
identity | string | undefined |
role | 'HOST' | 'CO_HOST' | 'VIEWER' |
isHost | boolean |
delivery | 'rtc' | 'hls' — how this viewer is watching: a live call, or an HLS video stream |
room | Room, a normal RTC room |
chat | ChatClient | undefined |
react(emoji) | Adds a reaction to the stream's main chat message |
raiseHand() / lowerHand() | A viewer asks to come on stage, or cancels that request. Needs the chat field on the join credentials. |
switchRole(credentials) | Switches to a new role using new credentials, without leaving and rejoining |
leave() | Leaves both the room and the chat |
react() throws if the credentials had no chat field. Reactions are stored
in chat, so without chat there's nowhere to put them.
HLS playback
A BROADCAST stream's viewer gets delivery: 'hls' instead of joining a
Room. Call attachPlayer() with a <video> element, and the stream plays
there:
if (live.delivery === 'hls') {
await live.attachPlayer(videoElement);
}
// Later, or automatically on leave():
live.detachPlayer();live.setPreferredQuality('medium'); // 'auto' | 'high' | 'medium' | 'low'
live.preferredQuality; // 'medium'setPreferredQuality() picks the video quality this viewer gets, whether
they're on a live call or watching HLS. 'auto' lets Livqeno choose for them.
The setting carries over if they later switchRole() into a new room. More
on scaling a large audience in Audience scale.
Events
live.on(event, handler) listens for things that happen during the stream.
Calling it returns a function you can call later to stop listening:
const unsubscribe = live.on('stage', (event) => {
// event.action: 'raise_hand' | 'lower_hand' | 'invited' | 'promoted' | 'demoted'
if (event.action === 'invited' && event.forMe) showGoLivePrompt();
});
live.on('roleChanged', (role, room) => setRole(role));
live.on('transcriptionSegment', (segment) => appendCaption(segment));
live.on('disconnected', (reason) => showReconnecting());
unsubscribe(); // stop listening| Event | Payload |
|---|---|
stage | LiveStreamStageEvent — someone raised or lowered their hand, or the server invited, promoted, or demoted them. See Stage. |
roleChanged | (role, room) — fires after switchRole() finishes |
permissionsChanged | This participant's RTC permissions changed |
localTrackMutedByServer | A moderator muted one of this participant's tracks |
disconnected | reason — the call disconnected |
transcriptionSegment | LiveStreamTranscriptionSegment — one piece of a live caption. It may still be changing, or it may be the final version. See Live Transcription. |
transcriptionStateChanged | LiveStreamTranscriptionState — whether transcription is starting, running, or has stopped in this room |
agentStateChanged | An AI voice agent in the room changed state (listening, thinking, or speaking). See AI Voice Agents. |
Errors
Errors come from the underlying packages. Use their checks. A common one to
handle is TOKEN_EXPIRED: ask your backend for a fresh token and try again.
import { isRTCError } from '@livqeno/rtc';
import { isLivqenoChatError } from '@livqeno/chat';
try {
await livqeno.join('room_123');
} catch (error) {
if (isRTCError(error) && error.code === 'TOKEN_EXPIRED') await refresh();
}All codes: Errors.
Example
examples/live-streaming-demo is a two-browser demo built on LiveStream:
one host tab publishes, one viewer tab watches, with live chat and reactions.
Common mistakes
| Mistake | What you'll see | Fix |
|---|---|---|
chatToken without chatApiUrl (or telemetryUrl) | The SDK throws right away. | Pass apiUrl from the chat-token response as chatApiUrl. |
Using livqeno.chat! when no chat credentials were passed | livqeno.chat is undefined. | Pass chat credentials, or check livqeno.chat first. |
Calling react() with credentials that have no chat field | It throws. | Reactions live in chat; make sure the credentials include chat. |
Calling leave() and expecting chat to close | Chat stays connected. | Call dispose() to close both. |
Next steps
- Next.js: the individual packages underneath, and Next.js setup.
- Build a live stream: a full host-and-viewer app.
- Live Streaming: how hosts, viewers, and chat fit together.