In this guide you'll build a live stream page, like a webinar, a live class, or a product launch: one person presents, many people watch and chat.
Where this fits: a live stream is a call and a chat working together, set up for you in one step. If you've built a call or a chat, most of this will look familiar. The new part is roles: hosts can send video, viewers can only watch.
What you'll build
- A host who sends camera and mic.
- Viewers who watch.
- Live chat and emoji reactions.
Host and viewers join the same stream, but your backend gives them different credentials (the set of tokens a person joins with). That's what stops a viewer from turning on their camera.
What you need
- A project and an API key, and a backend with
@livqeno/server. @livqeno/liveinstalled in your app with npm, pnpm, yarn or bun (it containsLiveStream).
Steps
1. Create the stream
On your backend, create the stream when the host schedules it:
const stream = await livqeno.liveStreams.create({
title: 'Friday Q&A',
hostIdentity: 'user-1', // required: a stream is created with its host
visibility: 'PUBLIC',
});This creates a room (for the video) and a chat conversation (for live chat) for you. You don't create them yourself.
2. Make credentials (a different call for each role)
On your backend, make credentials when someone opens the stream page. Which method you call decides the role, so check in your own code first that the person really is the host:
// The host, or an additional co-host.
const hostCredentials = await livqeno.liveStreams.addHost(stream.id, { identity: 'user-1' });
// A viewer.
const viewerCredentials = await livqeno.liveStreams.createViewerToken(stream.id, 'user-99');- Both return the same shape:
{ streamId, identity, role, rtc, chat, chatRootMessageId }.rtcis a call token,chatis a chat token. - It's ready for
LiveStream.join(). Send it to the app unchanged. - A viewer's RTC token has
publish: false. A viewer's app can't change that. addHostgives theCO_HOSTrole unless you passrole: 'HOST'. Both roles get the same permissions; the difference is only a label.
3. Go live
A new stream starts in CREATED, a waiting state. On your backend, start
it when the host clicks Go live:
await livqeno.liveStreams.start(stream.id); // CREATED → LIVE4. Join (host or viewer)
In your app, the same code joins hosts and viewers. The credentials
decide what each person can do, so you only check isHost to decide whether
to turn on the camera:
import { LiveStream } from '@livqeno/live';
const live = await LiveStream.join(credentials);
if (live.isHost) {
await live.room.enableCamera();
await live.room.enableMicrophone();
}
live.room.on('trackSubscribed', (track) => {
stage.append(track.attach());
});
live.chat?.on('message', (m) => appendChat(m));
await live.react('❤️');live.room is a normal Room and live.chat is a normal ChatClient.
Everything you know from calls and chat works here.
You should now see the host's video in a viewer's tab, and chat messages in both.
'use client';
import { LivqenoLiveStream, useLiveStreamRole, useCamera } from '@livqeno/react';
function StreamPage({ credentials }) {
return (
<LivqenoLiveStream credentials={credentials}>
<Stage />
</LivqenoLiveStream>
);
}
function Stage() {
const { isHost } = useLiveStreamRole();
const camera = useCamera();
return isHost ? <button onClick={() => camera.enable()}>Go live</button> : <ViewerGrid />;
}final stream = await LivqenoLiveStream.join(credentials);
if (stream.isHost) {
await stream.room.enableCamera();
await stream.room.enableMicrophone();
}
stream.chat?.messages.listen((m) => setState(() => _chat.add(m)));
await stream.react('❤️');5. Count viewers and end the stream
On your backend, read the live viewer count, and end the stream when the host is done:
const live = await livqeno.liveStreams.get(stream.id);
console.log(live.viewerCount, live.peakViewerCount);
await livqeno.liveStreams.end(stream.id); // LIVE → ENDED, terminalWhen a viewer leaves, tell the API. This keeps the viewer count and the
live_stream.viewer_left webhook correct:
await livqeno.liveStreams.leave(stream.id, 'user-99');How it works
- A stream = a room + a conversation. The credential calls make an RTC
token and a chat token together. That's why
LiveStream.roomis a realRoom. - Roles are just permission sets.
HOST,CO_HOST, andVIEWERdiffer only in what their tokens allow. The role depends on which call your backend made, never on what the app sends. - Reactions use chat. Each stream gets a starting chat message, and
react()adds a reaction to it. Soreact()throws if there are no chat credentials, andchatRootMessageIdis part of the credentials. - Status only moves forward:
CREATED → LIVE → ENDED. A wrong move returnsRAVEN_STREAM_INVALID_STATE.
Production tips
- One viewer credential per viewer. Each carries an identity. Sharing one breaks the people list and the counts.
- The viewer count is live only. It's the current number of people in the room minus the hosts. There's no history over time. See Analytics.
- No recording. If you need replays, build them outside Livqeno.
- Large audiences are untested. Livqeno's own tests reach 100 people on one machine with fake video. A 10,000-viewer stream hasn't been measured.
- Always end your streams. A forgotten
LIVEstream keeps its room open. Callend()when the host leaves.
Next steps
- Handle webhooks: get told when a stream starts, ends, or a viewer joins.
- Live Streaming · Streams & lifecycle
- Stream events: the seven webhooks.
- SDK support matrix: what each SDK supports.