Playback
Playback is deliberately boring: a plain HLS playlist, fetched through the gateway, served from a two-tier cache in front of a bucket. There is no DRM, no per-session signing, and no transcoding at request time.
From a play button to HLS segments
The cache sits between the player and gcsfuse; a miss reads from the output bucket.
The same system, laid out automatically. Use the controls to zoom, open it full screen, or download it for a deck.
The URL contract
| Use | Shape |
|---|---|
| Feature | <origin>/stream/videos/<videoId>/<resolution>/playlist.m3u8 |
| Trailer | <origin>/stream/trailers/<videoId>/<resolution>/playlist.m3u8 |
| Thumbnail | GCS URL stored in videos.thumbnail.thumbnailUrl |
| Trailer thumbnail | videos.thumbnail.trailerThumbnailUrl |
In practice STREAMING_BASE_URL is <origin>/stream/videos on the TV and in the webview. The
gateway routes /stream/ to gstv-multimedia:8000 and strips the prefix.
A player does not choose a rung itself. It reads the master playlist, which lists each rung with
its BANDWIDTH and RESOLUTION, sorted descending, and then adapts.
What the multimedia server does
- Static HLS through node-media-server's HTTP listener on
:8080. - Express on
:8000with/health,/api/cache/*,/videos/*,/trailers/*, and a catch-all proxy to NMS. - Two-tier cache: 512 MB in RAM, 8 GB on disk, promote after repeated hits, LRU eviction.
- gcsfuse mount as the origin. The bucket is a directory.
So a cache miss is not a slow API; it is a filesystem read that blocks on GCS.
The anonymous entry point
/trailler/:id plays the trailer rendition without an account. This is the only media a
signed-out visitor can watch. Signed-in visitors get the full playback chrome; anonymous
visitors keep the catalogue header so they can go back to browsing.
The feature itself is gated by subscription in the client, and by the customer API on the routes that serve playback data. See Security notes for why the gate is weaker than it looks on the native clients.
Symptoms map to layers
| Symptom | Layer to inspect |
|---|---|
404 on the master playlist | Gateway route, container, or a title whose master was never written |
| Master lists rungs that 404 | Worker wrote the master before the renditions; re-run the job |
| Starts, then stalls | gcsfuse read latency, or the disk tier full |
| Correct manifest, no picture | Codec/container mismatch in the ladder, not routing |
| Works locally, fails through the gateway | Prefix stripping, or a route missing from domains.json |
| Trailer loops forever | The client is retrying a 404 as if it were a transient error |
Deliberate limits
- HLS only. No DASH, no per-title manifest variants.
- No DRM. Anyone holding a URL can fetch the segments.
- RTMP is open. Publish and play, plus the cache statistics API, are unauthenticated, and
CORS is
*. Treat the streaming node as internal, even though/stream/is public. - Old base image. The media and worker images stay on
ubuntu:20.04for ffmpeg/gcsfuse parity. It is past standard support and is a known open item.
Cache operations
The cache API is not part of the public contract, but it is how you tell a cache problem from a bucket problem:
- inspect hit/miss counters and the resident set;
- evict a title before re-testing a re-transcode;
- compare a cached segment's size with the object in the bucket.
If a re-transcoded title plays the old rendition, the cache is the first suspect, not the worker.
Next: Identity and subscription — who is allowed to press play.