feat(streams): run live streams under the xc_fanout monitor, copy-only ones on its native remuxer

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
This commit is contained in:
root
2026-09-11 09:20:03 +00:00
co-authored by Claude Opus 5
parent 0603e5d2ab
commit 30eea7635c
14 changed files with 1233 additions and 41 deletions
+12 -1
View File
@@ -157,6 +157,15 @@ class ProcessManager {
);
}
// The fanout daemon's native remuxer (`xc_fanout remux … <streams>/<id>_.m3u8`),
// which produces a copy-only stream in ffmpeg's place. Its own subcommand
// and this stream's playlist must both be there: the daemon process shares
// the executable but names no stream playlist.
if (strpos($exe, 'xc_fanout') === 0) {
$cmdline = (string) @file_get_contents('/proc/' . $pid . '/cmdline');
return strpos($cmdline, "\0remux\0") !== false && strpos($cmdline, '/' . $streamId . '_.m3u8') !== false;
}
if (strpos($exe, 'php') === 0) {
return true;
}
@@ -415,7 +424,9 @@ class ProcessManager {
/**
* Start a stream monitor process in background.
*
* Extracted from ProcessManager::startMonitor().
* Always the PHP watchdog. Stream code calls StreamProcess::startMonitor()
* instead, which hands the stream to the fanout daemon's supervisor when this
* server supervises and only falls back to this.
*
* @param int $streamID
* @param int $restart