Skip to main content

Playback API

Endpoint overview​

Playlist and segments​

  • endpoint: GET /stream/{hash}/*
  • auth: none
  • purpose: proxy HLS playlists, init segments, and media segments from the media bucket

Key delivery​

  • endpoint: GET /api/key/{id}
  • auth: Authorization: Bearer {token}
  • purpose: return the 16-byte content key for encrypted HLS playback

GET /stream/{hash}/*​

GET /stream/{hash}/*

Common paths include:

/stream/{hash}/master.m3u8
/stream/{hash}/hls/master.m3u8
/stream/{hash}/video/r1080_h264/playlist.m3u8
/stream/{hash}/video/r1080_h264/seg_1.m4s

curl examples​

MEDIA_HASH='movie-2026-04-01'

curl -s \
"http://localhost:3000/stream/$MEDIA_HASH/master.m3u8"

curl -s \
"http://localhost:3000/stream/$MEDIA_HASH/hls/master.m3u8"

curl -s \
"http://localhost:3000/stream/$MEDIA_HASH/video/r720_h264/playlist.m3u8"

curl -I \
"http://localhost:3000/stream/$MEDIA_HASH/video/r720_h264/init.mp4"

Behavior notes​

  • proxies objects directly from the media bucket
  • supports .m3u8, .m4s, .mp4, .jpg, and .webp
  • rejects path traversal attempts containing ..
  • returns 404 when the object is missing
  • currently sets Cache-Control: public, max-age=31536000, immutable
  • returns Access-Control-Allow-Origin: *
Start playback from the master playlist

For public playback, the entrypoint depends on the media result. Video HLS currently starts from /stream/{hash}/master.m3u8, while the audio-only HLS path currently starts from /stream/{hash}/hls/master.m3u8. Treat the object keys stored in job results as backing storage paths, not as browser-facing URLs.

GET /api/key/{id}​

GET /api/key/{id}

This endpoint does not use X-API-Key. It only accepts:

Authorization: Bearer {token}
Do not move playback secrets into the browser

/api/key/{id} is intentionally separated from X-API-Key. Keep KEY_TOKEN_SECRET in a trusted backend or auth service, and never place Bearer tokens into playlist URLs or segment URLs.

For the exact meaning of KEY_TOKEN_SECRET and related playback configuration such as BASE_URL, see Configuration.

Error response contract​

Successful key delivery still returns the raw 16-byte key as application/octet-stream. Failures now use a JSON envelope:

{"message":"Forbidden"}

Common examples:

  • missing Bearer token: {"message":"Unauthorized"}
  • invalid signature, expired token, or hash mismatch: {"message":"Forbidden"}
  • unknown key id: {"message":"Not Found"}
  • rate-limited request: {"message":"Too Many Requests"}

Token format​

The Bearer token contains two base64url fragments:

base64url({"hash":"...","exp":<unix_timestamp>}).base64url(HMAC-SHA256(payload_b64, KEY_TOKEN_SECRET))

The signature covers the base64url payload string, not the raw JSON bytes.

shell token generation example​

MEDIA_HASH='movie-2026-04-01'
KEY_ID='replace-with-key-id'
KEY_TOKEN_SECRET='replace-with-key-token-secret'

PAYLOAD="$(jq -cn \
--arg hash "$MEDIA_HASH" \
--argjson exp "$(($(date +%s) + 3600))" \
'{hash: $hash, exp: $exp}')"

PAYLOAD_B64="$(printf '%s' "$PAYLOAD" \
| openssl base64 -A \
| tr '+/' '-_' \
| tr -d '=')"

SIG_B64="$(printf '%s' "$PAYLOAD_B64" \
| openssl dgst -sha256 -mac HMAC -macopt "key:$KEY_TOKEN_SECRET" -binary \
| openssl base64 -A \
| tr '+/' '-_' \
| tr -d '=')"

TOKEN="$PAYLOAD_B64.$SIG_B64"

curl examples​

curl -i "http://localhost:3000/api/key/$KEY_ID"

curl -s \
-H "Authorization: Bearer $TOKEN" \
"http://localhost:3000/api/key/$KEY_ID" \
| wc -c

On success, the last command should print 16 because the endpoint returns the raw 16-byte content key.

Response semantics​

StatusMeaning
200token verified and the content key was returned as application/octet-stream
401missing Authorization: Bearer ... header; response body uses the JSON error envelope
403invalid signature, expired token, or hash mismatch; response body uses the JSON error envelope
404no stream-key record exists for the key id; response body uses the JSON error envelope
429Redis-backed rate limit exceeded; response body uses the JSON error envelope and includes Retry-After
500unwrap failure or other internal error; response body uses the JSON error envelope

This endpoint also uses a Redis-backed rate limit. The current default is 120 requests per minute, keyed by Bearer token or remote IP.

Where token issuance should live​

In a typical deployment, your application or an internal auth service should mint the Bearer token after it has already decided that the current user may watch the requested media.

The usual flow is:

  1. the client asks your application to play media hash=X
  2. your application authenticates the caller and checks authorization
  3. your application returns the public playlist URL /stream/X/master.m3u8
  4. your application also returns a short-lived Bearer token whose payload contains hash=X
  5. your application also returns the stream-key id or key endpoint from the completed job result
  6. the player attaches that token only on /api/key/{id} requests

Do not move KEY_TOKEN_SECRET into browser code, and do not place the token into playlist or segment URLs.

If you want a cleaner separation of concerns, the playback token may come from a dedicated internal auth service rather than the same backend that serves your application API.