Skip to content

Moments ​

A moment is something that happens on your live stream that viewers should see on screen as it happens: a paid shoutout, a song request, a dedication, a poll, a product drop or a sponsor slot. You post it from your server. Each viewer's player shows it when the video reaches that second, so a fan who paid for a shoutout sees it when the DJ reads it, not ten seconds before or after.

How it stays in step ​

  1. Your server creates the moment. Vabu stamps it with its own clock to the millisecond (occurred_at).
  2. Each viewer's player polls the moments feed every 2 seconds. The feed can be cached for one second, so a CDN in front of the API answers for thousands of viewers.
  3. The player holds each moment until the playhead's program date time, the EXT-X-PROGRAM-DATE-TIME of the HLS stream, reaches occurred_at. It shows the moment for display_ms and drops it.

Because the player compares the video's own clock with one server stamp, viewers on slow connections see the moment later in wall time and at the same point in the video.

You need a live stream, a secret key on your server, and a publishable key in the page.

Post a moment from your server ​

Call this when your payment provider confirms a shoutout. The Idempotency-Key makes the post safe to retry: a second call with the same key returns the first moment, so one payment shows once.

ts
// Node 20 or later.
const API = 'https://api.vabu.live/v1'

export async function showShoutout(payment: { id: string; fan: string; name: string; message: string; cents: number }) {
  const res = await fetch(`${API}/streams/live_streams/${process.env.VABU_LIVE_STREAM_ID}/moments`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.VABU_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `shoutout-${payment.id}`,
    },
    body: JSON.stringify({
      type: 'shoutout',
      label: `Shoutout from ${payment.name}`,
      payload: { from: payment.name, message: payment.message },
      customer: payment.fan,
      amount: payment.cents,
      currency: 'KES',
      metadata: { payment: payment.id },
    }),
  })

  const body = await res.json()
  if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.error.request_id})`)

  // Keep the id. POST /streams/moments/{id}/cancel takes the moment off screen if the payment is reversed.
  return body.data.id as string
}

What goes where:

FieldSeen by viewersUse it for
type, label, payloadYesWhat the player draws. label is up to 140 characters and payload is a JSON object up to 4 KB.
display_msYesHow long it stays on screen. Default 8000.
customer, amount, currency, metadataNoYour own records. They are in your webhooks and never in the feed.
visibility: "private"NoA moment only you see, for example a note for a clip.

Show moments in the browser ​

Use the publishable key and the stream ID. The example uses hls.js, whose hls.playingDate is the program date time of the frame on screen. Any player that gives you that time works the same way.

ts
// Browser. `hls` is your hls.js instance. `draw` puts a moment on your overlay for `ms` milliseconds.
const API = 'https://api.vabu.live/v1'
const STREAM = 'ls_...'
const KEY = 'vpk_live_...'

type Moment = { id: string; type: string; label: string; payload: Record<string, unknown> | null; occurred_at: string; display_ms: number }

const waiting: Moment[] = []
const seen = new Set<string>() // ids already queued, so starting the feed again never shows a moment twice
let after: string | undefined

async function poll() {
  const url = new URL(`${API}/streams/live_streams/${STREAM}/moments/feed`)
  url.searchParams.set('key', KEY)
  // Every viewer in the same 2 second window asks for the same URL, which lets a CDN answer for all of them.
  url.searchParams.set('w', String(Math.floor(Date.now() / 2000)))
  if (after) url.searchParams.set('after', after)

  const res = await fetch(url)
  if (res.status === 422) after = undefined // the marker is not valid any more: ask for the newest moments again
  if (!res.ok) return

  for (const moment of (await res.json()).data as Moment[]) {
    after = moment.id
    if (seen.has(moment.id)) continue
    seen.add(moment.id)
    waiting.push(moment)
  }
}

function tick() {
  const playhead = hls.playingDate?.getTime()
  if (!playhead) return

  for (const moment of waiting.splice(0)) {
    const start = Date.parse(moment.occurred_at)
    if (start > playhead) waiting.push(moment) // not on screen yet
    else if (playhead - start < moment.display_ms) draw(moment, start + moment.display_ms - playhead)
  }
}

setInterval(poll, 2000)
setInterval(tick, 200)
poll()

The first poll returns the newest 100 moments, so tick skips any whose time has already passed. After that, after asks only for what was saved later, in the order it was saved. If Vabu answers 422 because after is not a moment of the stream, the example asks again without it. The seen set keeps that from showing a moment twice.

Take a moment off screen ​

sh
curl -X POST "https://api.vabu.live/v1/streams/moments/mom_01J9.../cancel" \
  -H "Authorization: Bearer $VABU_API_KEY"

A cancelled moment leaves the feed within a second. A player that already drew it cannot take it back. Cancelling twice is safe.

Show how many are watching ​

GET /v1/streams/live_streams/{id}/stats/public takes the same publishable key and returns { "data": { "viewers_now": 1284 } }. It can be cached for 5 seconds, so poll it about that often. Anyone who holds the page's publishable key and the stream ID can read it, and there is no setting to switch it off yet, so call it only if the count may be public. It counts viewers with an open playback session, so use playback tokens to start playback if you want them counted. Your own dashboard can read the full numbers, including the peak, from GET /v1/streams/live_streams/{id}/stats with a secret key.

Things to know ​

  • The feed is public. Anyone who knows the stream ID can read it, like the video itself when its playback policy is public, and a CDN may answer without checking the key. Put only text that is safe to show anyone in label and payload, and draw it as text, never as HTML.
  • Moments can be queued. You can post to a stream that is not live yet. Players show the moment once the video reaches it. A disabled stream refuses them with 409 live_stream_disabled. If two posts for one stream collide for 3 seconds, the second gets 503 try_again: retry with the same Idempotency-Key.
  • Your clock is optional. To stamp a moment with your own clock, send occurred_at as an ISO 8601 time with an offset. It must be within 5 minutes of the server time, or you get 422 occurred_at_out_of_range, whose details.server_time shows the server clock. Moments reach players in the order you post them, whatever time you stamp, so a moment stamped ahead of the clock never hides later ones. Leave occurred_at out unless your clock is the better one.
  • Test mode. A test key makes test moments that only test keys can read, so you can try the whole flow on a test stream first.
  • Events. streams.moment.created and streams.moment.cancelled reach your webhooks, with the customer and amount. See the event catalogue.

The full request and response for each endpoint is in the API reference.

Vabu OS documentation