A VOD asset is a stored recording or upload in the ClutchCall Stream library. Every asset row carries a status, an optional playback id, probe metadata, a poster position, and a signed-URL flag. The Assets screen reads these rows through streams.assets.list({ orgId, page, perPage }), which joins each stream_asset row with its 24-hour play count from the clutchcall.stream_asset_play_1m_q rollup in ClickHouse. The screen re-queries the list every 30 s, so state changes appear without a reload. This page explains what happens to an asset between upload and playback, and which URL to hand to which consumer.

Asset states

The playback endpoints modal is only meaningful for a ready asset. Open an asset that has no playback_id and the modal says so explicitly — “No playback id yet — the asset is still <status>” — instead of rendering URLs that would resolve to nothing.

How an asset is created

The library holds both recordings and direct uploads. Uploads go through a resumable TUS2 client against the control-plane endpoint /tus/uploads. Because the upload is tracked server-side rather than in the browser tab, an upload started elsewhere still shows up here: streams.tusUploads.inflight({ orgId }) returns the in-flight rows and the screen polls it every 5 s while uploads are moving. Each in-flight row exposes: When the transfer completes, the upload becomes a stream_asset row and moves to processing. Org-level VOD defaults — the default poster position and the auto-transcode flag — are applied at that point, so change them before you upload if you want them to take effect on the new asset. streams.assets.remove({ orgId, id }) deletes an asset from the library.

Passthrough versus auto-transcode (ABR)

Auto-transcode is an org-wide opt-in, stored in stream_org_settings and read and written through streams.settings.get / streams.settings.update:
Passthrough is the fast path: there is no transcode to wait for. ABR costs processing time on ingest and buys adaptive delivery for viewers on variable networks. The console decides which protocols to advertise from the asset’s probe dimensions: an asset with a width or height has been probed and carries a ladder, so HLS and DASH links render. A passthrough upload has neither, so those links are suppressed and the modal tells you why rather than offering URLs that would 404.

Poster frames: org default and per-asset override

A poster position is stored as a fraction of the asset’s duration, not as an absolute timestamp, so it stays valid regardless of how long the asset is.
  • Org default — defaultThumbnailTimestampPct in the VOD defaults bar. It applies to every new upload. Saved with streams.settings.update({ orgId, defaultThumbnailTimestampPct }).
  • Per-asset override — the slider on each asset card, in steps of 0.01 over the range 0–1. Saved with streams.assets.setThumbnail({ orgId, id, thumbnailTimestampPct }), which persists thumbnail_timestamp_pct on the row.
setThumbnail returns a requeued flag: An asset renders a poster on its player only once thumbnail_key is set on the row. Until then the player starts with a black frame.

The playback endpoints

Every URL for a ready asset hangs off its playback_id. With base = https://streams.clutchcall.dev/api/streams/playback/<playback_id>: HLS and DASH ride the same CMAF segments that auto-transcode produces, which is why they appear and disappear together. There is also a hosted player at https://streams.clutchcall.dev/api/streams/embed/<playback_id>. This is the “copy a link and it plays anywhere” URL: it negotiates the transport ladder (MoQ → HLS → MP4) for the viewer’s device and network, so it works for passthrough assets that do not have every protocol. Prefer it over hand-picking a protocol unless you are integrating a specific player. The console also offers the matching embed snippet:
Two identifiers are worth keeping straight. The asset id (external_asset_id, falling back to the internal id) is what you pass to the streams.assets.* procedures. The playback id is what appears in public URLs.

Signed URLs and token lifetime

Each asset carries a require_signed_urls flag, toggled with:
When the flag is on, the tokened surfaces are the HLS manifest, the DASH manifest, and the progressive MP4. A token is minted per viewer session:
The token is appended to the gated URLs as ?tok=<token>. The console mints one with a one-hour TTL when it opens the endpoints modal for a gated asset, and uses it for both the inline player and the copy-link fields. If the mint fails, the modal surfaces the error and offers a retry rather than waiting forever. For a one-off share of the source file, streams.assets.playbackUrl({ orgId, id }) returns a complete signed MP4 URL. The console copies it to the clipboard and notes that it expires in 10 minutes. Never embed a long-lived control-plane API key in a page in order to fetch these tokens from the browser — mint the token on your server and pass it down. See Authentication.

Play telemetry: startup, rebuffer and position samples

The in-console player is a real player and reports real watch data. It POSTs batches of samples to <base>/events as { "rows": [ … ] }, which land in the stream_asset_play_sample table in ClickHouse and feed the analytics screens. Each sample row: Emission points:
  • Heartbeat — one sample every 10 s, but only while the video is genuinely playing (not paused, not ended, playhead past zero).
  • Ended — a final sample when playback reaches the end.
  • Teardown — a last sample when the player unmounts, if anything happened at all (a startup time, accumulated rebuffer, or a non-zero playhead).
The ended and teardown sends use navigator.sendBeacon where available, falling back to a keepalive fetch, so the final numbers survive a tab close. Every send is best-effort: a failed POST is swallowed and never interrupts playback.

Transport selection in the player

The inline player picks its transport in this order:
  1. Native HLS, where the browser reports it can play application/vnd.apple.mpegurl — Safari takes this path.
  2. hls.js, lazy-imported so it stays out of the initial bundle, loading the CMAF vod/master.m3u8.
  3. Progressive MP4 at <base>/source, used when hls.js is unsupported, when the import fails, or when hls.js raises a fatal error mid-playback — in which case the player tears down hls.js and re-points the element at the MP4.
A passthrough asset therefore plays through path 3, which is the whole point of keeping the progressive source available for every asset.

When playback is unavailable