livqenoDocs

Live StreamingQuickstart

Live Streaming Quickstart

Create a stream, make host and viewer credentials, and join. Works on every SDK.

In this guide you'll run a complete live stream: alice hosts a "Launch Day" broadcast, and carol watches, chats, and sends a ❤️. You can run both sides in two browser tabs.

Where this fits: this is the Live Streaming version of the main Quickstart. The idea is the same: your backend makes credentials with the API key, and your app joins with them. The difference is that one call gives each person two tokens (video and chat), and the role (host or viewer) decides whether they can broadcast.

The plan:

StepWhere it runsWhat happens
1. Create a streamBackendLivqeno makes the room and the chat, and records alice as host.
2. Make the host's credentialsBackendAlice gets tokens that can broadcast.
3. Join as host and go liveApp, then backendAlice turns on her camera. Your backend marks the stream LIVE.
4. Make a viewer credential and joinBackend, then appCarol gets watch-only tokens and joins.
5. Chat and reactAppCarol and Alice talk in the stream chat.
6. End the streamBackendEveryone is disconnected. The stream is finished for good.

What you need:

  • A Livqeno project and an API key. See API credentials.
  • A backend (TypeScript, Python, Go, Java, PHP, or anything that can send HTTP) with LIVQENO_API_KEY and LIVQENO_API_URL set.
  • A client SDK in your app. See Install an SDK. On the web, Live Streaming lives in @livqeno/live.

1. Create a stream

On your backend, create the stream. Do this when your user schedules or opens an event, for example when Alice clicks Create event in your app.

This one call does three things: it creates the video room, creates the chat conversation, and records alice as the stream's HOST:

import { Livqeno } from '@livqeno/server';
const livqeno = new Livqeno({
  apiKey: process.env.LIVQENO_API_KEY,
  baseUrl: process.env.LIVQENO_API_URL, // https://api.livqeno.com
});
 
const stream = await livqeno.liveStreams.create({ title: 'Launch Day', hostIdentity: 'alice' });
// { id: 'stream_jRoD1T3EXh0PMJRGG4zYzQ', status: 'CREATED',
//   conversationId: 'conv_0iojWXJCtXZUUkDR7THBmQ',
//   chatRootMessageId: 'msg_Efm2zArYJTSRUr88BV5bZg',
//   hosts: [{ identity: 'alice', role: 'HOST' }], ... }

The room that call creates belongs to the stream. It is not listed with the rooms you make yourself (rooms.list(), and the Rooms page in the dashboard), because you run it through the stream, from its own page, never as a separate room. Closing a room disconnects everyone in it, which for a stream's room would cut the broadcast.

Save stream.id (it looks like stream_...). Every later call uses it. The stream is now CREATED: it exists, but nobody is live yet.

2. Mint the host's credentials

Creating the stream recorded Alice as host, but it didn't give her anything to join with. On your backend, call addHost to get her credentials. Do this when Alice opens the host screen, after your own check that she is allowed to host this event:

const hostCredential = await livqeno.liveStreams.addHost(stream.id, { identity: 'alice' });
// { identity: 'alice', role: 'HOST', rtc: {...}, chat: {...} }

You get back one object with two tokens inside:

  • rtc: a video token that can broadcast (publish camera and mic), and
  • chat: a chat token with ADMIN scope, so Alice can moderate.

It also has streamId, role and chatRootMessageId (used for reactions).

We left out role in the SDK calls above. That's fine here: Alice is already registered as HOST, and leaving role out keeps a person's existing role. For someone new, leaving it out makes them a CO_HOST.

Send this whole response to Alice's app, unchanged. Your app passes it straight to LiveStream.join(). Don't pick fields out or rebuild it.

3. Join as host

In Alice's app, join with the credentials from step 2 and turn on the camera and mic. As soon as a device is on, it's being broadcast into the room. There's no separate "publish" step:

import { LiveStream } from '@livqeno/live';
 
const stream = await LiveStream.join(credentials); // exactly what addHost() returned: { streamId, role, rtc, chat, chatRootMessageId }
 
await stream.room.enableCamera();
await stream.room.enableMicrophone();

stream.room is a normal @livqeno/rtc Room. stream.chat is a normal @livqeno/chat client. Everything you know about them works here.

Then, on your backend, start the stream. Call this when Alice clicks Go live in your app:

await livqeno.liveStreams.start(stream.id);

Now the stream is LIVE:

  • Your backend gets the live_stream.started webhook. You could use it to notify followers.
  • Starting is where the "one live stream at a time" free-tier limit is checked. A second start() while another stream is live fails with RAVEN_STREAM_CONCURRENCY_LIMIT_EXCEEDED.
  • The 240-minute stream limit counts from now.

