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
95 lines
4.2 KiB
PHP
95 lines
4.2 KiB
PHP
<?php
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
use XcVm\Streaming\Delivery\SegmentReader;
|
|
|
|
/**
|
|
* Unit tests for SegmentReader::selectSegments() — the duration-based prebuffer
|
|
* selection. Guards the fix where an on-demand stream's short (2s) fast-start
|
|
* segments used to yield only intval(prebuffer / seg_time) = 1 segment (~2s of
|
|
* buffer) instead of prebuffer seconds' worth.
|
|
*/
|
|
final class SegmentReaderTest extends TestCase {
|
|
|
|
/** @param array<int,array{0:int,1:float}> $segs [num, duration] pairs */
|
|
private function playlist(array $segs, bool $withExtinf = true): string {
|
|
$out = "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:10\n#EXT-X-MEDIA-SEQUENCE:{$segs[0][0]}\n";
|
|
foreach ($segs as [$num, $dur]) {
|
|
if ($withExtinf) {
|
|
$out .= sprintf("#EXTINF:%.6f,\n", $dur);
|
|
}
|
|
$out .= "564_{$num}.ts\n";
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
// ── the fix: short on-demand segments ──────────────────────
|
|
|
|
public function testShortStartupSegmentsGiveFullSecondsNotOneSegment(): void {
|
|
// 5x 2s segments; prebuffer 10s. Old code: intval(10/10)=1 -> 1 seg (2s).
|
|
$pl = $this->playlist([[1, 2.0], [2, 2.0], [3, 2.0], [4, 2.0], [5, 2.0]]);
|
|
$out = SegmentReader::selectSegments($pl, 10, 10);
|
|
$this->assertSame(['564_1.ts', '564_2.ts', '564_3.ts', '564_4.ts', '564_5.ts'], $out);
|
|
}
|
|
|
|
public function testPrebufferCappedByAvailableSegments(): void {
|
|
// Want 100s but only 10s exists -> return everything, no error.
|
|
$pl = $this->playlist([[1, 2.0], [2, 2.0], [3, 2.0], [4, 2.0], [5, 2.0]]);
|
|
$this->assertCount(5, SegmentReader::selectSegments($pl, 100, 10));
|
|
}
|
|
|
|
// ── normal 10s segments ────────────────────────────────────
|
|
|
|
public function testTenSecondSegmentsCountByDuration(): void {
|
|
$pl = $this->playlist([[1, 10.0], [2, 10.0], [3, 10.0], [4, 10.0], [5, 10.0], [6, 10.0]]);
|
|
$this->assertSame(['564_6.ts'], SegmentReader::selectSegments($pl, 10, 10));
|
|
$this->assertSame(['564_4.ts', '564_5.ts', '564_6.ts'], SegmentReader::selectSegments($pl, 25, 10));
|
|
}
|
|
|
|
public function testMixedRampDurations(): void {
|
|
// 567-style ramp: 2,2,2,2,8,10. Want >=15s from the end: 10+8=18 -> 2 segs.
|
|
$pl = $this->playlist([[1, 2.0], [2, 2.0], [3, 2.0], [4, 2.0], [5, 8.0], [6, 10.0]]);
|
|
$this->assertSame(['564_5.ts', '564_6.ts'], SegmentReader::selectSegments($pl, 15, 10));
|
|
}
|
|
|
|
// ── fallbacks / other modes ────────────────────────────────
|
|
|
|
public function testCountFallbackWhenNoExtinf(): void {
|
|
$pl = $this->playlist([[1, 0], [2, 0], [3, 0]], false); // bare .ts lines
|
|
$this->assertSame(['564_3.ts'], SegmentReader::selectSegments($pl, 10, 10)); // intval(10/10)=1
|
|
$this->assertSame(['564_1.ts', '564_2.ts', '564_3.ts'], SegmentReader::selectSegments($pl, 30, 10));
|
|
}
|
|
|
|
public function testMinusOneReturnsAll(): void {
|
|
$pl = $this->playlist([[1, 10.0], [2, 10.0]]);
|
|
$this->assertSame(['564_1.ts', '564_2.ts'], SegmentReader::selectSegments($pl, -1, 10));
|
|
}
|
|
|
|
public function testZeroReturnsLastSegmentIndex(): void {
|
|
$pl = $this->playlist([[1, 10.0], [2, 10.0], [7, 10.0]]);
|
|
$this->assertSame('7', SegmentReader::selectSegments($pl, 0, 10));
|
|
}
|
|
|
|
public function testNoSegmentsReturnsNull(): void {
|
|
$this->assertNull(SegmentReader::selectSegments("#EXTM3U\n#EXT-X-VERSION:3\n", 10, 10));
|
|
}
|
|
|
|
// ── playlistBufferedSeconds (on-demand prebuffer gate) ─────
|
|
|
|
public function testPlaylistBufferedSecondsSumsExtinf(): void {
|
|
// 567-style ramp: 2+2+8+10 = 22s buffered.
|
|
$tmp = tempnam(sys_get_temp_dir(), 'xcvmpl');
|
|
file_put_contents($tmp, $this->playlist([[1, 2.0], [2, 2.0], [3, 8.0], [4, 10.0]]));
|
|
$this->assertEqualsWithDelta(22.0, SegmentReader::playlistBufferedSeconds($tmp), 0.001);
|
|
unlink($tmp);
|
|
}
|
|
|
|
public function testPlaylistBufferedSecondsMissingOrNoExtinfIsZero(): void {
|
|
$this->assertSame(0.0, SegmentReader::playlistBufferedSeconds('/nonexistent/xcvm-none.m3u8'));
|
|
$tmp = tempnam(sys_get_temp_dir(), 'xcvmpl');
|
|
file_put_contents($tmp, $this->playlist([[1, 0], [2, 0]], false)); // bare .ts, no #EXTINF
|
|
$this->assertSame(0.0, SegmentReader::playlistBufferedSeconds($tmp));
|
|
unlink($tmp);
|
|
}
|
|
}
|