Phase C parity (ADR 0003): restore the legacy startProxy off-air behaviour
for daemon-served proxy streams. After registering a proxy source, live.php
calls FanoutClient::probe($id, on_demand_wait_time*1000) → daemon
POST /probe/<id>, which prewarms the puller and waits for the source to
produce data. If it stays dead, live.php shows the not-on-air page instead
of X-Accel'ing the viewer onto an empty/hanging daemon stream. A warm stream
(already producing) returns immediately, so only a cold/dead source pays the
wait — same as the legacy path.
Non-proxy off-air was already covered (the process block waits for the
playlist, and A3 gates on isStreamFed/has_data).
Box-validated: dead-source proxy → not-on-air video after on_demand_wait_time
(20s here); live-source proxy → fast play via the daemon (4.4 MB valid TS).
PHPStan clean, 11 FanoutClient tests.
Refs ADR 0003 (Phase C). Daemon: XC_VM_Binaries 0.4.0.
Completes Phase A (ADR 0003): non-proxy live streams now both feed the
daemon (A2 tee) and are SERVED by it. At live.php's non-proxy delivery
point, when the daemon is reachable and FanoutClient::isStreamFed($id)
(control GET /streams/<id> → has_data), hand the byte path to nginx→daemon
via X-Accel-Redirect: /xc_fanout/<id> — like proxy mode — instead of
pinning the worker in the per-viewer .ts chase-read. Daemon down / not fed
⇒ the legacy chase-read runs (the reachability rollback).
The tee feeds the daemon from the same ffmpeg that writes the on-disk HLS,
so by delivery time (playlist ready) the daemon has data. Ingest-fed
streams report running:false (no puller), so the gate is has_data.
Box-validated: 6 concurrent non-proxy viewers on stream 569 all served by
the daemon (daemon_conns=6) with php-fpm workers flat (not 1-per-viewer),
valid TS. PHPStan clean, 10 FanoutClient tests.
ADR 0003: A3 done; added a ⚠️ TODO that LLOD v3 (LlodCommand, streams.llod=2)
has no daemon path (separate PHP segmenter) and must be integrated before
Phase D — llod=0/1 already covered via buildLive+tee.
Refs ADR 0003 (Phase A).
Bring non-proxy live streams into the daemon (ADR 0003, Phase A). Before
launching a standard live stream's ffmpeg, StreamProcess registers a daemon
ingest (FanoutClient::registerIngest → PUT /ingest/<id>, returns the socket)
and, when the daemon accepted it, builds the HLS output through the `tee`
muxer so one ffmpeg feeds BOTH the existing on-disk HLS and the daemon:
-map … -f tee "[f=hls:…]<id>_.m3u8|[f=mpegts:onfail=ignore]unix:<ingest.sock>"
- onfail=ignore keeps the on-disk HLS alive if the daemon output drops
(verified: ffmpeg "continuing with 1/2 slaves"), so a daemon crash never
takes the stream down. A plain second output would abort ffmpeg entirely.
- Daemon unreachable ⇒ registerIngest returns null ⇒ the original on-disk-only
HLS output runs, unchanged (the reachability rollback). Loopback/delay
keep the legacy output.
- Two tee-muxer gotchas handled: tee needs an EXPLICIT -map (plain outputs use
automatic selection; tee errors "does not contain any stream") — reuse the
stream's map or -map 0 -copy_unknown; and `individual_header_trailer` is a
segment-muxer option the hls muxer ignores in flag form but rejects fatally
inside a tee slave, so it is omitted.
- stopStream unregisters the daemon ingest; FanoutClient::call() now checks the
socket exists so unregister is a no-op when the daemon isn't deployed.
Box-validated (real panel + ffmpeg, stream 569): one ffmpeg feeds on-disk HLS
(5 segs, m3u8) AND the daemon (/live TS + /hls in-RAM), no errors; panel
playback via the legacy path unaffected (1.69 MB valid TS). PHPStan clean,
441 unit tests pass.
Refs ADR 0003 (Phase A).
Route proxy-mode live TS through the xc_fanout daemon behind the
`live_fanout` settings flag, taking PHP out of the byte path. When the
flag is on, live.php runs its existing auth unchanged, then instead of the
socket_read→echo relay it registers the source with the daemon over the
control unix socket and hands the viewer to nginx via
`X-Accel-Redirect: /xc_fanout/live/<id>` — the FPM worker is freed
immediately, same pattern as P1 HLS delivery. Flag off (the default) keeps
the legacy relay; a failed registration falls through to it in-request, so
a down daemon degrades gracefully.
- FanoutClient: control-socket client (PUT/DELETE /streams/<id> over
FANOUT_CTL_SOCK via cURL unix socket) + buildSource(), a pure mirror of
ProxyCommand's stream_source/user_agent/proxy/cookie extraction whose
JSON matches the daemon's streamConfig.
- Paths: FANOUT_RUN_PATH / FANOUT_CTL_SOCK / FANOUT_HTTP_SOCK under /run.
- nginx: internal `location ^~ /xc_fanout/live/` → proxy_pass unix daemon
client socket (buffering off, long read timeout).
- FanoutClientTest: 8 cases covering source-config parity and the
empty-urls short-circuit.
Refs ADR 0002. Daemon lives in XC_VM_Binaries (not this repo).