mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-25 11:03:14 +01:00
With fanout switched off (FanoutMode::legacyDelivery(), which is also true when the licence denies fanout), live delivery goes back to the paths that ADR 0003 Phase E removed. Each is restored from git history: - HLS (E3): live.php serves HLSGenerator::generateHLS() over the on-disk playlist. segment.php serves on-disk LIVE segments again through X-Accel /xc_hls/, AES-128-encrypting on first read, and burns a pending send-message signal with SignalSender. generateHLS now mints tokens with Encryption::mintToken (so secure_stream_tokens applies), rewrites one segment line at a time (a str_replace of 1_1.ts also hit 11_1.ts), and tokenizes an fMP4 init map. - TS (E2): live.php chase-reads the on-disk segments again (SegmentReader and its test restored). $rSegmentSettings, which the old loop read but never defined, is now set there. A signalled segment is no longer sent twice. - Proxy streams (E1): StreamProcess::startProxy() starts ProxyCommand (XC_VMProxy[<id>]), and live.php relays its datagrams from the viewer's unix socket. Fixed while restoring: - the read loop ended after its first read (a stray break); - it spun on an idle source; - it never exited when the last viewer left; - it passed the proxy argument's whole row to ffmpeg; - it ran ffmpeg with no input when no source opened. Runtime state goes through StreamStateWriter. With fanout on, nothing changes: a daemon that is merely down still gives not-on-air until the keepalive restarts it, and segment.php still 404s non-daemon live segments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Es8UhyPS8MNjuYTagqfGWi
93 lines
4.0 KiB
PHP
93 lines
4.0 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use XcVm\Core\Util\Encryption;
|
|
use XcVm\Streaming\Delivery\HLSGenerator;
|
|
|
|
/**
|
|
* HLSGenerator::generateHLS — the pre-fanout client playlist, built from the
|
|
* on-disk playlist while fanout is switched off.
|
|
*/
|
|
class HLSGeneratorLegacyTest extends TestCase {
|
|
private string $rDir;
|
|
private const PASS = 'test-live-pass';
|
|
|
|
protected function setUp(): void {
|
|
if (!defined('SERVER_ID')) {
|
|
define('SERVER_ID', 1);
|
|
}
|
|
if (!defined('STREAMS_PATH')) {
|
|
define('STREAMS_PATH', sys_get_temp_dir() . '/xcvm-hls-legacy-test/');
|
|
}
|
|
if (!is_dir(STREAMS_PATH)) {
|
|
mkdir(STREAMS_PATH, 0777, true);
|
|
}
|
|
$this->rDir = STREAMS_PATH;
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
@unlink($this->rDir . '9901_.m3u8');
|
|
@unlink($this->rDir . '9901_.iv');
|
|
}
|
|
|
|
private function settings(array $rOver = []): array {
|
|
return array_merge(['live_streaming_pass' => self::PASS, 'encrypt_hls' => 0, 'allow_cdn_access' => 0, 'secure_stream_tokens' => 0], $rOver);
|
|
}
|
|
|
|
private function playlist(string $rBody): string {
|
|
$rPath = $this->rDir . '9901_.m3u8';
|
|
file_put_contents($rPath, $rBody);
|
|
return $rPath;
|
|
}
|
|
|
|
private function build(array $rSettings, string $rPath) {
|
|
return HLSGenerator::generateHLS($rSettings, $rPath, 'user', 'pass', 9901, 'uuid-1', '10.0.0.1', null, '', 'h264', 0, 1, null);
|
|
}
|
|
|
|
public function testMissingPlaylistOrNoSegmentsIsFalse(): void {
|
|
$this->assertFalse($this->build($this->settings(), $this->rDir . 'nope_.m3u8'));
|
|
$this->assertFalse($this->build($this->settings(), $this->playlist("#EXTM3U\n#EXT-X-TARGETDURATION:10\n")));
|
|
}
|
|
|
|
public function testEverySegmentBecomesATokenThatSegmentPhpReads(): void {
|
|
$rOut = $this->build($this->settings(), $this->playlist("#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-MEDIA-SEQUENCE:10\n#EXTINF:10.0,\n9901_10.ts\n#EXTINF:10.0,\n9901_11.ts\n"));
|
|
$this->assertIsString($rOut);
|
|
$this->assertStringContainsString('#EXT-X-MEDIA-SEQUENCE:10', $rOut);
|
|
$this->assertStringNotContainsString('9901_10.ts', $rOut);
|
|
preg_match_all('#^/hls/(\S+)$#m', $rOut, $rM);
|
|
$this->assertCount(2, $rM[1]);
|
|
// The payload segment.php splits on '/': user/pass/ip/stream/segment/uuid/server/codec/on_demand.
|
|
$rParts = explode('/', (string) Encryption::readToken($rM[1][1], self::PASS, OPENSSL_EXTRA, true));
|
|
$this->assertSame(['user', 'pass', '10.0.0.1', '9901', '9901_11.ts', 'uuid-1', '1', 'h264', '0'], $rParts);
|
|
}
|
|
|
|
public function testOverlappingNamesAreRewrittenOneLineAtATime(): void {
|
|
// 9901_1.ts is a substring of 19901_1.ts-like names; each line must get its own token.
|
|
$rOut = $this->build($this->settings(), $this->playlist("#EXTM3U\n#EXTINF:2.0,\n9901_1.ts\n#EXTINF:2.0,\n9901_11.ts\n"));
|
|
preg_match_all('#^/hls/(\S+)$#m', $rOut, $rM);
|
|
$this->assertCount(2, $rM[1]);
|
|
$rSegs = array_map(fn($t) => explode('/', (string) Encryption::readToken($t, self::PASS, OPENSSL_EXTRA, true))[4], $rM[1]);
|
|
$this->assertSame(['9901_1.ts', '9901_11.ts'], $rSegs);
|
|
}
|
|
|
|
public function testCdnAccessKeepsTheNameAndAddsTheToken(): void {
|
|
$rOut = $this->build($this->settings(['allow_cdn_access' => 1]), $this->playlist("#EXTM3U\n#EXTINF:10.0,\n9901_5.ts\n"));
|
|
$this->assertMatchesRegularExpression('#^/hls/9901_5\.ts\?token=\S+$#m', $rOut);
|
|
}
|
|
|
|
public function testEncryptedHlsDeclaresTheKeyAfterTheHeader(): void {
|
|
file_put_contents($this->rDir . '9901_.iv', str_repeat("\x02", 16));
|
|
$rOut = $this->build($this->settings(['encrypt_hls' => 1]), $this->playlist("#EXTM3U\n#EXTINF:10.0,\n9901_5.ts\n"));
|
|
$rLines = explode("\n", $rOut);
|
|
$this->assertSame('#EXTM3U', $rLines[0]);
|
|
$this->assertStringStartsWith('#EXT-X-KEY:METHOD=AES-128,URI="/key/', $rLines[1]);
|
|
$this->assertStringEndsWith(',IV=0x' . str_repeat('02', 16), $rLines[1]);
|
|
}
|
|
|
|
public function testFmp4InitMapIsTokenized(): void {
|
|
$rOut = $this->build($this->settings(), $this->playlist("#EXTM3U\n#EXT-X-MAP:URI=\"9901_init.mp4\"\n#EXTINF:10.0,\n9901_5.m4s\n"));
|
|
$this->assertMatchesRegularExpression('#\#EXT-X-MAP:URI="/hls/[^"]+"#', $rOut);
|
|
$this->assertStringNotContainsString('9901_init.mp4', $rOut);
|
|
}
|
|
}
|