4. Mint a viewer token and join

On your backend, make a viewer credential for each person who opens the stream page. Here, Carol opens it:

const viewerCredential = await livqeno.liveStreams.createViewerToken(stream.id, 'carol');

A viewer credential can only watch. No field on this call can let a viewer broadcast. This is on purpose: it means a viewer can never take over the stream, whatever their app sends.

You get back the same shape as the host credential, with role: 'VIEWER'. Send it to Carol's app unchanged.

Then, in Carol's app, join and show the host's video. A viewer doesn't turn on any devices. They only receive:

const stream = await LiveStream.join(credentials); // role: 'VIEWER'
 
stream.room.on('trackSubscribed', (track, participant) => {
  if (track.kind === 'camera') track.attach(videoEl);
});

trackSubscribed fires when the host's video is ready to play. If Carol joined before Alice turned her camera on, it fires the moment Alice does.

5. Chat and react

In either app, host or viewer, use stream.chat to send and read messages and stream.react() to send an emoji. stream.chat is a normal Livqeno chat client, already connected to this stream's conversation:

await stream.chat.sendMessage({ text: 'hey!' });
stream.chat.on('message', (m) => console.log(m.senderId, m.text));
 
await stream.react('❤️');
stream.chat.on('reactionAdded', (e) => console.log(e.userId, e.emoji));

6. End the stream

On your backend, end the stream when Alice clicks End stream, or when your event is over:

await livqeno.liveStreams.end(stream.id);

This:

  • closes the room and disconnects everyone still connected, host and viewers, and
  • sends the live_stream.ended webhook.

You can't call start or end again on the same stream, and you can't make new credentials for it. Status only moves forward. For the next event, create a new stream.

If you forget to end a stream, Livqeno ends it for you after 240 minutes of being LIVE.

Done! You ran a full live stream.

What Livqeno handles vs. what you handle

Livqeno handlesYou handle
The video room and the media serversDeciding who is allowed to host (your own permission check before addHost)
The chat conversationDeciding who may watch (your own check before createViewerToken)
Making sure viewers can never broadcast, on any SDKThe UI: video player, chat panel, buttons
Ending streams that run past the time limitWhen to call start() and end()

Common errors

ErrorCauseFix
RAVEN_STREAM_NOT_FOUNDWrong streamId, or the stream is in another project or environment.Use the id create() returned.
RAVEN_STREAM_INVALID_STATEstart() on a stream that isn't CREATED, or end() on one that isn't LIVE.Check stream.status first. Status only moves forward.
Viewer's room.enableCamera() throws PERMISSION_DENIEDThis is expected. Viewer tokens always have publish: false.Make a host or co-host credential instead.
RAVEN_STREAM_INVALID_STATE when making a credentialThe stream has ENDED.Create a new stream.
RAVEN_STREAM_VIEWER_LIMIT_EXCEEDED100 viewers are already connected.Wait for someone to leave. See Free tier.
RAVEN_STREAM_CONCURRENCY_LIMIT_EXCEEDED on start()The account already has a LIVE stream. The limit is per account, across every project and environment, so a stream your app started earlier and never ended (a crash, a closed tab, a test run) counts. The error message names the stream.End it: livqeno.liveStreams.end(id). To find strays, list streams and end the ones still LIVE. See Clean up a stuck stream.
stream.react() throwsThe credentials passed to join() were missing chat or chatRootMessageId.Pass the whole addHost / createViewerToken response, unchanged.

Clean up a stuck stream

A stream stays LIVE until your backend ends it; closing the host's app does not. On the free tier that blocks the next start(), so end strays before starting a new one, and in tests:

const live = (await livqeno.liveStreams.list()).filter((s) => s.status === 'LIVE');
for (const s of live) await livqeno.liveStreams.end(s.id);

A stream also ends by itself when it reaches the 240-minute limit.

Production tips

  • Never call livqeno.liveStreams / livqeno.live_streams from a browser or app. They need your API key.
  • addHost() and createViewerToken() decide the user's role. The role depends only on which one your backend calls, never on anything the app sends.
  • Always include chatRootMessageId in the join credentials. stream.react() needs it.

Full working example

See examples/live-streaming-demo in the Livqeno repo. It's a complete host and viewer demo in two tabs:

  • a FastAPI backend that makes the credentials, and
  • a plain HTML/JS frontend using the calls above (no bundler).

Next steps

You've seen the whole flow. To build it into your app:

  1. Streams & lifecycle: list, update, and end streams from your backend.
  2. Hosts & co-hosts: bring a guest on stage.
  3. Viewers: build the watch page.
  4. Stream events: react to webhooks on your backend.
  5. SDK support: check what your platform can do.