Files
XC_VM/tests/Unit/StreamProcessLogoTest.php
T
Divarion_D 0f773b9758 refactor(streaming): extract map/scodec/source/HLS/FLV helpers with tests
Continue breaking up createChannelItem, startMovie and startStream by pulling
five self-contained blocks into pure, tested helpers:

- resolveOutputMap()          — VOD -map selection (custom > strip-subs > copy-all)
- subtitleCodecForContainer() — mp4->mov_text, mkv->srt, else copy
- resolveChannelSource()      — channel source string -> [serverId, path], rewriting
                                s:<server>:<path> to the remote getFile API URL
- buildHlsMpegtsOutput()      — HLS/mpegts segmenter output args
- buildFlvOutput()            — shared FLV output wrapper (RTMP relay + external
                                push, previously two identical lines)

The startStream extractions pass the conditional $rOptions/$rFLVOptions in as
parameters, so the control flow is unchanged. Behaviour preserved.

StreamProcessMovieOutputTest covers map precedence, container->codec mapping,
every source-resolution branch, HLS output assembly, and FLV wrapping.

Verified: php -l clean, make gates green, phpunit 360/360 green.
2026-08-07 19:31:18 +03:00

110 lines
4.2 KiB
PHP

<?php
use PHPUnit\Framework\TestCase;
use XcVm\Domain\Stream\StreamProcess;
/**
* Unit tests for the shared ffmpeg-command helpers extracted from
* createChannelItem/startMovie/startStream: buildLogoFilterOptions() and
* applyDefaultCopyCodecs(). Locks in their behaviour so the deduplication
* cannot silently drift.
*/
final class StreamProcessLogoTest extends TestCase {
/** @param array<mixed> $attrs (by ref — attr 16 is consumed on success) */
private function buildLogo(array &$attrs, bool $loopback): string {
$m = new ReflectionMethod(StreamProcess::class, 'buildLogoFilterOptions');
$m->setAccessible(true);
return $m->invokeArgs(null, [&$attrs, $loopback]);
}
/** @param array<mixed> $attrs (by ref) */
private function copyCodecs(array &$attrs): void {
$m = new ReflectionMethod(StreamProcess::class, 'applyDefaultCopyCodecs');
$m->setAccessible(true);
$m->invokeArgs(null, [&$attrs]);
}
// ── buildLogoFilterOptions ─────────────────────────────────
public function testNoLogoReturnsEmptyAndKeepsAttributes(): void {
$attrs = ['-acodec' => 'copy'];
$this->assertSame('', $this->buildLogo($attrs, false));
$this->assertSame(['-acodec' => 'copy'], $attrs);
}
public function testLoopbackNeverOverlaysLogo(): void {
$attrs = [16 => ['val' => '/logo.png']];
$this->assertSame('', $this->buildLogo($attrs, true));
$this->assertArrayHasKey(16, $attrs, 'attr 16 not consumed when skipped');
}
public function testBasicLogoBuildsFilterAndConsumesAttr16(): void {
$attrs = [16 => ['val' => '/logo.png'], 'keep' => 1];
$out = $this->buildLogo($attrs, false);
$this->assertStringContainsString("-i '/logo.png'", $out);
$this->assertStringContainsString('[1:v]scale=250:-1[logo]', $out);
$this->assertStringContainsString('[0:v][logo]overlay=10:main_h-overlay_h-10', $out);
// attr 16 consumed so parseTranscode won't re-emit it as a plain flag.
$this->assertArrayNotHasKey(16, $attrs);
$this->assertArrayHasKey('keep', $attrs);
}
public function testCustomPositionUsed(): void {
$attrs = [16 => ['val' => '/l.png', 'pos' => '25:40']];
$out = $this->buildLogo($attrs, false);
$this->assertStringContainsString('overlay=25:40', $out);
}
public function testDefaultPositionSentinelFallsBack(): void {
// '10:10' is the UI "unset" sentinel → falls back to the default anchor.
$attrs = [16 => ['val' => '/l.png', 'pos' => '10:10']];
$out = $this->buildLogo($attrs, false);
$this->assertStringContainsString('overlay=10:main_h-overlay_h-10', $out);
}
public function testDeinterlaceAndScaleChain(): void {
$attrs = [
16 => ['val' => '/l.png'],
17 => [], // deinterlace flag present
9 => ['val' => '1280:720'], // scale
];
$out = $this->buildLogo($attrs, false);
$this->assertStringContainsString('[0:v]yadif,scale=1280:720[bg]', $out);
$this->assertStringContainsString('[bg][logo]overlay=', $out);
}
public function testEmptyScaleValueSkipsPreFilterChain(): void {
$attrs = [16 => ['val' => '/l.png'], 9 => ['val' => '']];
$out = $this->buildLogo($attrs, false);
$this->assertStringNotContainsString('[bg]', $out);
$this->assertStringContainsString('[0:v][logo]overlay=', $out);
}
// ── applyDefaultCopyCodecs ─────────────────────────────────
public function testCopyCodecsDefaultWhenAbsent(): void {
$attrs = [];
$this->copyCodecs($attrs);
$this->assertSame('copy', $attrs['-acodec']);
$this->assertSame('copy', $attrs['-vcodec']);
}
public function testCopyCodecsPreserveExisting(): void {
$attrs = ['-acodec' => 'aac', '-vcodec' => 'libx264'];
$this->copyCodecs($attrs);
$this->assertSame('aac', $attrs['-acodec']);
$this->assertSame('libx264', $attrs['-vcodec']);
}
// ── resolveGpuInputCodec (pure path only — the GPU path probes ffprobe) ──
public function testGpuInputCodecEmptyWhenNoGpu(): void {
$m = new ReflectionMethod(StreamProcess::class, 'resolveGpuInputCodec');
$m->setAccessible(true);
// No GPU options → returns '' without ever probing the source.
$this->assertSame('', $m->invoke(null, '', '/some/source.ts'));
}
}