Playback & streaming
Get HLS/DASH playback URLs for video assets, batch-resolve many, and serve manifests, segments, embeds, and thumbnails.
Video (stream) assets are delivered as adaptive-bitrate HLS and DASH. There are two layers:
- Authenticated playback endpoints (
/api/v1/media/...) — resolve the playback URLs for an asset you own. - Public delivery endpoints (
/api/v1/playback/...) — the URLs a player actually hits for manifests, segments, the embed page, and thumbnails. These are not behind the auth guard; access is governed by the asset'svisibility.
Playback only returns usable URLs once stream_status is ready
(lifecycle).
Get playback URLs
GET /api/v1/media/{id}/playback → 200 OK
What it is — Returns freshly signed HLS / DASH / embed / thumbnail URLs for one video asset.
When to use it — Right before playing a single video (a place walkthrough, a user video post, a feedback clip). Bunny/Cloudflare URLs are re-signed live on every call, so fetch them just before playback; on-prem returns stable proxy URLs.
How to use it — GET by id, then feed playback.hls to an HLS player, playback.dash to a DASH
player, or drop playback.embed into an <iframe>. Returns 400 until stream_status is ready.
Scope — Token owner or service key. Stream (video) assets only — file assets return
400. Errors: 400 / 401 / 403 / 404 / 500.
curl https://media.example.com/api/v1/media/770e8400-e29b-41d4-a716-4466554400ff/playback \
-H "Authorization: Bearer $ACCESS_TOKEN"Batch playback URLs
POST /api/v1/media/playbacks → 200 OK
What it is — Resolves freshly signed playback URLs for many video assets at once, with partial success.
When to use it — Rendering a grid or feed of videos in one round-trip — e.g. all video posts on a place, or a mix of clips where you don't want a request each.
How to use it — POST asset_ids (≤ 50) and read each item's playback and error. Partial
success: file assets come back with error: "not_stream"; assets still transcoding with
error: "not_ready".
Scope — User token (own assets only) or service key (owner_user_id required).
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
asset_ids | uuid[] | ✅ | 1–100 ids (deduped). Capped by BATCH_MEDIA_MAX_ASSETS (default 50). |
owner_user_id | string (1–255) | ⚠️ | Required for service-key callers. |
Per-item error is one of not_found, forbidden, not_ready, not_stream, or null.
curl -X POST https://media.example.com/api/v1/media/playbacks \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "asset_ids": ["770e8400-e29b-41d4-a716-4466554400ff"] }'Public delivery endpoints
These are the URLs a video player requests. They live under /api/v1/playback/{assetId} and, for
on-prem streams, are rewritten by the service so segment requests route back through it (and get
signed R2 redirects). They return raw bodies — not the JSON envelope — and are not behind the
auth guard; a private asset is refused with the standard error envelope (401 / 403).
| Endpoint | Returns | Notes |
|---|---|---|
GET /playback/{assetId}/hls.m3u8 | HLS master playlist (application/vnd.apple.mpegurl) | Rewritten; Cache-Control: public, max-age=60. |
GET /playback/{assetId}/hls/* | Child manifest or 302 to a presigned R2 segment | Sub-resources referenced by the master playlist. |
GET /playback/{assetId}/dash.mpd | DASH manifest (application/dash+xml) | Rewritten; max-age=60. |
GET /playback/{assetId}/dash/{filename} | 302 redirect to a presigned R2 segment | e.g. chunk-video-00001.m4s. |
GET /playback/{assetId}/embed | Iframe-friendly HTML player (text/html) | On-prem embed page; max-age=300. |
GET /playback/{assetId}/thumb.jpg | 302 redirect to a presigned R2 thumbnail | Poster image. |
ffplay "https://media.example.com/api/v1/playback/770e8400-e29b-41d4-a716-4466554400ff/hls.m3u8"Manifests are cached for 60 seconds and segments/thumbnails are served via 302 redirects to
short-lived presigned R2 URLs — so players fetch bytes straight from storage, not through the
service.