Make 'beta' the canonical update_channel value across the settings UI,
GitHubReleases (prerelease filter + cache-file suffix + setChannel), the
binary/fanout update commands, and the module channel mapping.
'unstable' is kept as a legacy alias so nothing breaks mid-upgrade:
GitHubReleases::normalizeChannel() maps it to 'beta', and the binary commands
still accept it. Migration 011 rewrites existing settings.update_channel
'unstable' -> 'beta', and the settings dropdown normalizes a stale 'unstable' to
show Beta selected before the migration runs. Docs (en) updated.
Add a version selector to the docs and publish one snapshot per release instead
of a single rolling site, so readers can pick the docs matching their installed
version (the docs change release to release).
- mkdocs.yml: enable the Material version selector (extra.version.provider: mike,
alias: true).
- docs/requirements.txt: add mike==2.1.3.
- pages.yml: deploy with mike, triggered by a release TAG (semver) instead of
every push to main — publishing is tied to the release because docs/ru is only
regenerated then. Deploys `X.Y.Z` + the `latest` alias to the gh-pages branch
and sets latest as default. workflow_dispatch takes an explicit version.
- updates_checklist.md: note the tag-triggered versioned publish.
One-time setup (GitHub UI): Settings → Pages → Deploy from a branch → gh-pages.
Add a Developer Guide page covering the admin `?action=` JSON endpoints:
- The PSR-4 controllers under Admin\Ajax that replaced the retired ~4985-line
Views/admin/api.php (PR #173) — BaseAjaxController scaffolding
(ok/fail/gate/gateAny/requireXhr/json), LineStateTrait, per-area controllers,
route registration and the dispatchApi → AjaxController fallback order.
- The structured search JSON contract (PR #174): envelope, item shape,
self-describing actions, per-entity data, and the client-side card renderer;
plus the note that only the render path changed (matching is unchanged; a
missing live stream means a stale streams FULLTEXT index).
Wire it into the Developer Guide nav (+ ru nav_translations) and cross-link it
from HTTP Request Handling. English source only; docs/ru is regenerated before
release.
Replace the server-side HTML builder in the `search` admin-ajax action with a
structured JSON contract: each result item now carries `{id, url, text, entity,
data}` and the client renders the card. Permission checks, status resolution and
category/server lookups stay server-side; only markup moves to the browser. This
unblocks the upcoming redesign, where the embedded HTML builder was in the way.
- SearchAjaxController::search() now emits structured items (per-entity `data`
payloads for stream/movie/channel/radio/episode, series, user, line, mag,
enigma) plus a `no_results` item for empty responses. Data-gathering is
unchanged (byte-identical to the previous fuzzy full-text gather).
- New client renderer `assets/admin/js/search.js` (`renderSearchItem`), loaded
before common.js; the Select2 quick-search `templateResult` now calls it
(with a loading-state guard) instead of consuming a server `html` field.
- ADR docs/adr/search-json-contract.md documents the envelope, item shape,
self-describing actions and per-entity payloads.
Note: this changes only the search render path, not search matching. Live
streams missing from results are a stale streams FULLTEXT index on the DB
(rebuild with `ALTER TABLE streams ENGINE=InnoDB`), unrelated to this change.
The line-by-line web translator reordered words inside `**bold**` spans and
misplaced/dropped the markers, producing `**LB` or `****` (empty bold). Mask
each `**...**` as ONE atomic sentinel: translate the inner text on its own,
then store the whole balanced `**inner**` — the engine never sees the markers
and cannot reorder or collapse them. Also harden the anthropic prompt to keep
emphasis balanced.
Auto-prune: after translating, delete generated docs/ru files whose docs/en
source no longer exists (renamed/removed) and drop now-empty dirs, so the tree
mirrors docs/en 1:1 (removes the stale development/modules.md and
guides/geoip-and-device-detection.md).
Bump PROMPT_VERSION to 6 to invalidate the contaminated cache and regenerate
docs/ru (0 broken bold spans remaining, aside from pre-existing multi-line
bold that spans a soft line break).
- Move "Pre-Release Validation" to step 2 (right after Changelog) and fold the
"Regenerate translated documentation" sub-step (make docs-translate/build)
into it, so code checks and doc regeneration happen together, early.
- Run `make new` as the very first action (step 1), before generating
dist/changes.md, and drop it from "Build Archives". `make new` wipes AND
recreates dist/, so running it in the build step deleted the changes.md
generated in step 1; main/lb don't depend on new and their final `clean`
only removes TEMP_DIR, so changes.md now survives to the GitHub Release step.
- Renumber sections and update the make-new command reference.
Adds a "safety net" rollback so a server can be downgraded to an earlier
release without a manual redeploy, mirroring the existing update flow.
- GitHubReleases: getVersionFile() fetches a specific version's asset (not
just latest); getPreviousVersions() lists the newest releases older than a
given version, each flagged beta (GitHub prerelease). getReleases() now
delegates to a shared getFilteredReleases() that keeps the prerelease flag.
- UpdateCommand: new `rollback <version>` case — validates the target is a
real, strictly-older release; on MAIN takes an automatic DB backup first
(aborting if it fails); downloads the exact version (main/lb_update asset),
verifies MD5, and hands off to the same python `update` applier.
- RootSignalsCronJob: dispatch the `rollback` signal to `console.php update
rollback <version>` (version regex-validated, escapeshellarg'd).
- api.php: `rollback_versions` (channel-aware list, optional per-server base
version) and server `sub=rollback` (validates version, writes the signal).
- servers.php: per-server "Rollback Version" action (dropdown item + button)
opening a modal to pick a version; beta builds show "(beta)". Works for
MAIN and each LB independently.
- docs: rollback sections in server-update.md and update-system.md.
The downgrade reuses the version-agnostic python applier, so binaries,
config and user data are preserved. Migrations are forward-only and are not
rolled back; the automatic MAIN backup is the recovery path.
stream_queue_check.py: batch --playlist mode, per-stream JSON via --out-dir, TTY-aware summary vs JSON, HLS health judged by rebuffers (TS by stall), 120s default duration, --tolerance/--stall-timeout. New stream_graph.py renders the checker JSON as dependency-free SVG charts (per-stream + --combined comparison, unique colour per stream). Both moved under tools/stream-check/ with a README; tools/README.md and docs/en updated.
PHP-CS-Fixer's `no_unused_imports` is conservative — it treats a class name that
merely appears in a PHPDoc *description* as "used", so genuinely-dead imports
(e.g. `use ...Request;` next to a "Request IP" doc description) were never
flagged. Slevomat's UnusedUses is precise: it parses annotation *types*
(@param/@return/@var), so it keeps docblock-typed imports but removes truly
unused ones — matching what Intelephense (P1003) reports.
- Swap require-dev: friendsofphp/php-cs-fixer -> squizlabs/php_codesniffer +
slevomat/coding-standard (+ phpcodesniffer-composer-installer, allow-listed).
- New narrow ruleset build/phpcs.xml.dist (import/namespace hygiene only, NOT
full PSR-12): UnusedUses (searchAnnotations=true), UseFromSameNamespace,
UseDoesNotStartWithBackslash, AlphabeticallySortedUses, UseSpacing,
NamespaceSpacing. View templates stay excluded.
- Makefile: `make cs` -> phpcs, `make cs-fix` -> phpcbf (same target names).
- CI code-style job, CLAUDE.md, CONTRIBUTING.md, docs, .gitignore updated;
build/.php-cs-fixer.dist.php removed. Committed vendor stays production-only.
Move the generated site out of the repo root: site_dir -> build/site, with the
Pages upload path and .gitignore updated to match. build/ already holds tooling
config, so the build output no longer clutters the top-level listing.
Fix Markdown-mangling artifacts the free web engine (yandex) produced in the
committed docs/ru, and regenerate the whole tree cleanly (0 fallbacks):
- Sentinel format @@N@@ -> {N}. MT engines are trained to preserve curly
format-string placeholders, so {N} survives code-heavy lines where @@N@@ (and
ZZZ…ZZZ, which also duplicated its Z) were split/moved — e.g. the stray
"@0@@" in the FAQ and "load balancerZ" in the README are gone.
- Possessive: a trailing English `'s` is consumed INTO the masked span and
dropped on restore. Every sentinel format breaks when a bare `'s` sits right
after it, and Russian has no possessive `'s`.
- Validate + retry: after restore, any leftover brace fragment triggers a retry
(the engine is non-deterministic); after a few failures the line stays English
so a broken token is never emitted.
- Glossary += KeyDB, yt-dlp, Ubuntu, iptables, MAGSCAN.
Regenerated docs/ru (37 files, translators/yandex): no residual sentinels,
mkdocs build --strict clean.
CI translation was slow, so move it out of GitHub Actions: docs/ru is now a
committed, generated tree refreshed LOCALLY before a release; CI only builds it.
- pages.yml: drop the setup-python/cache/translate steps — the workflow now just
installs the build toolchain and runs `mkdocs build --strict` on the committed
en+ru trees.
- .gitignore: stop ignoring docs/ru (now committed); keep site/ + .docs-cache/.
- Split deps: docs/requirements.txt = build only (mkdocs-material, static-i18n,
used by CI); tools/docs/requirements.txt = translators (local-only).
- Makefile: docs-venv installs both; docs-build/docs-serve no longer translate
(translation is the deliberate `make docs-translate` release step).
- updates_checklist.md: new "Regenerate translated documentation" step
(make docs-translate + docs-build, commit docs/ru with the release commit).
- Commit the generated docs/ru (37 files, translators/yandex).
Rule: edit ONLY docs/en; docs/ru is generated — never hand-edit it.
Enrich the English source (ru regenerates automatically) with subsystem
behaviour that was missing or stale:
- streaming-subsystem: live client delivery is now daemon-only via xc_fanout
(X-Accel handoff, fan-out over a unix socket, control-socket off-air/telemetry,
fanout_sync reconciliation); on-disk HLS kept only for timeshift/thumbnail/
analyse. Documented the admin "Send Message" drawtext overlay via
POST /signal/<uuid>. Replaced the stale generateHLS/chase-read delivery step.
- caching-and-redis: how long-lived daemon connections survive a server idle
`timeout` close (phpredis silent reconnect without AUTH → non-PONG guard +
re-authenticated reconnect; getCapacity multi() guard), and cold-cache
fail-closed defaults in LegacyInitializer::initStreaming().
Replace the hand-maintained Docsify site (parallel docs/en + docs/ru trees
that had already drifted) with a MkDocs Material build where English is the
single source of truth and Russian is generated at build time.
Engine & structure
- mkdocs.yml: Material theme, site_url for the /XC_VM/ Pages subpath, and a
two-tab information architecture — User Guide (administration, API/Swagger,
UI translations, diagnostics, info/FAQ) vs Developer Guide (architecture,
workflow, security, integrations, build). Files are NOT moved — the split is
nav-only, so URLs and cross-links stay stable.
- mkdocs-static-i18n (folder mode): English at root, Russian under /ru/, with a
language switcher. `mkdocs build --strict` validates every link/anchor.
Translation pipeline (tools/docs/translate.py)
- Engine-agnostic via DOCS_TRANSLATE_PROVIDER: translators (free, no API key —
default), anthropic, deepl, or noop. Per-file sha256 cache so only changed
English files are re-translated. Markdown-safe: code, URLs, HTML tags and
glossary terms (XC_VM, FFmpeg, HLS, ...) are masked and never translated.
A file whose translation fails falls back to English so the build never breaks.
- docs/ru is generated and gitignored — never committed. It is produced locally
(`make docs-serve` / `docs-build`) and in CI.
CI & tooling
- pages.yml: build-then-upload (setup-python -> install -> restore .docs-cache
with restore-keys -> translate -> mkdocs build --strict -> deploy), replacing
the verbatim docs/ upload.
- Makefile: docs-venv / docs-translate / docs-build / docs-serve.
- docs/requirements.txt; .gitignore for docs/ru, site/, .docs-cache.
Migration details
- Removed Docsify control files (index.html, _navbar.md, _sidebar.md, .nojekyll)
and de-Docsify-ed body links in 6 English files (en-us/ aliases -> relative,
swagger _media paths, stripped ':ignore' link syntax).
- Preserved the two Russian-only planning docs (no English source) by moving
them into docs/adr/ as *.ru.md (repo-internal, excluded from the site).
The PHP SignalSender byte-path overlay class had 0 callers after E3 moved
the admin "send message" feature into xc_fanout (drawtext on the viewer's
HLS segment / TS window via FanoutClient::sendSignal -> POST /signal/<uuid>).
Delete the class and scrub its now-dangling mentions:
- src/Streaming/Delivery/SignalSender.php: removed (git rm)
- FanoutClient.php: comment reworded (legacy PHP byte-path, not the class)
- docs/{en,ru}/development/streaming-subsystem.md: dropped the tree line
- docs/adr/0003: overlay is e2e-proven on the LB; note the drawtext-ffmpeg
selection gotcha (bundled 8.0/7.1 lack the filter)
E2 re-applied on the current feature work; live.php 698->467, chase-read +
orphaned SegmentReader removed; daemon-only non-proxy TS. Gating satisfied (daemon
box-proven across all types on LB this session). E3 stays not-done by decision.
E2 deleted live.php's non-proxy chase-read — the only user of SegmentReader
(playlist segment extraction). Remove the now-dead class, its unit test, and the
doc references. SignalSender (still used by segment.php) and CacheReader (used
widely) are kept.
xcvm_core self-update ships config_set_redis to LB nodes -> configureRedisLb points
Redis at MAIN -> RedisManager connects -> viewer served video/mp2t by the daemon on
the LB (LINE_CREATE_FAIL gone, /connections+/rates populated, php-fpm flat). Item 1
(LB non-proxy source-mode e2e) closed; edge-types remain for coverage.
Deployed current code to the canary LB (was pre-daemon): ffmpeg tee -> daemon
has_data, /live + /hls served on the LB, viewer redirect lands on the LB. Blocked
at LINE_CREATE_FAIL: redis_connect reads creds from config.enc only, the sole
setter config_set_redis is absent in all xcvm_core builds, and the LB config.enc
has no Redis creds. socat fixes the host (refused->NOAUTH) but not the password.
Needs a new xcvm_core with config_set_redis (StatusCommand::configureRedisLb is
the committed forward-looking fix).
LoopbackCommand mirrors to the daemon ingest socket (like LlodCommand), so an LB
restreaming from MAIN via php_loopback serves viewers through the daemon, not the
PHP byte path. Only a live-node e2e remains (canary LB currently unreachable).
Root cause of the LB 'no fanout locations' defect: the LB build overrides
nginx.conf with the stale lb_configs/nginx.conf (missing all X-Accel locations).
Fixed. libogg already in getPackages; daemon 0.8.0 released. Only a real-source
stream-to-LB e2e remains (external source/assignment).
fanout_binary path verified on canary (0.7.3 -> pulls 0.8.0 from the new GitHub
release -> installs -> restart). Panel P4 (connectionRates + writeDivergence) still
needs to land in the panel release for the telemetry to be consumed fleet-wide.
Daemon GET /rates + fanout_sync writeDivergence proven end-to-end on the canary
(redis mode): viewer on stream 568 -> /rates 857 KB/s -> lines_divergence written
(0.0, healthy). Takes effect fleet-wide once the 0.8.0 daemon is released as
latest.
- 0002 §2.5 rewritten: light-P4 reconciliation shipped; per-viewer divergence is the
remaining P4 gap (daemon-served viewers never enter the chase-read loop)
- 0002 S6 marked done (scale validation: O(N) worker-per-viewer ceiling removed)
- 0002 Risks/§2.4/S1-sketch: auth_request references reframed as superseded by X-Accel;
removed live_fanout flag operational claims
- 0003 Phase C + Risks + snapshot: new P4-in-progress bullet for divergence/bitrate
telemetry, with slice-1 plan (daemon /rates -> fanout_sync writes lines_live.divergence)
Status → Accepted/in-progress. Record what the code actually shows: phases 0/A/B
done, Phase C in progress (C1a cold-start done, C1b moot, C2 adaptive verified,
C3 restreamer-prebuffer fixed), E1 done, Phase F cancelled (on-disk HLS stays).
Add a Phase C work breakdown (C1–C5 + C-ops) including the fanout_sync
self-respawn fix and the daemon-restart re-feed gap surfaced in production.
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.
Plain-language companion to the ADRs: the worker-per-viewer problem it
solves, what it does (and doesn't), and the client/control surfaces PHP
uses (FanoutClient, X-Accel routes, fanout_sync).
Close Phase B's unencrypted-only limitation: encrypted HLS is now served from
the daemon's in-RAM segmenter too, so encrypt_hls streams no longer need the
legacy tmpfs .enc path — one of the two prerequisites for dropping the
streaming tmpfs (Phase F).
- StreamProcess: generate the stream's HLS key/iv BEFORE registering the daemon
ingest (moved writeStreamKeyIv up) and, when encrypt_hls is on, hand the hex
key/iv to the daemon at FanoutClient::registerIngest so it encrypts the HLS
segments it serves.
- FanoutClient::registerIngest: optional key/iv passed in the ingest PUT body.
- HLSGenerator::tokenizeDaemonPlaylist: prepend #EXT-X-KEY (URI /key/<token>,
IV from <id>_.iv) for encrypted streams, matching generateHLS.
- live.php m3u8: drop the !encrypt_hls gate — daemon HLS now covers both.
- segment.php unchanged: the daemon-segment X-Accel already returns the
daemon's (now-encrypted) bytes.
Box-validated: encrypt_hls=1 → 569.m3u8 has #EXT-X-KEY + tokenized daemon
segments; a segment decrypts (stream key + playlist IV) to a valid TS. Daemon
crypto is byte-identical to openssl. PHPStan clean, 443 tests. Daemon 0.6.0.
Refs ADR 0003 (Phase B encrypted). Daemon: XC_VM_Binaries 0.6.0.
Phase C (ADR 0003): fix the connection-limit leak for daemon-served live TS.
Under X-Accel the auth PHP-FPM worker returns immediately, so UsersCronJob's
reaper — which closes a TS row when its pid stops being a running php-fpm
worker — can neither track nor time out a daemon viewer (its recorded pid is
a reused, still-alive worker). Those rows leaked, filling the line's slots
until a false "max connections reached".
Fix (light P4):
- live.php marks daemon-served TS rows pid=0 (decided once per request via
$rTSDaemon: proxy $rFanout, or non-proxy isStreamFed), and passes the
connection uuid to the daemon in the X-Accel URL (?c=<uuid>).
- daemon (0.5.0) tracks connected uuids per stream and serves them at
control GET /connections.
- new fanout_sync daemon reconciles pid=0 open rows against that set every
~10s, closing any whose uuid is no longer connected (20s connect-grace);
skips reconcile when the daemon is unreachable (added to service boot()).
- UsersCronJob leaves pid=0 TS rows to fanout_sync (reaper would leak or,
on worker recycle, close them early).
HLS was already self-heartbeating (playlist re-fetch → updateLive), so this
targets long-lived TS. Box-validated: viewer → pid=0 row + uuid in
/connections; disconnect → daemon drops the uuid and fanout_sync closes the
row (slot freed). PHPStan clean, 443 tests. Daemon: XC_VM_Binaries 0.5.0.
Refs ADR 0003 (Phase C).
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.
Phase B (ADR 0003): HLS is now served from the xc_fanout daemon's in-RAM
HLS end-to-end for unencrypted streams, PHP doing only auth — the stepping
stone toward dropping the streaming tmpfs.
- live.php m3u8 branch: when the daemon is reachable, the stream is fed
(isStreamFed) and encrypt_hls is off, fetch the daemon playlist
(FanoutClient::hlsPlaylist → client socket) and tokenize its <seq>.ts
entries (HLSGenerator::tokenizeDaemonPlaylist) into the same /hls/<token>
auth'd URLs as the legacy path, with the segment marked <id>_d<seq>.ts.
Else the legacy on-disk generateHLS runs (encrypted / daemon down).
- segment.php: a daemon-segment token runs the same uuid + IP checks, then
X-Accel-Redirect: /xc_fanout_hls/<id>_<seq> → daemon /hls/<id>/<seq>.ts.
- nginx: internal location ^~ /xc_fanout_hls/ → proxy_pass daemon (two path
segments, id_seq, to avoid the server-level rewrite hijack — same reason
as /xc_fanout/ and /xc_hls/).
The daemon's HLS on-demand lifecycle was already closed (0.2.0 touch+reaper).
Box-validated: 569.m3u8 → daemon MEDIA-SEQUENCE + tokenized segments; a
segment fetch → 200 valid TS (953 KB) from the daemon's RAM. PHPStan clean.
Limitation: daemon HLS is plain mpegts → unencrypted streams only; encrypted
streams stay on legacy tmpfs HLS. Daemon-side encryption + removing the tee's
on-disk HLS slave are what Phase F needs to drop the tmpfs.
Refs ADR 0003 (Phase B).
The last live stream type not on the daemon. LLOD v3 is a PHP MPEG-TS
segmenter (not ffmpeg), so it can't tee — instead it now mirrors the raw
bytes it reads into the daemon's push-fed ingest socket
(FanoutClient::registerIngest + a non-blocking stream_socket_client write).
The same stream then fans out via /live/<id> and in-RAM /hls, and live.php's
A3 isStreamFed() routes viewers to the daemon.
- Non-blocking, best-effort write: a daemon stall never slows LLOD's own
segmenting (its primary job); on a write error (daemon gone) it stops
mirroring and live.php falls back to legacy. Null/failed connect ⇒
legacy-only, no behaviour change.
- LLOD keeps writing its on-disk segments (legacy fallback intact).
- Cleanup: closes the conn and unregisters on clean exit; stopStream() is
the backstop when LLOD is killed mid-loop.
Box-validated (stream 599, llod=2): daemon fed (has_data) with a routed
viewer (refs:1), valid /live TS + /hls m3u8, LLOD segments still produced,
playback OK (3.98 MB). PHPStan clean.
Now every live stream type runs through the daemon when reachable: proxy
(S3), non-proxy llod=0/1 (A2 tee + A3), and LLOD v3 (push + A3).
Refs ADR 0003 (Phase A).
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).
Record the daemon ingest listener (0.3.0) as done and box-validated, with
the validated production tee command for A2, and the A2/A3 coordination
(register ingest before ffmpeg; reachability switch).
For a permanent cutover a runtime settings flag is throwaway scaffolding
(and a settings column that ends up always-1 is dead weight). Remove the
`live_fanout` dependency from live.php entirely; the switch is now the
daemon itself.
live.php decides proxy → daemon vs legacy on REGISTRATION SUCCESS, moved
ahead of the legacy process block: if the control socket is present and
FanoutClient::register succeeds, skip legacy startProxy and X-Accel to the
daemon; otherwise run the FULL legacy path (producer included). This also
fixes a latent bug in the old flag path — it skipped startProxy but could
fall through to a producerless legacy relay when registration failed.
Rollback is now "stop the daemon": its socket disappears / register fails,
every stream falls back automatically, per node, no DB write, no cache
rebuild, self-healing via the keepalive. Which stream types go to the
daemon stays a code decision (proxy now; +non-proxy after Phase A).
ADR 0003 Phase 0 dropped (no schema column / admin toggle); ADR 0002
rollback-flag note marked superseded. Test-box live_fanout column to be
dropped.
Refs ADR 0003.
Wrap the xc_fanout launch in service boot() in a keepalive loop (2s
backoff) so a daemon crash is auto-restarted — the Restart=always
equivalent without adding a second systemd unit. Both the loop and the
daemon run as xc_vm, so stop()'s `pkill -u xc_vm` ends both together and
nothing respawns during shutdown; the daemon still logs to tmp/xc_fanout.log.
ADR 0003: mark the Phase C foundations landed (HLS on-demand lifecycle,
off-air status endpoint, auto-restart — daemon 0.2.0 in XC_VM_Binaries).
Refs ADR 0003.
Plan the full transition of all LIVE streaming (proxy + non-proxy, TS +
HLS) onto the xc_fanout daemon, then retire the legacy byte path. Scopes
VOD/timeshift OUT (random-access, not fan-out); keeps per-stream ffmpeg
(transcode/logo) and PHP auth, moving only delivery to the daemon.
Phases: 0 schema/flag column + admin toggle → A non-proxy via tee'd
ingest (S4) → B HLS end-to-end through the daemon (+ HLS on-demand
lifecycle) → C parity/robustness (off-air, cold start, adaptive,
restreamer, limits, disconnect accounting) → D make daemon default → E
delete legacy (ProxyCommand, live.php byte loops, tmpfs HLS serving,
CONS_TMP_PATH sockets) → F drop streaming tmpfs → G LB rollout.
Records the box A/B result and the two daemon gaps surfaced (HLS must join
the on-demand lifecycle; daemon needs a health/last-data signal for
off-air detection).
The fanout hand-off used `X-Accel-Redirect: /xc_fanout/live/<id>`, but nginx
server-level `rewrite` directives run during the rewrite phase on internal
redirects too — before location selection — and the 3-segment catch-all
`^/(user)/(pass)/(stream)` rewrote `/xc_fanout/live/569` into
`/stream/auth?username=xc_fanout&password=live&stream=569`, yielding
INVALID_CREDENTIALS instead of reaching the internal proxy location.
Fix: make the target two path segments (`/xc_fanout/<id>`, location
`^~ /xc_fanout/`), which matches no server rewrite — the same reason P1's
`/xc_hls/<file>` target is two segments. Added a NOTE in nginx.conf so it
isn't "simplified" back to a 3-segment form.
Validated end-to-end on the test node with a real player: flag-off vs
flag-on A/B at 8 concurrent viewers on a real proxy stream — legacy pinned
~1 php-fpm worker + ~24 MB/viewer (9 workers / 242 MB); fanout served all 8
through the daemon (daemon_conns=8) with php-fpm flat at baseline (1 worker
/ 45 MB). PHP fully out of the byte path.
Refs ADR 0002.