- Updated README.md to reflect changes in load testing commands and added usage examples for Bash scripts. - Converted PowerShell load testing scripts to Bash equivalents for better cross-platform compatibility. - Added new Bash scripts: run-loadtests.sh, summarize-artillery.sh, and summarize-k6.sh to mirror existing PowerShell functionality. - Updated admin-api.js and ws-chat.yml for consistency and improved performance metrics reporting. - Enhanced error handling and output formatting in summary scripts for better readability.
179 lines
12 KiB
Markdown
179 lines
12 KiB
Markdown
# Streaming Pipeline
|
||
|
||
Operational contract for Linear Void video ingest, playout, publication, caching, and browser playback. Deployment, chat, admin details live in `DEPLOYMENT.md`, `SETUP.md`, `API.md`.
|
||
|
||
## Production flow
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A["Normalized MP4 assets"] --> B["Playlist and schedule"]
|
||
B --> C["Playout service and FFmpeg"]
|
||
C --> D["Local HLS staging"]
|
||
D --> E["R2 object storage"]
|
||
E --> F["Cloudflare edge"]
|
||
F --> G["hls.js or native HLS"]
|
||
```
|
||
|
||
The playout service is the timeline authority. It reads the ordered playlist, runs FFmpeg continuously, stages a live HLS window under `data/hls/`, and publishes complete objects to R2. Viewers read only the public R2 custom domain through Cloudflare:
|
||
|
||
```text
|
||
https://media.linearvoid.tv/hls/playlist.m3u8
|
||
```
|
||
|
||
Nginx `/hls/` is a local/test fallback, not the production origin. The SPA, admin API, and chat use `linearvoid.tv`; media uses `media.linearvoid.tv`.
|
||
|
||
## Source-media contract
|
||
|
||
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 |
|
||
|
||
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.
|
||
|
||
Reference normalization:
|
||
|
||
```bash
|
||
ffmpeg -i input.mov \
|
||
-c:v libx264 -preset medium -crf 21 \
|
||
-r 30 -g 60 -keyint_min 60 -sc_threshold 0 \
|
||
-pix_fmt yuv420p \
|
||
-c:a aac -b:a 160k -ar 48000 -ac 2 \
|
||
-movflags +faststart \
|
||
output-normalized.mp4
|
||
```
|
||
|
||
## Playlist and timeline
|
||
|
||
- `data/playlist/playlist.txt`: one local media path per line; blank lines and `#` comments ignored.
|
||
- Service validates the complete playlist before activating a revision.
|
||
- Playlist edits are atomic and get a revision ID. The active FFmpeg run keeps its current item and adopts the new revision at the next item boundary.
|
||
- All viewers follow the same live manifest; clients never calculate the schedule independently.
|
||
- Schedule metadata derives from the same active revision/timeline so "now/next" agrees with the video.
|
||
- When the last item ends, playout returns to the first unless an active schedule override says otherwise.
|
||
|
||
## Playout and HLS packaging
|
||
|
||
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 |
|
||
|
||
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.
|
||
|
||
**Segment duration rationale**: 2s lowers latency and keeps the window small (12 × 2s = 24s) while staying above the manifest TTL (1–2s). The 2s GOP (30fps, `-g 60`) already matches the target, so segments cut cleanly on keyframes. Longer (4–6s) raises latency and window depth; shorter than 2s inflates segment/request overhead and R2 Class B ops with little gain.
|
||
|
||
Every segment name is globally unique and never reused, including after restart or playlist loop — use a run/session ID plus a monotonically increasing media sequence:
|
||
|
||
```text
|
||
hls/r-20260807T170412Z/init-000001.mp4
|
||
hls/r-20260807T170412Z/seg-000001234.m4s
|
||
```
|
||
|
||
A URL advertised as immutable must never later contain different bytes.
|
||
|
||
## Safe publication to R2
|
||
|
||
Order prevents a manifest referencing an unavailable segment:
|
||
|
||
1. FFmpeg writes a segment to a temp local name and closes it.
|
||
2. Publisher uploads the init segment (if new) and media segment to final immutable R2 keys.
|
||
3. Verify upload success; record object size/checksum.
|
||
4. Render the next manifest referencing only confirmed objects.
|
||
5. Upload the manifest to a temp key, then replace `hls/playlist.m3u8` as the final commit step.
|
||
|
||
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.
|
||
|
||
`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` |
|
||
|
||
Further rules:
|
||
|
||
- No query-string cache busters on segment URLs; uniqueness belongs in the path.
|
||
- Cache keys preserve hostname and full path; ignore only explicitly approved tracking params.
|
||
- HLS responses need `Access-Control-Allow-Origin: https://linearvoid.tv` (or reviewed allowlist), `GET`/`HEAD`/`OPTIONS`, and correct MIME types.
|
||
- Enable Smart Tiered Cache for `media.linearvoid.tv` to cut R2 reads on regional misses.
|
||
- Purge manifests when necessary, never immutable segments during normal operation.
|
||
- Retain expired-window segments at least max manifest TTL + retry/grace time; practical minimum 1 hour; lifecycle deletion may run later per storage policy.
|
||
|
||
Validation:
|
||
|
||
```bash
|
||
curl -I https://media.linearvoid.tv/hls/playlist.m3u8
|
||
curl -I https://media.linearvoid.tv/hls/<segment>.m4s
|
||
```
|
||
|
||
The manifest must refresh within seconds; repeated segment requests should progress `CF-Cache-Status: MISS` → `HIT`, keep the same ETag/content length, and carry the immutable cache header.
|
||
|
||
## Player behavior
|
||
|
||
SPA uses native HLS where supported, hls.js elsewhere.
|
||
|
||
- Firefox has no native HLS → hls.js/MSE path; fMP4 (`.m4s` + init) is supported. Autoplay needs `muted` + `playsinline`, already set on the `<video>` element.
|
||
- Start at the live edge with a conservative buffer; do not seek to playlist start.
|
||
- Treat the manifest as the sync source; target ~3 segments behind the newest unless testing supports a smaller margin.
|
||
- Recover transient network/media errors with bounded retries + exponential backoff.
|
||
- On fatal hls.js network error, reload the manifest; on fatal media error, attempt media recovery once before recreating the player.
|
||
- If tab suspension/drift leaves playback materially behind the target, seek forward rather than play the backlog.
|
||
- Surface a clear reconnecting state; never silently loop stale media.
|
||
- Schedule/chat timestamps should account for measured player latency, not wall-clock playout.
|
||
|
||
## Failure and recovery
|
||
|
||
The service retries an item failure with bounded backoff. Past the failure threshold it switches to a pre-normalized looping slate and reports `mode=slate`, periodically retrying normal playout, returning only after a segment and committed manifest publish successfully.
|
||
|
||
On process/host restart:
|
||
|
||
- create a new run/session ID and continue with unique segment paths;
|
||
- resume from schedule-derived current position when reliable state exists, else start the current playlist item;
|
||
- never overwrite old segment objects;
|
||
- keep the last valid manifest available until the replacement is playable;
|
||
- emit a discontinuity when timeline continuity isn't guaranteed.
|
||
|
||
`/health` must report more than "process running": at least mode (`live`/`slate`/`starting`/`degraded`), active item, playlist revision, run ID, last segment sequence/time, last successful R2 publication, consecutive failures, uptime. Mark unhealthy if the manifest or media sequence hasn't advanced for more than two segment durations.
|
||
|
||
## Monitoring and acceptance
|
||
|
||
Monitor/alert on: manifest age and media-sequence advancement; time since last published segment; FFmpeg restarts and consecutive failures; live/slate mode; R2 upload latency/errors; segment 404/5xx and manifest/segment latency; Cloudflare cache-hit ratio, origin requests, R2 Class B ops; player startup, fatal errors, rebuffer ratio, live-edge drift; local staging disk usage.
|
||
|
||
A release is streaming-ready only when: one full playlist boundary plays without timestamp errors; the manifest never references a missing object; a second viewer joins the same timeline; cache headers match the table; a forced FFmpeg failure activates slate; and normal playout recovers without reusing an object URL.
|
||
|
||
## 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 |
|
||
|
||
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. |