---
sidebar_position: 8
---

# Live streams

One person presents live to everyone watching: a product show in a shop, a class, a launch. The audience watches the picture, writes in the chat of the stream and sends reactions. The host goes live from the browser or a phone, or streams from OBS. It runs on the same connection, key and calls route as rooms and calls, on the media servers of WordPlus Cloud.

## Who is who

Your server declares streams as it declares rooms and calls, by pattern. The rule says who each person is in the stream:

```js
// The show of the shop, show:mugs: Ann presents it, and everyone else watches, signed in or not.
realtime.streams( 'show:{id}', ( user, { id } ) => {
    if ( ! showExists( id ) ) return false;
    return user === presenterOf( id ) ? 'host' : 'viewer';
}, { reactions: true } );
```

- **The answer:** `false`, a role, or the role with more:

  | Field | |
  |---|---|
  | `role` | `host` (goes live, ends it, keeps the chat) or `viewer` |
  | `info` | `{ name, avatar }` as everyone sees them, your `profile` by default. `avatar` is an `https://` address |
  | `chat` | `write` or `read` |
  | `mod` | `true` for someone who keeps the chat without hosting |
  | `src` | what the host may send: `camera`, `microphone`, `screen` |
  | `quality` | the best picture the host sends: `720p` or `1080p` |

- **A visitor who is not signed in** comes with `user` null, and may only be a viewer. A rule that gives them more is refused with `no_user`.
- **The settings of the stream**, the third argument, are each a value or a function of the parts of its name:
  - `reactions`: whether it takes reactions.
  - `people`: who may see who is watching, `mods` or `all`.
  - `chat: { offline, links, fresh }`. `offline` is `'open'` or `'closed'` for the chat while the stream is not live. `links: false` takes links only from those who keep the chat. `fresh: true` empties the chat each time the stream goes live.
- **Names** use letters, digits and `-=@,.;:`, up to 128 characters, as room names do but without `_`.

The calls route (`/api/realtime/calls`) answers streams too: nothing more to add to `realtime.node()` or your routes.

## Watching

```js
const calls = await rt.calls();
const show = await calls.stream( 'show:mugs' );

show.watch( video );                   // the main picture of the stream, as it changes, and its sound
show.on( 'state', ( state ) => {} );   // idle, live, ended
show.on( 'viewers', ( n ) => {} );     // how many watch, the host not counted
```

- **The main picture** is the one the host chose: a shared screen, the picture from OBS, else the camera of the host. `show.tiles` has each picture with its own `camera`, `screen` and `audio`, for a layout of your own.
- **Sound** plays from the page, and `video` itself stays `muted`. A browser that holds sound back until a click says so in `show.snapshot().audioBlocked`: call `show.startAudio()` from a click.
- **`video: false`** in `calls.stream( name, { video: false } )` joins without the picture, for a page that shows the chat or the count. `watch()` fetches it later.
- **`show.leave()`** leaves the stream, its picture and its chat.

## Going live

The page of the host shows the camera before going live, then goes live with whatever it has on:

```js
await show.preview();                  // the camera and microphone, shown in watch( video )
await show.setCamera( false );         // or setMicrophone(), setScreenShare( true )
await show.goLive();                   // everyone watching sees it now
await show.end();
```

`goLive()` with nothing on turns the camera and microphone on first. A host may take an ended stream live again.

**From OBS** or another encoder, over WHIP:

```js
const { url, key } = await show.ingest();   // ingest( true ) makes a new key and cuts off the old one
```

The picture from the encoder takes the place of the camera.

## The chat and reactions

```js
show.on( 'message', ( m ) => add( m.from.name, m.text ) );   // each new message, show.messages has the history
await show.chat.send( 'Hello!' );
show.react( 'heart' );                                       // heart, clap, laugh, wow, fire
```

Whoever keeps the chat, the host and anyone the rule made `mod`:

```js
await show.chat.delete( id );
await show.chat.pin( id );
await show.chat.timeout( user, 600 );   // from a minute to a day, they go on watching
await show.chat.slow( 30 );             // one message per 30 seconds, 0 turns it off
await show.chat.clear();
await show.kick( user );                // out now, they may come back
await show.ban( user );                 // out, and kept out
```

A refused message rejects with its code: `read_only`, `slow`, `timeout`, `too_long`, `links`, `not_live`.

## A screen of your own

`show.snapshot()` is everything the screen of the stream draws: its phase, the audience, the chat, the pinned message, the pictures and their layout. `show.subscribe( fn )` hears every change, as `useSyncExternalStore` in React wants. In React, `useStream( name )` does both ([React](react.md)).

## What WordPlus Cloud sees

A stream is for everyone watching it, so its picture and chat are not encrypted end to end. They are encrypted on the way, and the media servers of WordPlus Cloud see them, as a stream needs.

## Refusals

`calls.stream()` and the actions of the host reject with a code:

| Code | |
|---|---|
| `forbidden` | Your rule said no, or no pattern matches the name |
| `no_user` | Your rule gave a visitor who is not signed in more than the place of a viewer |
| `not_included` | The plan of your app does not include live streams |
| `banned` | This user is banned from the stream |
| `no_hours` | The account has no credits left this month, what it may borrow included: new viewers above the plan come in once credits are bought |
| `not_host` | An action of the host from someone who is not the host |
