mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-25 20:01:56 +02:00
Bring non-proxy live streams into the daemon (ADR 0003, Phase A). Before launching a standard live stream's ffmpeg, StreamProcess registers a daemon ingest (FanoutClient::registerIngest → PUT /ingest/<id>, returns the socket) and, when the daemon accepted it, builds the HLS output through the `tee` muxer so one ffmpeg feeds BOTH the existing on-disk HLS and the daemon: -map … -f tee "[f=hls:…]<id>_.m3u8|[f=mpegts:onfail=ignore]unix:<ingest.sock>" - onfail=ignore keeps the on-disk HLS alive if the daemon output drops (verified: ffmpeg "continuing with 1/2 slaves"), so a daemon crash never takes the stream down. A plain second output would abort ffmpeg entirely. - Daemon unreachable ⇒ registerIngest returns null ⇒ the original on-disk-only HLS output runs, unchanged (the reachability rollback). Loopback/delay keep the legacy output. - Two tee-muxer gotchas handled: tee needs an EXPLICIT -map (plain outputs use automatic selection; tee errors "does not contain any stream") — reuse the stream's map or -map 0 -copy_unknown; and `individual_header_trailer` is a segment-muxer option the hls muxer ignores in flag form but rejects fatally inside a tee slave, so it is omitted. - stopStream unregisters the daemon ingest; FanoutClient::call() now checks the socket exists so unregister is a no-op when the daemon isn't deployed. Box-validated (real panel + ffmpeg, stream 569): one ffmpeg feeds on-disk HLS (5 segs, m3u8) AND the daemon (/live TS + /hls in-RAM), no errors; panel playback via the legacy path unaffected (1.69 MB valid TS). PHPStan clean, 441 unit tests pass. Refs ADR 0003 (Phase A).
89 lines
3.2 KiB
PHP
89 lines
3.2 KiB
PHP
<?php
|
|
|
|
use XcVm\Streaming\Fanout\FanoutClient;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* @covers \XcVm\Streaming\Fanout\FanoutClient
|
|
*
|
|
* Exercises the pure source-config builder that feeds the xc_fanout daemon's
|
|
* control API (ADR 0002, P2). buildSource() mirrors ProxyCommand's argument
|
|
* extraction; the output shape must match the daemon's streamConfig JSON
|
|
* (urls/ua/proxy/cookie/ffmpeg).
|
|
*/
|
|
final class FanoutClientTest extends TestCase {
|
|
|
|
public function testBuildSourceDecodesStreamSourceArray() {
|
|
$row = ['stream_source' => json_encode(['http://a/1.ts', 'http://b/2.m3u8'])];
|
|
$src = FanoutClient::buildSource($row, []);
|
|
|
|
$this->assertSame(['http://a/1.ts', 'http://b/2.m3u8'], $src['urls']);
|
|
$this->assertArrayHasKey('ua', $src);
|
|
$this->assertArrayHasKey('proxy', $src);
|
|
$this->assertArrayHasKey('cookie', $src);
|
|
$this->assertArrayHasKey('ffmpeg', $src);
|
|
}
|
|
|
|
public function testBuildSourceTrimsAndDropsEmptyUrls() {
|
|
$row = ['stream_source' => json_encode([' http://a/1.ts ', '', ' '])];
|
|
$src = FanoutClient::buildSource($row, []);
|
|
|
|
$this->assertSame(['http://a/1.ts'], $src['urls']);
|
|
}
|
|
|
|
public function testBuildSourceDefaultsUserAgentWhenArgumentMissing() {
|
|
$row = ['stream_source' => json_encode(['http://a/1.ts'])];
|
|
$src = FanoutClient::buildSource($row, []);
|
|
|
|
$this->assertSame('Mozilla/5.0', $src['ua']);
|
|
$this->assertSame('', $src['proxy']);
|
|
$this->assertSame('', $src['cookie']);
|
|
}
|
|
|
|
public function testBuildSourceFallsBackToArgumentDefaultUserAgent() {
|
|
$row = ['stream_source' => json_encode(['http://a/1.ts'])];
|
|
$args = ['user_agent' => ['value' => '', 'argument_default_value' => 'VLC/3.0']];
|
|
$src = FanoutClient::buildSource($row, $args);
|
|
|
|
$this->assertSame('VLC/3.0', $src['ua']);
|
|
}
|
|
|
|
public function testBuildSourcePrefersExplicitUserAgentValue() {
|
|
$row = ['stream_source' => json_encode(['http://a/1.ts'])];
|
|
$args = ['user_agent' => ['value' => 'MyPlayer/1.0', 'argument_default_value' => 'VLC/3.0']];
|
|
$src = FanoutClient::buildSource($row, $args);
|
|
|
|
$this->assertSame('MyPlayer/1.0', $src['ua']);
|
|
}
|
|
|
|
public function testBuildSourceExtractsProxyAndCookie() {
|
|
$row = ['stream_source' => json_encode(['http://a/1.ts'])];
|
|
$args = [
|
|
'proxy' => ['value' => '1.2.3.4:8080'],
|
|
'cookie' => ['value' => 'sid=xyz'],
|
|
];
|
|
$src = FanoutClient::buildSource($row, $args);
|
|
|
|
$this->assertSame('1.2.3.4:8080', $src['proxy']);
|
|
$this->assertSame('sid=xyz', $src['cookie']);
|
|
}
|
|
|
|
public function testBuildSourceHandlesMissingOrInvalidStreamSource() {
|
|
$this->assertSame([], FanoutClient::buildSource([], [])['urls']);
|
|
$this->assertSame([], FanoutClient::buildSource(['stream_source' => ''], [])['urls']);
|
|
$this->assertSame([], FanoutClient::buildSource(['stream_source' => 'not-json'], [])['urls']);
|
|
}
|
|
|
|
public function testRegisterRejectsEmptyUrlsWithoutTouchingDaemon() {
|
|
// No daemon is running in tests; empty urls must short-circuit to false
|
|
// before any socket call is attempted.
|
|
$this->assertFalse(FanoutClient::register(42, ['urls' => []]));
|
|
}
|
|
|
|
public function testRegisterIngestReturnsNullWithoutDaemon() {
|
|
// FANOUT_CTL_SOCK is not defined in the unit env, so registerIngest must
|
|
// fail closed (null) — the caller then launches ffmpeg legacy-only.
|
|
$this->assertNull(FanoutClient::registerIngest(42));
|
|
}
|
|
}
|