From f99048b79f8473448e2f4342d7625e24b7ebb65f Mon Sep 17 00:00:00 2001 From: Divarion_D Date: Fri, 7 Aug 2026 20:43:37 +0300 Subject: [PATCH] refactor(streaming): extract resolveStreamCodecMeta from startStream The codec-metadata derivation persisted for a started live stream (player compatibility, audio/video codec names, resolution snapped to the nearest standard height) was an inline block reading ffprobe output. Pull it into a pure resolveStreamCodecMeta() helper taking the ffprobe array and the player_allow_hevc flag and returning [compatible, audio, video, resolution]. The only external input, SettingsManager::getAll()['player_allow_hevc'], is now passed in (getAll() just returns the cached settings array, so hoisting the read out of the guard is free). Add unit tests: non-array/missing-codecs defaults, compatible h264/aac, resolution snapping, HEVC gating by the allow flag, and incompatible-but-reported codecs. Verified: phpstan level 5 green, phpunit 379/379, make gates green. --- src/Domain/Stream/StreamProcess.php | 41 +++++++++++------ tests/Unit/StreamProcessMovieOutputTest.php | 50 +++++++++++++++++++++ 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/Domain/Stream/StreamProcess.php b/src/Domain/Stream/StreamProcess.php index 65c7aa856..9ddf495c4 100644 --- a/src/Domain/Stream/StreamProcess.php +++ b/src/Domain/Stream/StreamProcess.php @@ -468,6 +468,33 @@ class StreamProcess { return $rArguments; } + /** + * Derive the codec metadata persisted for a started live stream from its + * ffprobe output: player compatibility, audio/video codec names and the + * resolution snapped to the nearest standard height. Extracted from startStream. + * + * @param mixed $rFFProbeOutput ffprobe result (array with a 'codecs' entry) or anything else. + * @param mixed $rAllowHevc player_allow_hevc setting, passed to the compatibility check. + * @return array{0:int,1:?string,2:?string,3:mixed} [compatible, audioCodec, videoCodec, resolution] + */ + private static function resolveStreamCodecMeta($rFFProbeOutput, $rAllowHevc) { + $rCompatible = 0; + $rAudioCodec = $rVideoCodec = $rResolution = null; + + if (is_array($rFFProbeOutput) && isset($rFFProbeOutput['codecs']) && is_array($rFFProbeOutput['codecs'])) { + $rCompatible = intval(DiagnosticsService::checkCompatibility($rFFProbeOutput, $rAllowHevc)); + $rAudioCodec = isset($rFFProbeOutput['codecs']['audio']['codec_name']) ? $rFFProbeOutput['codecs']['audio']['codec_name'] : null; + $rVideoCodec = isset($rFFProbeOutput['codecs']['video']['codec_name']) ? $rFFProbeOutput['codecs']['video']['codec_name'] : null; + $rResolution = isset($rFFProbeOutput['codecs']['video']['height']) ? $rFFProbeOutput['codecs']['video']['height'] : null; + + if ($rResolution) { + $rResolution = StreamSorter::getNearest(array(240, 360, 480, 576, 720, 1080, 1440, 2160), $rResolution); + } + } + + return array($rCompatible, $rAudioCodec, $rVideoCodec, $rResolution); + } + public static function createChannelItem($rStreamID, $rSource) { global $rSettings, $rServers, $rFFMPEG_CPU, $rFFMPEG_GPU; $db = self::db(); @@ -1290,19 +1317,7 @@ class StreamProcess { $rFFProbeOutput = array(); } - $rCompatible = 0; - $rAudioCodec = $rVideoCodec = $rResolution = null; - - if (isset($rFFProbeOutput) && is_array($rFFProbeOutput) && isset($rFFProbeOutput['codecs']) && is_array($rFFProbeOutput['codecs'])) { - $rCompatible = intval(DiagnosticsService::checkCompatibility($rFFProbeOutput, SettingsManager::getAll()['player_allow_hevc'])); - $rAudioCodec = isset($rFFProbeOutput['codecs']['audio']['codec_name']) ? $rFFProbeOutput['codecs']['audio']['codec_name'] : null; - $rVideoCodec = isset($rFFProbeOutput['codecs']['video']['codec_name']) ? $rFFProbeOutput['codecs']['video']['codec_name'] : null; - $rResolution = isset($rFFProbeOutput['codecs']['video']['height']) ? $rFFProbeOutput['codecs']['video']['height'] : null; - - if ($rResolution) { - $rResolution = StreamSorter::getNearest(array(240, 360, 480, 576, 720, 1080, 1440, 2160), $rResolution); - } - } + list($rCompatible, $rAudioCodec, $rVideoCodec, $rResolution) = self::resolveStreamCodecMeta($rFFProbeOutput, SettingsManager::getAll()['player_allow_hevc']); $rFFProbeOutputSafe = isset($rFFProbeOutput) && is_array($rFFProbeOutput) ? $rFFProbeOutput : []; $db->query('UPDATE `streams_servers` SET `delay_available_at` = ?,`to_analyze` = 0,`stream_started` = ?,`stream_info` = ?,`audio_codec` = ?, `video_codec` = ?, `resolution` = ?,`compatible` = ?,`stream_status` = 2,`pid` = ?,`progress_info` = ?,`current_source` = ? WHERE `stream_id` = ? AND `server_id` = ?', $rDelayStartAt, time(), json_encode($rFFProbeOutputSafe), $rAudioCodec, $rVideoCodec, $rResolution, $rCompatible, $rPID, json_encode(array()), $rSource, $rStreamID, SERVER_ID); diff --git a/tests/Unit/StreamProcessMovieOutputTest.php b/tests/Unit/StreamProcessMovieOutputTest.php index 4f85f0c04..720d37724 100644 --- a/tests/Unit/StreamProcessMovieOutputTest.php +++ b/tests/Unit/StreamProcessMovieOutputTest.php @@ -228,4 +228,54 @@ final class StreamProcessMovieOutputTest extends TestCase { $this->assertSame('headers', $out[0]['argument_key']); $this->assertSame('X:1', $out[0]['value']); } + + // ── resolveStreamCodecMeta (startStream codec metadata) ──── + + public function testCodecMetaNonArrayReturnsDefaults(): void { + $this->assertSame([0, null, null, null], $this->call('resolveStreamCodecMeta', null, false)); + $this->assertSame([0, null, null, null], $this->call('resolveStreamCodecMeta', 'not-json', false)); + } + + public function testCodecMetaWithoutCodecsKey(): void { + $this->assertSame([0, null, null, null], $this->call('resolveStreamCodecMeta', ['container' => 'mpegts'], false)); + } + + public function testCodecMetaCompatibleH264Aac(): void { + $probe = ['codecs' => [ + 'video' => ['codec_name' => 'h264', 'height' => 1080], + 'audio' => ['codec_name' => 'aac'], + ]]; + [$compat, $audio, $video, $res] = $this->call('resolveStreamCodecMeta', $probe, false); + $this->assertSame(1, $compat); + $this->assertSame('aac', $audio); + $this->assertSame('h264', $video); + $this->assertSame(1080, $res); + } + + public function testCodecMetaResolutionSnapsToNearest(): void { + $probe = ['codecs' => ['video' => ['codec_name' => 'h264', 'height' => 700]]]; + [, , , $res] = $this->call('resolveStreamCodecMeta', $probe, false); + $this->assertSame(720, $res); // 700 -> nearest of 240..2160 + } + + public function testCodecMetaHevcGatedByAllowFlag(): void { + $probe = ['codecs' => [ + 'video' => ['codec_name' => 'hevc', 'height' => 2160], + 'audio' => ['codec_name' => 'ac3'], + ]]; + $this->assertSame(0, $this->call('resolveStreamCodecMeta', $probe, false)[0]); // hevc not allowed + $this->assertSame(1, $this->call('resolveStreamCodecMeta', $probe, true)[0]); // hevc allowed + } + + public function testCodecMetaIncompatibleStillReportsCodecs(): void { + $probe = ['codecs' => [ + 'video' => ['codec_name' => 'mpeg2video', 'height' => 576], + 'audio' => ['codec_name' => 'aac'], + ]]; + [$compat, $audio, $video, $res] = $this->call('resolveStreamCodecMeta', $probe, false); + $this->assertSame(0, $compat); // mpeg2video not whitelisted + $this->assertSame('mpeg2video', $video); + $this->assertSame('aac', $audio); + $this->assertSame(576, $res); + } }