Commit Graph
5 Commits
Author SHA1 Message Date
rootandClaude Opus 5 1b86ba3670 feat(fanout): surface the daemon's memory, and let the panel drain a node
Two control calls the daemon has always answered and the panel never made.

FanoutClient::memory() reads GET /memory, and status() now carries it — so it
lands in servers.watchdog_data through the watchdog that already writes that
array, and the admin server views read it back by key with no view change. The
daemon's RSS is dominated by the per-stream join rings, by design: one watched
channel holds its whole prebuffer. Until now the panel could see that a node
was running hot and nothing more, and "the rings are as big as the tuning says"
and "something else is growing" are different problems with different answers.

FanoutClient::unregisterAll() reads DELETE /streams, the bulk teardown for a
maintenance drain or a decommission, instead of scripting a DELETE per id. The
daemon logs each teardown, so it stays an auditable operator action.

Both answer null when the daemon is unreachable, which for the watchdog is an
ordinary state rather than an error, and status() skips the memory call when
the socket did not answer.

Verified by loading the class directly: with no control socket, status() keys
are running,socket,connections,memory with memory null, and both new calls
answer null. The PHPUnit suite could NOT be run here — no PHP on this box has
the dom/mbstring/xml/xmlwriter extensions phpunit.phar requires — so the two
tests added to FanoutClientTest are unrun; they assert exactly what the harness
above printed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 00:45:37 +00:00
Divarion_D 8b4342cc75 feat(streaming): P2/C — off-air detection for the daemon proxy path
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.
2026-08-16 19:08:16 +03:00
Divarion_D 401e2307e4 feat(streaming): P2/A3 — route non-proxy live viewers through the daemon
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).
2026-08-16 18:37:42 +03:00
Divarion_D 070d89b3c4 feat(streaming): P2/A2 — tee non-proxy live ffmpeg into the xc_fanout daemon
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).
2026-08-16 18:16:45 +03:00
Divarion_D 6d3c84a473 feat(streaming): P2/S3 — proxy-mode live TS via xc_fanout, flag-gated
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).
2026-08-16 15:54:40 +03:00