mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-04 12:02:33 +02:00
The daemon moves to its own repo (XC_VM_Fanout, GIT_REPO_FANOUT) that ships per-arch static binaries as GitHub Release assets. Since we're fully migrating onto the daemon, the panel needs to fetch and update it. - FanoutBinaryCommand (`console.php fanout_binary`): reads the installed version straight from the binary (`xc_fanout -version`), compares to the latest release, and when they differ downloads the arch-matched asset, verifies its SHA-256 against the release SHA256SUMS, installs it atomically and restarts the daemon (the service keepalive respawns it). `force` reinstalls the current version. Modelled on BinariesCommand. - GIT_REPO_FANOUT constant. - docs/xc_fanout.md: separate-repo + install/update section. The daemon repo itself (source + release.sh + a v* release GitHub Action, binaries gitignored) is prepared under XC_VM_Fanout/ for the user to push.
139 lines
6.2 KiB
Markdown
139 lines
6.2 KiB
Markdown
# `xc_fanout` — the Go live-delivery daemon (why it exists)
|
|
|
|
`xc_fanout` is a small, native Go daemon that XC_VM uses to deliver **live**
|
|
streams to viewers. This page explains *why* it exists and *how the panel talks
|
|
to it*. The decision records are ADR 0001/0002/0003; this is the plain-language
|
|
overview.
|
|
|
|
The daemon's **source** lives in its own repo, **`XC_VM_Fanout`** (`GIT_REPO_FANOUT`),
|
|
and its compiled binaries ship as **GitHub Release assets** (never committed to a
|
|
tree). It is a single static binary per architecture and is never bundled into the
|
|
panel/LB archive.
|
|
|
|
---
|
|
|
|
## The problem it solves
|
|
|
|
Historically PHP was **in the byte path** of every live viewer:
|
|
|
|
- **Proxy streams** — `Public/stream/live.php` ran a `socket_read → echo → flush`
|
|
loop for the whole session, one PHP-FPM worker pinned per viewer.
|
|
- **Non-proxy streams** — `live.php` chase-read the HLS `.ts` segments off tmpfs
|
|
and echoed them as a continuous MPEG-TS, again one worker per viewer.
|
|
|
|
We measured it (ADR 0001, P0): **~1 PHP-FPM worker + ~25 MB of RAM per viewer.**
|
|
That linear RAM law — not `pm.max_children` — is the real ~400-concurrent
|
|
ceiling. A 4 GB box walls at ~55 viewers on RAM alone. Every viewer is a blocked,
|
|
expensive worker instead of a cheap connection.
|
|
|
|
The whole redesign is about **getting PHP out of the byte path** so a viewer
|
|
becomes a cheap nginx→daemon connection. The daemon is what holds those
|
|
connections.
|
|
|
|
---
|
|
|
|
## What the daemon does
|
|
|
|
One process serves many streams. Per stream it holds **one** ingest and fans it
|
|
out to many viewers, plus segments the same feed into HLS **entirely in RAM**
|
|
(no tmpfs, no files):
|
|
|
|
- **Live TS fan-out** — `GET /live/<id>`: PAT/PMT + keyframe-aware ring buffer,
|
|
each new viewer gets a clean-join snapshot then the live tail; slow viewers are
|
|
dropped, never blocking the producer or others.
|
|
- **In-RAM HLS** — `GET /hls/<id>/index.m3u8` and `/hls/<id>/<seq>.ts`: cuts
|
|
segments at video keyframes with PTS-accurate `EXTINF`, sliding window; can
|
|
AES-128-CBC **encrypt** segments (matching the panel's `encrypt_hls`).
|
|
- **Two ways to be fed**:
|
|
- **pull** (proxy streams) — the daemon connects to the source itself
|
|
(direct `video/mp2t`, or spawns ffmpeg to remux other inputs). Replaces
|
|
`Cli/Commands/ProxyCommand.php`.
|
|
- **push / ingest** (non-proxy & LLOD) — the stream's existing producer writes
|
|
MPEG-TS into a per-stream unix socket the daemon listens on. The producer's
|
|
real work (transcode, logo, profiles) stays where it was.
|
|
- **On-demand lifecycle** — starts a puller on the first viewer, stops it after a
|
|
grace once the last leaves; HLS requests keep it alive too.
|
|
|
|
Two unix-socket HTTP surfaces: a **client** surface nginx proxies viewers to, and
|
|
a **control** surface only the panel (PHP) talks to.
|
|
|
|
---
|
|
|
|
## What the daemon does NOT do
|
|
|
|
- **It does not replace ffmpeg.** For non-proxy streams the per-stream ffmpeg
|
|
keeps doing the real work (transcode, logo overlay, profiles, timeshift
|
|
recording). The daemon only takes over *delivery*.
|
|
- **It does not do auth.** PHP still authenticates every request (tokens, line
|
|
limits, IP match, connection tracking). The daemon only moves bytes.
|
|
- **It does not touch the database.** All DB writes stay in PHP — this keeps LB
|
|
nodes privilege-free.
|
|
- **Live only.** VOD, series, timeshift/archive are random-access (seek /
|
|
byte-range) over files, not a shared live tail — they keep their own path.
|
|
|
|
---
|
|
|
|
## How the panel talks to it
|
|
|
|
The switch is the **daemon's reachability**, not a settings flag: `live.php`
|
|
routes a stream to the daemon when its control socket is present and the stream
|
|
is being served; otherwise it runs the legacy path. **Stopping the daemon is the
|
|
rollback** — every stream falls back automatically, per node.
|
|
|
|
Delivery uses the same `X-Accel-Redirect` pattern as P1 HLS: PHP authenticates,
|
|
then hands the byte path to nginx, which proxies to the daemon. The FPM worker is
|
|
freed the instant PHP returns.
|
|
|
|
**Client surface** (nginx → daemon, via internal `X-Accel` locations):
|
|
|
|
| Request | Daemon route |
|
|
|---|---|
|
|
| live TS | `X-Accel-Redirect: /xc_fanout/<id>?c=<uuid>` → `/live/<id>` |
|
|
| HLS segment | `X-Accel-Redirect: /xc_fanout_hls/<id>_<seq>` → `/hls/<id>/<seq>.ts` |
|
|
| HLS playlist | PHP fetches `/hls/<id>/index.m3u8`, tokenizes the segment URLs |
|
|
|
|
**Control surface** (PHP → daemon, `FanoutClient` over the control unix socket):
|
|
|
|
| Call | Purpose |
|
|
|---|---|
|
|
| `PUT /streams/<id>` | register a proxy source (urls, ua, proxy, cookie, ffmpeg, key/iv) |
|
|
| `PUT /ingest/<id>` | create the push ingest socket for a non-proxy/LLOD producer (+ key/iv) |
|
|
| `POST /probe/<id>?wait=` | prewarm + wait for first data → off-air detection |
|
|
| `GET /streams/<id>` | status (`running`, `has_data`, `since_data_ms`) |
|
|
| `GET /connections` | active live-TS viewer uuids (for `fanout_sync` reconcile) |
|
|
| `DELETE /streams/<id>` | stop / drop the stream |
|
|
|
|
`FanoutClient` (`src/Streaming/Fanout/FanoutClient.php`) is the PHP client.
|
|
The **`fanout_sync`** daemon (`console.php fanout_sync`) reconciles connection
|
|
records against `GET /connections` so a viewer's disconnect frees their line's
|
|
slot (under X-Accel, PHP never sees the disconnect itself).
|
|
|
|
---
|
|
|
|
## Where it runs
|
|
|
|
Supervised from `src/service` (a keepalive loop restarts it on crash). Its
|
|
sockets live next to the binary in the app bin tree
|
|
(`bin/xc_fanout/sockets/{http,control}.sock`), mirroring the php-fpm sockets
|
|
layout nginx already reaches over `unix:`. Installed on MAIN first; LB rollout is
|
|
a later phase.
|
|
|
|
### Install / update
|
|
|
|
The binary is fetched from the latest `XC_VM_Fanout` release, like the other
|
|
distributed binaries:
|
|
|
|
```
|
|
console.php fanout_binary # install or update to the latest release
|
|
console.php fanout_binary force # reinstall the current version
|
|
```
|
|
|
|
`FanoutBinaryCommand` reads the installed version straight from the binary
|
|
(`xc_fanout -version`), compares it to the latest release tag, and — when they
|
|
differ — downloads the arch-matched asset (`xc_fanout-linux-<arch>`), verifies its
|
|
SHA-256 against the release's `SHA256SUMS`, installs it atomically, and kills the
|
|
running daemon so the `service` keepalive respawns it with the new binary.
|
|
|
|
See `docs/adr/0001-*`, `0002-*`, `0003-*` for the full design and the phased
|
|
cutover plan.
|