Skip to main content

Web player with the JavaScript SDK

Embed a video player in the browser using @videas/sdk on your server to mint a token, then hls.js to play the signed HLS stream.

This guide wires a working <video> player using @videas/sdk on your server and hls.js in the browser. Read How playback works first — the key rule is that minting the token needs your secret key and must run server-side, while the browser only ever handles the token.

Step 1 — mint a player token (server)

Your server holds the sk_ key and exposes a small endpoint that returns only browser-safe data. Here with Express:

// server.ts — runs on YOUR server, never shipped to the browser
import express from 'express'
import { createVideasClient } from '@videas/sdk'

const app = express()
const videas = createVideasClient({ apiKey: process.env.VIDEAS_API_KEY! })

app.get('/api/player/:uid', async (req, res) => {
  const player = await videas.videos.getPlayer(req.params.uid)
  // Hand ONLY browser-safe fields to the page — never the sk_ key.
  res.json({
    assetId: req.params.uid,
    playbackToken: player.playback_token, // safe to expose; valid 4h
    poster: player.items[0]?.asset.thumbnail_url ?? null,
    config: player.player_config, // auto_play, loop, theme, …
  })
})

The API returns absolute URL fields (like thumbnail_url), so poster is ready to use as-is.

player_config is resolved every time a token is minted, not frozen into your code: the theme, accent colour and logo your client changes inside Videas apply without you redeploying. That is what lets an integration shipped once carry on without you — see Developers and integrators.

Step 2 — resolve the stream and play (browser)

In the browser you don’t instantiate the SDK (that would need the sk_ key). Call POST /playback/info/ directly with the token, then feed the HLS manifest into a player.

<video id="player" controls playsinline style="width: 100%"></video>

<script type="module">
  const API = 'https://api.videas.com/api/external/v1'

  // 1. Get the browser-safe token from your own server (step 1).
  const { assetId, playbackToken, poster } =
    await (await fetch('/api/player/vid_123')).json()

  // 2. Exchange the token for signed stream URLs (no API key here).
  const info = await (await fetch(`${API}/playback/info/`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ asset_id: assetId, playback_token: playbackToken }),
  })).json()

  const video = document.getElementById('player')

  // 3. Respect the wallet gate.
  if (info.blocked) {
    video.outerHTML = '<p>This video is temporarily unavailable.</p>'
    throw new Error('playback blocked')
  }

  if (poster) video.poster = poster

  // 4. Play the HLS manifest — native in Safari/iOS, hls.js elsewhere.
  const src = info.hls_url?.url
  if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.src = src
  } else {
    const { default: Hls } = await import('https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.mjs')
    if (Hls.isSupported()) {
      const hls = new Hls()
      hls.loadSource(src)
      hls.attachMedia(video)
    } else if (info.source_url) {
      video.src = info.source_url.url // last-resort progressive MP4
    }
  }

  // 5. Resume where the viewer left off, if we know.
  if (info.resume_position_seconds) {
    video.currentTime = info.resume_position_seconds
  }
</script>

That’s a fully working player. Everything below is optional polish.

Refreshing expired URLs

The signed URLs expire (see info.hls_url.expires_at) well before the 4-hour token. To support long sessions, re-run step 2 when they’re near expiry — you can keep reusing the same playbackToken:

async function resolve(assetId, token) {
  const res = await fetch(`${API}/playback/info/`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ asset_id: assetId, playback_token: token }),
  })
  return res.json()
}

Only when the token itself expires (after 4h) do you call your server again to mint a fresh one.

Chapters & seek-bar thumbnails

info.chapters is a ready-to-use list; render markers on your progress bar:

for (const chapter of info.chapters ?? []) {
  console.log(chapter.start_time, chapter.title) // + chapter.thumbnail_url
}

info.preview_sprites_url is a sprite sheet for hover-scrub thumbnails; wire it into your player’s thumbnail plugin if it supports sprite sheets.

Alternative — resolve entirely on the server

If you render pages server-side and play immediately, you can skip the split and use the one-call helper, which does both steps with your key:

// Server-side only — needs the sk_ key.
const info = await videas.videos.resolvePlayback('vid_123')
// Inject info.hls_url.url into your template.

Because the signed URLs are short-lived, resolve just before serving the page, and expect to re-resolve for long-lived sessions (which is exactly why the token-to-browser approach above scales better).

Next steps