Viewers are the audience. They watch the host's video, and they can chat and react, but they can't broadcast.
Where this fits: you've created a stream and your host is joining. Now each person who opens your stream page needs a viewer credential from your backend, which their app uses to join.
There's no viewer list to manage in Livqeno. Anyone your backend gives a valid viewer credential to can join. "Who's watching" is simply whoever is connected right now. Whether a stream is public, members-only, or paid is decided by your backend, when it chooses who gets a credential.
Make a viewer token (backend)
On your backend, make one credential per viewer, each time they open the
stream page. Use the viewer's id from your own login session, like carol:
const credential = await livqeno.liveStreams.createViewerToken(streamId, 'carol');credential = livqeno.live_streams.create_viewer_token(stream_id, "carol")credential, err := client.LiveStreams.CreateViewerToken(ctx, streamID, "carol", nil)IssuedStreamCredential credential = livqeno.liveStreams().createViewerToken(streamId, "carol");$credential = $client->liveStreams->createViewerToken($streamId, 'carol');curl -X POST "$LIVQENO_API_URL/v1/live-streams/$STREAM_ID/viewer-tokens" \
-H "Authorization: Bearer $LIVQENO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"identity": "carol"}'What this does:
- Returns a credential with
role: 'VIEWER': anrtctoken that can only watch, and achattoken withMEMBERscope. - Adds the viewer to the stream's chat, so they can send messages.
- Sends the
live_stream.viewer_joinedwebhook. This fires when the credential is made, not when the viewer actually connects. - Fails with
RAVEN_STREAM_VIEWER_LIMIT_EXCEEDEDif 100 viewers are already connected. Credentials that were made but never used don't count.
Viewer credentials work while the stream is CREATED or LIVE, so viewers
can wait on the page before the host goes live. They fail once the stream
has ENDED.
Send the whole credential to the viewer's app, unchanged.
Join and watch (app)
In the viewer's app, join with the credential. A viewer doesn't turn on any devices. They show the host's video when it arrives:
const stream = await LiveStream.join(credentials); // role: 'VIEWER'
// Already-live tracks arrive synchronously; new ones fire trackSubscribed.
for (const participant of stream.room.remoteParticipants) {
for (const track of participant.tracks) {
if (track.kind === 'camera') track.attach(videoEl);
}
}
stream.room.on('trackSubscribed', (track, participant) => {
if (track.kind === 'camera') track.attach(videoEl);
});import { LivqenoLiveStream, useLiveStreamViewer, useRemoteParticipants, ParticipantView } from '@livqeno/react';
function Viewer({ credentials }) {
return (
<LivqenoLiveStream credentials={credentials}>
<Stage />
</LivqenoLiveStream>
);
}
function Stage() {
const remote = useRemoteParticipants(); // the host(s), already subscribed
return remote.map((p) => <ParticipantView key={p.identity} participant={p} />);
}import { joinLiveStream, useLiveStream, useRemoteParticipants, LivqenoVideoView } from '@livqeno/react-native';
const stream = await joinLiveStream(credentials); // role: 'VIEWER'
const { room } = useLiveStream(stream);
const remote = useRemoteParticipants(room);final stream = await LivqenoLiveStream.join(credentials); // role: VIEWER
stream.room.participantChanges.listen((participants) {
// rebuild your grid
});If the host hasn't turned on the camera yet, the viewer simply sees nothing until they do. Show a "Waiting for the host…" message until the first video arrives.
Leave
Call leave() when the viewer closes the stream page or navigates away. It
disconnects them from the video and the chat. The stream carries on for
everyone else:
await stream.leave();await stream.leave();await stream.leave();curl -X POST "$LIVQENO_API_URL/v1/live-streams/$STREAM_ID/leave" \
-H "Authorization: Bearer $LIVQENO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"identity": "carol"}'This REST call runs on your backend. It only records the leave (it
sends live_stream.viewer_left). The client's stream.leave() closes the
room and chat. Call this endpoint too if you want the viewer_left webhook,
for example to track how long people watched.
What viewers can't do
-
Viewers can't broadcast. A viewer token always has
publish: false. No request field can change it. See Authentication. -
Viewers can send chat messages and reactions (
MEMBERscope). -
To bring a viewer on stage, your backend calls
addHostfor them. They then join again with the new host credential.
Common errors
| Problem | Cause | Fix |
|---|---|---|
PERMISSION_DENIED on room.enableCamera() | Viewer tokens always have publish: false. | This is expected. To let a viewer broadcast, your backend calls addHost(). |
RAVEN_STREAM_VIEWER_LIMIT_EXCEEDED (403) | 100 viewers are already connected. | Show a "Stream is full" message, and let the viewer try again later. |
RAVEN_STREAM_INVALID_STATE | The stream has ENDED. | Show "This stream has ended." |
| A viewer's app crashed, but no leave was recorded | Dropped connections aren't detected yet. | Only the leave endpoint sends live_stream.viewer_left. Don't rely on it for crashes. |