feat(streams): say which producer runs a stream, and never hand remux to an old daemon

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
This commit is contained in:
root
2026-09-11 10:30:07 +00:00
co-authored by Claude Opus 5
parent 2c0e765a33
commit 269bee0148
4 changed files with 136 additions and 25 deletions
+13 -5
View File
@@ -6,7 +6,7 @@ use XcVm\Domain\Stream\StreamProcess;
/**
* The pure parts of handing a live stream to the xc_fanout supervisor: the
* native remuxer command (buildNativeLive), which streams may use it
* (isNativeEligible / isNativeSource), the policy and health the PHP monitor
* (nativeRefusal / isNativeSource), the policy and health the PHP monitor
* obeyed (supervisorPolicy / supervisorHealth), and what a supervisor state
* means for streams_servers (supervisedRowUpdate).
*/
@@ -111,7 +111,7 @@ final class StreamProcessSupervisionTest extends TestCase {
}
public function testPlainCopyStreamIsNativeEligible(): void {
$this->assertTrue(self::call('isNativeEligible', $this->plainStream(), []));
$this->assertNull(self::call('nativeRefusal', $this->plainStream(), []));
}
public function testAnythingNeedingFfmpegIsNotNativeEligible(): void {
@@ -126,10 +126,18 @@ final class StreamProcessSupervisionTest extends TestCase {
'radio' => ['type_key' => 'radio_streams'],
'created channel' => ['type_key' => 'created_live'],
] as $rWhy => $rOverride) {
$this->assertFalse(self::call('isNativeEligible', $this->plainStream($rOverride), []), $rWhy);
// The refusal is written to the stream's log, so every one of them
// must say something an operator can act on.
$this->assertIsString(self::call('nativeRefusal', $this->plainStream($rOverride), []), $rWhy);
}
$this->assertTrue(self::call('isNativeEligible', $this->plainStream(['external_push' => json_encode([7 => ['rtmp://push/x']])]), []), "another server's push is not this one's");
$this->assertFalse(self::call('isNativeEligible', $this->plainStream(), ['force_input_acodec' => ['value' => 'ac3']]), 'forced input codec');
$this->assertNull(self::call('nativeRefusal', $this->plainStream(['external_push' => json_encode([7 => ['rtmp://push/x']])]), []), "another server's push is not this one's");
$this->assertIsString(self::call('nativeRefusal', $this->plainStream(), ['force_input_acodec' => ['value' => 'ac3']]), 'forced input codec');
}
public function testRemuxCommandIsRecognisedForTheCommandRecord(): void {
$rCmd = $this->native(['binary' => \XcVm\Streaming\Fanout\FanoutClient::binaryPath()]);
$this->assertTrue(self::call('isRemuxCommand', $rCmd), 'the remuxer command is recorded as <id>_.fanout');
$this->assertFalse(self::call('isRemuxCommand', '/bin/ffmpeg40 -i x -c copy -f tee y'), 'an ffmpeg command is not');
}
public function testNativeSources(): void {