livqenoDocs

GuidesBuild a live stream

Build a live stream

One host broadcasts to many viewers, with live chat and reactions.

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/live installed in your app with npm, pnpm, yarn or bun (it contains LiveStream).

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 }. rtc is a call token, chat is 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.
  • addHost gives the CO_HOST role unless you pass role: '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 → LIVE

4. 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.

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, terminal

When 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.room is a real Room.
  • Roles are just permission sets. HOST, CO_HOST, and VIEWER differ 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. So react() throws if there are no chat credentials, and chatRootMessageId is part of the credentials.
  • Status only moves forward: CREATED → LIVE → ENDED. A wrong move returns RAVEN_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 LIVE stream keeps its room open. Call end() when the host leaves.

Next steps