Files
XC_VM/tests/Unit/HttpRangeTest.php
T
obscuremindandClaude Opus 5 40bcbdba48 fix(delivery): correct byte ranges for VOD and catch-up seeking
- Timeshift TS seeking returned the wrong bytes: the start file was
  estimated from the average file size, files before it were never skipped
  (an empty `if` where a `continue` belonged) and the in-file offset came
  out negative, so every catch-up seek streamed from the archive's first
  byte; the range end was ignored too. The served range is now mapped
  exactly onto the minute files (first file from its .offset).
- The timeshift throttle never reset its chunk counter (vod.php's copy
  did), so past vod_limit_perc every chunk paused a whole second.
- A shared HttpRange parser (RFC 7233 single ranges) replaces the inline
  copies: suffix ranges (bytes=-N) died on PHP 8 arithmetic, an
  unsatisfiable range answered with the resource's range instead of
  `bytes */size`, and Accept-Ranges said "0-<len>" instead of "bytes".
  VOD never reads past a bounded range's end.
- Direct-proxy VOD reads the source's headers with cURL: get_headers()
  goes through the https stream wrapper, which does not work under
  PHP-FPM here, and returned Content-Length as an array after a redirect.
  The upstream is asked for exactly the requested range, and curl's
  verbose output no longer goes to the FPM log on every request.
- The limiter spares the requesting connection by uuid (VOD/timeshift).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:19:50 +01:00

44 lines
1.9 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Streaming\Delivery\HttpRange;
/**
* @covers \XcVm\Streaming\Delivery\HttpRange
*
* The single-range parser the VOD and timeshift endpoints share. The inline
* copies it replaced broke on suffix ranges (`bytes=-N`) and answered an
* unsatisfiable range with the full resource's Content-Range.
*/
final class HttpRangeTest extends TestCase {
public function testNoRangeServesTheWholeResource(): void {
$this->assertNull(HttpRange::parse(null, 1000));
$this->assertNull(HttpRange::parse('', 1000));
$this->assertNull(HttpRange::parse('items=0-9', 1000), 'another unit is ignored');
}
public function testBoundedAndOpenRanges(): void {
$this->assertSame([0, 99], HttpRange::parse('bytes=0-99', 1000));
$this->assertSame([0, 1], HttpRange::parse('bytes=0-1', 1000), 'a player probing the first bytes');
$this->assertSame([500, 999], HttpRange::parse('bytes=500-', 1000));
$this->assertSame([10, 999], HttpRange::parse('bytes=10-5000', 1000), 'end clamped to the size');
$this->assertSame([10, 20], HttpRange::parse(' Bytes = 10 - 20 ', 1000));
}
public function testSuffixRangeIsTheLastBytes(): void {
$this->assertSame([900, 999], HttpRange::parse('bytes=-100', 1000));
$this->assertSame([0, 999], HttpRange::parse('bytes=-5000', 1000), 'longer than the resource: all of it');
}
public function testUnsatisfiableRanges(): void {
$this->assertFalse(HttpRange::parse('bytes=1000-', 1000), 'starts past the end');
$this->assertFalse(HttpRange::parse('bytes=5-2', 1000), 'end before start');
$this->assertFalse(HttpRange::parse('bytes=-0', 1000), 'empty suffix');
$this->assertFalse(HttpRange::parse('bytes=0-1,5-6', 1000), 'multipart is not served');
$this->assertFalse(HttpRange::parse('bytes=abc', 1000));
$this->assertFalse(HttpRange::parse('bytes=-', 1000));
$this->assertFalse(HttpRange::parse('bytes=0-9', 0), 'nothing to serve');
}
}