Client delivery is daemon-only since ADR 0003 Phase E, but several
producers never fed the daemon, or fed it in a way no player could use:
- Delayed channels (delay_minutes > 0) were unwatchable: the encoder's tee
is skipped for them (its output is the undelayed stream), the supervisor
refuses them, and DelayCommand never registered an ingest, so every
viewer got not-on-air. DelayCommand now pushes each delayed segment into
the daemon as it publishes it (IngestFeeder), paced over the segment's
duration, with a two-segment burst at start. Its loop polls every 50 ms
instead of hashing the playlist every 1 ms.
- An ffmpeg loopback (php_loopback off, or no supervision) registered no
ingest at all; startStream now tees loopback streams too.
- Encrypted HLS was undecodable for loopback and llod=2: the playlist
declares AES-128 whenever encrypt_hls is on, but those producers
registered without the key, so the daemon served plain segments. Every
producer now passes the key; startLLOD/startLoopback write it before
spawning the child that registers.
- LlodCommand/LoopbackCommand feed through IngestFeeder (no torn packets,
reconnect after a daemon restart). LLOD also honours the stream's
headers/cookie/proxy/default user agent and request_prebuffer, accepts
TS sources by content when the Content-Type is not video/mp2t, kills a
stale segmenter by process title (it read the MONITOR's pid), and writes
its playlist atomically (the loopback relay too).
- LLOD ffmpeg: +nobuffer moves to the input where it has an effect;
-tune zerolatency only for x264/x265 (NVENC rejects it and failed the
start; it gets -zerolatency 1); an LLOD start uses the first source
instead of falling through to the last.
- Remote VOD subtitles URL-encoded the shell-quoted path.
- cron:streams: the restreamer "attached" subqueries lacked GROUP BY, so
on-demand parents could be stopped under a child LB; a stopped on-demand
stream went on to spawn thumbnail/archive workers; the daemon re-feed
restart now skips self-feeding producers (PHP relays, delay) and its
throttle stamp moved out of STREAMS_PATH, where the restart's
`rm -f <id>_*` deleted it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two reasons the native remuxer never ran on a real panel, both in the
eligibility check:
- it compared `type_key` against `live_streams`, which is no type at all —
`streams_types` holds (1, 'Live Streams', 'live'), (3, 'created_live'),
(4, 'radio_streams'). Every ordinary live channel was refused, so
`fanout_source_backend` native/auto silently kept running ffmpeg. The new
log line said it out loud ("ffmpeg runs this stream: not a live channel"),
which is how it surfaced; the refusal now names the type it saw.
- `gen_timestamps` and `read_native` were treated as "the operator asked for
timestamp repair / realtime pacing", but both DEFAULT to 1 in `streams`, so
they carry no intent and refusing them refuses everything. -re paces a
file-ish input, which a passthrough of a live source does by itself, and
genpts only synthesises timestamps a source failed to send — a source that
broken has no usable video clock either, which ends the run with exit 3 and,
in `auto`, hands it to ffmpeg.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K48c64npichw9ZCZDU16ja
With fanout_supervise on, StreamProcess::startMonitor() no longer spawns a
PHP watchdog. It builds the stream's commands and hands them to the
daemon's supervisor (PUT /monitor/<id>), which starts, watches and
restarts them: failover, priority backup, forced source, stalled output,
audio loss, frame-rate drop and scheduled restart. PHP still builds every
command and makes every database write.
A copy-only live stream's command is the daemon's native remuxer,
`xc_fanout remux`, composed by the new buildNativeLive() exactly as
buildLive() composes an ffmpeg line. It reads the source natively and
writes the same on-disk HLS and daemon feed as the ffmpeg -f tee output,
with no ffmpeg process. fanout_source_backend decides: auto = remuxer with
the ffmpeg command as fallback_cmd (taken when the remuxer exits 3,
"cannot serve this source": fMP4 or encrypted HLS, rtmp, no keyframes),
native = remuxer only, ffmpeg = ffmpeg only. Eligibility is explicit
(isNativeEligible / isNativeSource): no transcode, custom ffmpeg, custom
map, RTMP output, external push, timestamp repair, read-native or forced
input codec; http(s)/udp/rtp sources only.
The rest of the panel learns who owns the producer:
- superviseStream() asks the daemon first and touches nothing unless it
is accepting; without a restart it adopts a running encoder, so
cron:streams moves PHP-monitored streams over with no blip. A producer
the daemon cannot adopt (PHP LLOD, PHP loopback) is replaced, never left
beside the new one. The row is marked watched before the hand-over, so
the reconcile cannot release a stream mid-start.
- reconcileSupervised() copies the daemon's state (status, pid, source,
codecs, resolution, measured bitrate) into streams_servers: every
cron:streams pass and every 5 s from the signals daemon. Supervised
streams whose row is gone or stopped are released.
- stopStream() and the on-demand reaper release before killing anything;
killing the producer first is what the supervisor restarts.
- isWatched() replaces bare isMonitorAlive() checks in live.php,
admin/live.php, rtmp.php and cron:streams: a supervised stream's
monitor_pid is the daemon's. MonitorCommand stands down for supervised
streams; startMonitor() releases one before falling back to PHP, so
turning supervision off does bring streams back on their next restart.
- force_stream goes through the daemon for a supervised stream (the .force
file is only read by the PHP monitor).
- ProcessManager::isStreamRunning() recognises the remuxer, so the
archive, thumbnail and delay workers follow it like ffmpeg.
- A supervised loopback child tees into the daemon (the supervisor judges
a stream by the bytes it receives); legacy loopback is unchanged.
Delay streams, created channels and yt-dlp platform sources stay on the
PHP monitor. A daemon without /monitors/state (older than this) is never
handed a stream, so the panel is safe against an un-upgraded node.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012QL93N6dzGkmKoQgA4oh16
Reverts the five commits PR #1 brought in and the two test follow-ups made
after it merged:
a1d454fe Enhance assertions in FanoutConfigTest
19607a91 Update StreamProcessBuildLiveTest.php
b3714269 fix(fanout): stop, force and rogue-kill must account for the daemon
cea545fe feat(fanout): reconcile supervised streams back into the panel
8e24a242 feat(fanout): make encoder supervision a real, settable option
8244ece1 feat(fanout): health policy, source forcing and supervision reconcile
56c0619e feat(fanout): hand live encoders to the daemon to supervise
Only those. The upstream commits that reached main through the same merge
(the GeoIP refactor, release 2.5.0, the update guard, the mass-page fix)
stay: reverting the merge commit itself would have taken them out too.
The panel side of supervision is re-done in the commits that follow,
together with the xc_fanout native remuxer (xc_fanout 0.13.0): every live
stream handed to the daemon's monitor, copy-only streams run on
`xc_fanout remux` instead of ffmpeg, the ffmpeg command as an explicit
fallback.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012QL93N6dzGkmKoQgA4oh16
Phase 1 of extracting the live ffmpeg command assembly out of startStream.
buildLive(array $data): string is a byte-faithful copy of the inline assembly,
fed from a prepared $data array instead of loop-local state, with the delay
playlist I/O and segment-start/sleep left in startStream (arriving via
$data['segmentStart']/['delayActive']). The one non-verbatim change is the
ac3/eac3 dts_legacy bin switch, now a local reassignment instead of mutating the
$rFFMPEG_CPU/$rFFPROBE globals (the $rFFPROBE write was already dead). Homed on
StreamProcess for now so the private output/logo/aac helpers resolve via self::;
a FFmpegCommand facade is a follow-up.
startStream computes buildLive() alongside the inline assembly and error_log()s
any divergence, but still executes the inline $rFFMPEG -- shadow mode, no flip.
Once the diff log stays empty on real traffic the call site can switch over.
Characterisation tests cover simple/custom_ffmpeg/loopback/delay/rtmp branches
and assert no unresolved {TOKEN} survives.
Verified: phpstan level 5 green, phpunit 397/397, make gates green.