Files
XC_VM/tests/Unit/SegmentReaderTest.php
T
Divarion_D 1c9d837761 fix(streaming): make HLS prebuffer duration-based, not segment-count
An on-demand stream emits short (2s) fast-start segments before it ramps up to
seg_time. getPlaylistSegments picked intval(client_prebuffer / seg_time)
segments -- with the default 10s prebuffer / 10s nominal seg_time that is 1
segment, i.e. only ~2s of actual buffer at cold start, which players surfaced as
"caches 2 seconds". Persistent streams (segments already ~10s) were unaffected:
1 segment = 10s.

Select the newest segments whose #EXTINF durations sum to at least the requested
prebuffer seconds instead, so client_prebuffer means real seconds regardless of
segment length. Fast-start (hls_init_time 2) is kept. Falls back to the old
nominal count when the playlist has no #EXTINF lines; the -1 (all) and 0 (index)
modes are unchanged. Split into a pure selectSegments()/parseSegmentDurations()
so the selection is unit tested.

Verified: phpstan level 5 green, phpunit 405/405, make gates green.
2026-08-07 22:36:44 +03:00

77 lines
3.3 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));
}
}