The daemon reads a stream's codecs and picture size off the bytes it fans out
and reconcileSupervised copied them into `video_codec`, `audio_codec`,
`resolution` and `bitrate` — but not into the `stream_info` JSON, which is the
shape the rest of the panel actually reads. A supervised stream therefore
showed "? x ?" and "N/A" in the streams list; worse, every adaptive variant was
dropped from the master playlist for want of a width, and stream/auth.php fell
back to calling every stream h264 when handing the viewer its codec.
The JSON is now written beside the columns, merged rather than replaced, so
whatever ffprobe once found that the daemon does not read (frame rate,
container) survives, and an unchanged reading writes nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZP7fc2Hz36wbPmuLd9F9o
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
Three things an operator could not see, all in the file they already open:
- the command handed to the supervisor is recorded beside the stream's files
the way the self-launched path records its ffmpeg line — <id>_.fanout for the
native remuxer, <id>_.ffmpeg for ffmpeg (in auto, both: the second is the
fallback). A supervised stream used to leave no record at all.
- when the native backend is on and a stream runs ffmpeg anyway, the reason is
appended to <id>.errors ("[panel] ffmpeg runs this stream: Generate PTS is
on"). isNativeEligible() becomes nativeRefusal(), returning that sentence
instead of a bare false, because the answer is always one of these settings.
- the panel only composes `xc_fanout remux` when the node's daemon advertises
it (features in GET /monitors/state, FanoutClient::supportsRemux). An older
binary does not reject that command, it misparses it — "remux" reads as a
positional argument, the process tries to become a second daemon on sockets
the running one holds, and the stream never starts. On a node whose panel was
updated first, streams now keep running ffmpeg and say so.
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