chore(release): 1.1.0
Release / release (push) Successful in 1m15s
CI / test (push) Successful in 2m53s
CI / docker (push) Successful in 2m0s

- playout: run-scoped R2 manifest URIs (candidate a rewrite via rewriteManifestToRun)
- tests: playlist_rewrite_test.go (rewrite + publish integration + slate guard)
- docs: STREAMING, cloudflare-workers-setup, architecture (incl. Level-2 diagram)
- release housekeeping: TODO->DONE, CHANGELOG 1.1.0, version bump
This commit is contained in:
zwitschi committed 2026-09-03 09:27:38 +02:00
1 parent 8c5e70959b
commit d8cf853842
13 files changed
+526 -164

No files matched your search

+44 -34
View File
@@ -26,14 +26,14 @@ Nginx `/hls/` is a local/test fallback, not the production origin. The SPA, admi
Only validated files may enter the production playlist.
| Property | Required profile |
| ---------- | ------------------------------------------------------------------------ |
| Container | MP4 |
| Video | H.264/AVC, `yuv420p` |
| Resolution | 1280x720 or 1920x1080; avoid resolution changes within a playlist block |
| Frame rate | Constant 30 fps |
| GOP | Closed, fixed 2 seconds: `-g 60 -keyint_min 60 -sc_threshold 0` |
| Audio | AAC-LC, stereo, 48 kHz, 128–192 kbit/s |
| Property | Required profile |
| ---------- | ----------------------------------------------------------------------- |
| Container | MP4 |
| Video | H.264/AVC, `yuv420p` |
| Resolution | 1280x720 or 1920x1080; avoid resolution changes within a playlist block |
| Frame rate | Constant 30 fps |
| GOP | Closed, fixed 2 seconds: `-g 60 -keyint_min 60 -sc_threshold 0` |
| Audio | AAC-LC, stereo, 48 kHz, 128–192 kbit/s |
The ingest gate uses `ffprobe` to reject incompatible codecs, variable frame rate, missing audio, invalid dimensions, or wrong keyframe cadence. Assets are immutable after acceptance; corrections create a new asset ID/object key.
@@ -62,14 +62,14 @@ ffmpeg -i input.mov \
The playout service wraps FFmpeg, tracks the active item/revision, restarts failed processes, and exposes `/health` plus Prometheus metrics.
| Setting | Value |
| -------------- | ---------------------------------------------------------------------------------- |
| Protocol | Standard live HLS, not LL-HLS |
| Segment format | fMP4 (`.m4s`) with init segment; `.ts` may be supported during migration |
| Segment target | 2 seconds (`HLS_SEGMENT_DURATION=2`) |
| Live window | 12 segments (`HLS_WINDOW_SIZE=12`), ~24 seconds |
| Codec path | Stream copy for conforming assets; no per-viewer transcoding |
| Expected latency | 15–35 seconds |
| Setting | Value |
| ---------------- | ------------------------------------------------------------------------ |
| Protocol | Standard live HLS, not LL-HLS |
| Segment format | fMP4 (`.m4s`) with init segment; `.ts` may be supported during migration |
| Segment target | 2 seconds (`HLS_SEGMENT_DURATION=2`) |
| Live window | 12 segments (`HLS_WINDOW_SIZE=12`), ~24 seconds |
| Codec path | Stream copy for conforming assets; no per-viewer transcoding |
| Expected latency | 15–35 seconds |
Segment boundaries must align with keyframes; timestamps must stay monotonic across item boundaries. If FFmpeg cannot preserve a seamless timeline, insert `#EXT-X-DISCONTINUITY` and publish the corresponding new init segment.
@@ -96,15 +96,25 @@ Order prevents a manifest referencing an unavailable segment:
Never upload an actively written segment or publish the manifest first. Use a single publisher (or fencing/leader election) so two workers can't update the live manifest concurrently.
**Run-scoped manifest URIs.** Segments and the init file are stored run-scoped under `hls/r-{runID}/…`, so the live `hls/playlist.m3u8` must reference them with that run prefix. Before the final commit step, the publisher rewrites each bare relative media URI (segments plus `#EXT-X-MAP:URI`) to `r-{runID}/<name>`. `publishManifest` in `backend/playout/src/playlist.go` does this via `rewriteManifestToRun`, using the same `runID` under which the segments were uploaded; the rewrite is skipped in slate mode (its manifest is served as-is). The published manifest therefore contains, e.g.:
```m3u8
#EXT-X-MAP:URI="r-20260807T170412Z/init.mp4"
#EXTINF:2.000000,
r-20260807T170412Z/seg-000001234.m4s
```
The `linearvoid-media` Worker on `media.linearvoid.tv` is a faithful path→R2 mapper and serves these run-scoped paths directly; it does **not** rewrite URIs at serve time.
`data/hls/` is regenerable staging. Source media, playlist revisions, and schedule state are authoritative; generated HLS is not backed up as application state.
## Cache contract
| Objects | Edge policy | Browser policy | Required headers/behavior |
| -------------------------- | --------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------- |
| `.m4s`, `.ts`, init `.mp4` | Cache eligible; TTL 1 year | TTL 1 year | `Cache-Control: public, max-age=31536000, immutable`; full path in cache key |
| Live `.m3u8` | Cache eligible; edge TTL 2s | Do not retain beyond freshness interval | `Cache-Control: public, max-age=1, s-maxage=2, must-revalidate` |
| Versioned JS/CSS | Cache eligible; TTL 1 year | TTL 1 year | Content-hashed filenames and `immutable` |
| Objects | Edge policy | Browser policy | Required headers/behavior |
| -------------------------- | --------------------------- | --------------------------------------- | ---------------------------------------------------------------------------- |
| `.m4s`, `.ts`, init `.mp4` | Cache eligible; TTL 1 year | TTL 1 year | `Cache-Control: public, max-age=31536000, immutable`; full path in cache key |
| Live `.m3u8` | Cache eligible; edge TTL 2s | Do not retain beyond freshness interval | `Cache-Control: public, max-age=1, s-maxage=2, must-revalidate` |
| Versioned JS/CSS | Cache eligible; TTL 1 year | TTL 1 year | Content-hashed filenames and `immutable` |
Further rules:
@@ -159,21 +169,21 @@ A release is streaming-ready only when: one full playlist boundary plays without
## Required configuration
| Variable | Purpose |
| ------------------------------------------ | -------------------------------------------------- |
| `PLAYLIST_PATH` | Active playlist file |
| `HLS_SEGMENT_DURATION` | Target segment duration; baseline `2` |
| `HLS_WINDOW_SIZE` | Manifest window; baseline `12` |
| `R2_ENDPOINT`, `R2_REGION` | S3-compatible publication endpoint |
| `R2_BUCKET_NAME` | Media bucket |
| `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY` | Write credentials, publisher only |
| `R2_KEEP_RUNS` | Run folders to retain (default `2`) |
| `R2_MAX_RETENTION_BYTES` | Max bytes across run folders (10 GiB) |
| `MEDIA_DOMAIN` | Public R2 custom domain |
| `VITE_PUBLIC_HLS_URL` | Public live manifest URL |
| Variable | Purpose |
| ------------------------------------------ | ------------------------------------- |
| `PLAYLIST_PATH` | Active playlist file |
| `HLS_SEGMENT_DURATION` | Target segment duration; baseline `2` |
| `HLS_WINDOW_SIZE` | Manifest window; baseline `12` |
| `R2_ENDPOINT`, `R2_REGION` | S3-compatible publication endpoint |
| `R2_BUCKET_NAME` | Media bucket |
| `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY` | Write credentials, publisher only |
| `R2_KEEP_RUNS` | Run folders to retain (default `2`) |
| `R2_MAX_RETENTION_BYTES` | Max bytes across run folders (10 GiB) |
| `MEDIA_DOMAIN` | Public R2 custom domain |
| `VITE_PUBLIC_HLS_URL` | Public live manifest URL |
Production values: `MEDIA_DOMAIN=media.linearvoid.tv`, `VITE_PUBLIC_HLS_URL=https://media.linearvoid.tv/hls/playlist.m3u8`. Local fallback may use `http://localhost/hls/playlist.m3u8`.
## Explicitly deferred
ABR ladders, LL-HLS, DVR/catch-up, multi-origin playout, and regional chat affinity are outside the v1 contract. Adding any requires an ADR and an update to this document.
ABR ladders, LL-HLS, DVR/catch-up, multi-origin playout, and regional chat affinity are outside the v1 contract. Adding any requires an ADR and an update to this document.