# Vabu player element

`<vabu-player>` is a video player for Vabu Streams that you add to a page with one HTML tag. Give it a publishable key, the id of a live stream or video, and a function that fetches a customer token from your server. It does the rest: it opens the viewer's session, plays, switches to the next source when one fails, shows moments when the video reaches them and sends quality data.

It works in any framework and in Android and iOS WebViews. If you want your own player and buttons, use the [player kit](https://docs.vabu.live/guides/streams/player-kit.md) instead. This element is built on it.

## Add it to a page

The element is not on npm yet. Build `packages/elements` and host `dist/vabu-player.js` on your own site. Point `src` at your copy.

```html
<script type="module" src="/vabu-player.js"></script>

<vabu-player
  publishable-key="vpk_live_..."
  resource="ls_..."
  api-base="https://api.vabu.live"
  collector-base="https://collect.vabu.live"
  poster="/poster.jpg"
></vabu-player>

<script type="module">
  const player = document.querySelector('vabu-player')

  player.customerToken = () => fetch('/api/vabu-token').then((res) => res.text())
</script>
```

Or install it and import it in your bundle:

```sh
npm install @vabu/elements
```

```ts
import '@vabu/elements'
```

The file carries hls.js. It is about 20 KB gzipped for the player and Lit, plus about 113 KB for the light hls.js build. It needs a browser from 2022 or later.

Your server signs the customer token. The [player kit](https://docs.vabu.live/guides/streams/player-kit.md#the-token-route-on-your-server) page has an example route.

Nothing opens until the viewer presses play, so a page view does not use up a view or a device. Add `autoplay` to start at once. Browsers usually allow autoplay only for muted video, so pair it with `muted`.

## Attributes and properties

| Attribute | Meaning |
|---|---|
| `publishable-key` | Your publishable key. |
| `resource` | The id of a live stream (`ls_...`) or a video (`ast_...`). |
| `api-base` | The Vabu API host. Default `https://api.vabu.live`. |
| `collector-base` | Where quality data goes. Leave it out to send none. |
| `poster` | An image to show before play. |
| `autoplay` | Start as soon as the key, the resource and the token function are set. |
| `muted` | Start muted. The mute button changes it. |
| `data-saver` | Cap renditions at 360p, or the closest lower height. |
| `moments` | `feed` (default for a live stream), `manual` or `none`. See below. |
| `lang` | `en` or `sw`. Without it the player follows the nearest `lang` on the page. |

The property `customerToken` is a function that returns a token, or a promise of one. The player calls it when it starts and again before the token expires. Setting it, the key or the resource while the player runs starts it again with the new values.

Methods: `play()`, `pause()` and `addMoment(moment)`.

## Events

Every event bubbles and crosses the shadow boundary, so you can listen on the tag or above it.

| Event | When | `detail` |
|---|---|---|
| `vabu-ready` | The session is open and playback is attached. | `{ sessionId, provider }` |
| `vabu-moment` | The video reached a moment. | The moment: `{ id, type, label, payload, occurred_at, display_ms }` |
| `vabu-source` | Playback moved to another source. | `{ from, to, reason }` (provider names) |
| `vabu-ended` | The session ended on the server. | `{ reason }`, as in the [kit's list](https://docs.vabu.live/guides/streams/player-kit.md#when-something-goes-wrong) |
| `vabu-error` | Something failed. | `{ code, message, fatal, details, cause }` |

```js
player.addEventListener('vabu-error', (event) => {
  console.warn(event.detail.code, event.detail.details)
})
```

## Moments

With `moments="feed"` the player follows the viewer feed of the live stream and shows each moment when the video reaches it. With `moments="manual"` it does not poll. Hand moments over from your own realtime channel with `addMoment`:

```js
realtime.on('shoutout', (shoutout) => {
  player.addMoment({
    id: shoutout.id,
    occurred_at: shoutout.paidAt, // ISO 8601 or milliseconds, on the same clock as your stream
    label: shoutout.title,
    payload: { message: shoutout.message },
    display_ms: 8000,
  })
})
```

`moments="none"` turns moments off. A video has no feed.

The default is a small toast with the `label` and, if `payload.message` is a string, that text. It draws text only, never HTML. To draw your own, put an element in the `moment` slot. The toast hides while something is slotted, and you listen to `vabu-moment` to know what to show:

```html
<vabu-player ...>
  <div slot="moment" id="shoutout" hidden></div>
</vabu-player>

<script type="module">
  const player = document.querySelector('vabu-player')
  const box = document.getElementById('shoutout')

  player.addEventListener('vabu-moment', ({ detail }) => {
    box.textContent = detail.label
    box.hidden = false
    setTimeout(() => (box.hidden = true), detail.display_ms)
  })
</script>
```

## What the viewer sees

A play button first. Then play and pause, a live badge for a live stream, mute, a quality menu, picture in picture where the browser has it, and full screen. The quality menu has Auto and Data saver, and adds Audio only when the stream has an audio-only rendition. The controls fade out while the video plays and come back on any movement, touch or key.

When playback stops, the player says why. These are the main messages:

| Case | English | Kiswahili |
|---|---|---|
| Replaced by another device | Playing on another device | Inacheza kwenye kifaa kingine |
| Access ended | Your access has ended | Ufikiaji wako umeisha |
| Connection lost, or every source failed | Check your connection | Angalia muunganisho wako |

There are also messages for the device limit, no views left, a video that is not available and a browser that cannot play HLS. A replaced session offers "Watch here" and a connection problem or an unexpected error offers "Try again". The others are for your page to explain, for example with a link to buy.

Everything works with the keyboard. `k` or space plays and pauses, `m` mutes, `f` toggles full screen, arrow keys move in the quality menu and Escape closes it. Animation stops when the viewer prefers reduced motion.

## Make it look like yours

The player is dark, because it sits on video. The coral accent and the other tokens are CSS custom properties. Set them on the tag:

```css
vabu-player {
  --vabu-accent: #1f7a4d;
  --vabu-radius: 4px;
}
```

| Property | Default | Used for |
|---|---|---|
| `--vabu-accent` | `#ff6b57` | The play button, the selected menu item and the moment toast's edge. |
| `--vabu-surface` | `rgba(10, 10, 10, 0.86)` | The control bar, the menu, the toast and error panel. |
| `--vabu-surface-hover` | `rgba(255, 255, 255, 0.12)` | A hovered button. |
| `--vabu-text` | `#f5f5f7` | Text and icons. |
| `--vabu-text-muted` | `#c7c7cc` | Second lines of text. |
| `--vabu-live` | `#ef4444` | The live badge. |
| `--vabu-radius` | `8px` | Corners. |
| `--vabu-font` | Inter, then the system font | All text. |

The tag is a block with a 16:9 shape. Set its width, and its `aspect-ratio` if you want another shape. The toast has `part="moment"`, so `vabu-player::part(moment)` styles it too.

## Try it in test mode

A test key never reaches a real provider, and a test stream has no video. The player opens a session, then tells you that nothing could be played. That is enough to check your key, your token route and your event handlers. The demo page in `packages/elements/demo` has fields for the key, the id and a token, and lists every event. Run it with `pnpm --filter @vabu/elements dev` and open `/demo/`.

## In a framework

The element is a plain custom element. Set `customerToken` as a property, not an attribute.

```tsx
// React
useEffect(() => {
  ref.current!.customerToken = () => fetch('/api/vabu-token').then((res) => res.text())
}, [])

return <vabu-player ref={ref} publishable-key="vpk_live_..." resource="ls_..." />
```

In a WebView, load a page of your own that holds the tag, and allow inline media playback (`allowsInlineMediaPlayback` on iOS, `mediaPlaybackRequiresUserGesture = false` on Android if you use `autoplay`).
