diff --git a/src/Domain/Stream/StreamProcess.php b/src/Domain/Stream/StreamProcess.php index 65c7aa85..9ddf495c 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 4f85f0c0..720d3772 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); + } }