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), soposteris 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).