mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-29 20:02:04 +02:00
refactor(stream): flip startStream to buildLive(), remove inline assembly + shadow
Completes the in-progress FFmpeg-command migration. startStream's ~180-line
inline command assembly (and the shadow buildLive() diff-check that ran beside
it) are gone; the command is now built solely by the pure buildLive(), fed the
raw stream row. The delay-playlist bookkeeping (segment start + sleep window)
is hoisted just above the call since buildLive() consumes segmentStart /
delayActive. Net -171 lines.
buildLive() already did all transcode-attribute resolution and {TEMPLATE}
substitution internally, so feeding it the raw row (instead of the
post-mutation row the shadow used) removes a latent double-mutation in the
custom-transcode branch (transcode_profile_id == -1): json_decode((string)
$array) on an already-decoded value would have yielded [] there. Only the
inline path ever ran, so this was never a live bug — but it made the shadow an
invalid equivalence probe for that branch, which is why the flip is verified
directly rather than via the shadow log.
Equivalence verified two ways:
- Structural: the 129 command-building statements of buildLive() are
line-for-line identical to the inline block (the sole non-command delta was a
dead `$rFFPROBE = FFPROBE_BIN_40` assignment that never entered the string).
- Empirical: on a live box, the generated _.ffmpeg is byte-identical
(md5-equal) before vs after the flip for BOTH a plain stream and a
custom-transcode stream (enable_transcode=1, profile_id=-1, custom attrs),
and the flipped stream serves (m3u8 200/#EXTM3U, ts 200 ~2.8MB).
Refs: #148
Verified: php -l, make phpstan (No errors), make gates.
This commit is contained in:
@@ -1383,137 +1383,9 @@ class StreamProcess {
|
||||
}
|
||||
}
|
||||
|
||||
$externalPushJson = $rStream['stream_info']['external_push'] ?? '[]';
|
||||
$rExternalPush = json_decode($externalPushJson, true);
|
||||
// ffmpeg writes progress reports to this local file; StreamsCronJob tails it.
|
||||
// (PHP-FPM cannot read ffmpeg's open-ended chunked progress POST, so the HTTP
|
||||
// /progress endpoint only ever ran at stream end -> speed was stuck at "1x".)
|
||||
$rProgressFile = STREAMS_PATH . intval($rStreamID) . '_.progress';
|
||||
// LLOD input resilience: an on-demand HTTP source that drops the
|
||||
// connection makes ffmpeg exit ("Stream ends prematurely"), which the
|
||||
// watchdog then restarts — turning a brief upstream hiccup into a
|
||||
// multi-second re-probe gap and client re-buffering. Reconnecting keeps
|
||||
// ffmpeg alive across drops instead of dying. Guarded to HTTP(S): these
|
||||
// options are http-protocol-only and are a fatal "Option not found"
|
||||
// error on udp/rtmp/file inputs.
|
||||
$rLLODReconnect = ($rLLOD && !$rLoopback && is_string($rSource) && preg_match('#^https?://#i', $rSource))
|
||||
? '-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5 '
|
||||
: '';
|
||||
$rLLODInputFlags = ($rLLOD && !$rLoopback ? $rLLODReconnect . '-fflags +discardcorrupt ' : '');
|
||||
|
||||
// Command-template defaults: only the non-custom_ffmpeg branch below
|
||||
// assigns these, yet the {MAP}/{GEN_PTS}/{READ_NATIVE} substitution and
|
||||
// the delay sleep read them unconditionally. Default them so the
|
||||
// custom_ffmpeg path (which skips the branch) stays defined.
|
||||
$rMap = '';
|
||||
$rGenPTS = '';
|
||||
$rReadNative = '';
|
||||
$rSleepTime = 0;
|
||||
if (empty($rStream['stream_info']['custom_ffmpeg'])) {
|
||||
if ($rLoopback) {
|
||||
$rOptions = '{FETCH_OPTIONS}';
|
||||
} else {
|
||||
$rOptions = '{GPU} {FETCH_OPTIONS}';
|
||||
}
|
||||
|
||||
if ($rStream['stream_info']['stream_all'] == 1) {
|
||||
$rMap = '-map 0 -copy_unknown ';
|
||||
} else {
|
||||
if (!empty($rStream['stream_info']['custom_map'])) {
|
||||
$rMap = escapeshellcmd($rStream['stream_info']['custom_map']) . ' -copy_unknown ';
|
||||
} else {
|
||||
if ($rStream['stream_info']['type_key'] == 'radio_streams') {
|
||||
$rMap = '-map 0:a? ';
|
||||
} else {
|
||||
$rMap = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (($rStream['stream_info']['gen_timestamps'] == 1 || empty($rProtocol)) && $rStream['stream_info']['type_key'] != 'created_live') {
|
||||
$rGenPTS = '-fflags +genpts -async 1';
|
||||
} else {
|
||||
if (is_array($rFFProbeOutput) && isset($rFFProbeOutput['codecs']['audio']['codec_name']) && in_array($rFFProbeOutput['codecs']['audio']['codec_name'], array('ac3', 'eac3')) && $rSettings['dts_legacy_ffmpeg']) {
|
||||
$rFFMPEG_CPU = FFMPEG_BIN_40;
|
||||
$rFFPROBE = FFPROBE_BIN_40;
|
||||
}
|
||||
|
||||
$rNoFix = ($rFFMPEG_CPU == FFMPEG_BIN_40 ? '-nofix_dts' : '');
|
||||
$rGenPTS = $rNoFix . ' -start_at_zero -copyts -vsync 0 -correct_ts_overflow 0 -avoid_negative_ts disabled -max_interleave_delta 0';
|
||||
}
|
||||
|
||||
$container = (isset($rFFProbeOutput) && is_array($rFFProbeOutput)) ? ($rFFProbeOutput['container'] ?? null) : null;
|
||||
if (empty($rStream['server_info']['parent_id']) && (($rStream['stream_info']['read_native'] == 1) || ($container && stristr($container, 'hls') && $rSettings['read_native_hls']) || empty($rProtocol) || ($container && stristr($container, 'mp4')) || ($container && stristr($container, 'matroska')))) {
|
||||
$rReadNative = '-re';
|
||||
} else {
|
||||
$rReadNative = '';
|
||||
}
|
||||
|
||||
if (!$rStream['server_info']['parent_id'] && $rStream['stream_info']['enable_transcode'] == 1 && $rStream['stream_info']['type_key'] != 'created_live') {
|
||||
if ($rStream['stream_info']['transcode_profile_id'] == -1) {
|
||||
$rStream['stream_info']['transcode_attributes'] = array_merge(StreamUtils::getArguments($rStream['stream_arguments'], $rProtocol, 'transcode'), json_decode((string) $rStream['stream_info']['transcode_attributes'], true) ?: array());
|
||||
} else {
|
||||
$rStream['stream_info']['transcode_attributes'] = json_decode((string) $rStream['stream_info']['profile_options'], true) ?: array();
|
||||
}
|
||||
} else {
|
||||
$rStream['stream_info']['transcode_attributes'] = array();
|
||||
}
|
||||
|
||||
$rFFMPEG = ((isset($rStream['stream_info']['transcode_attributes']['gpu']) ? $rFFMPEG_GPU : $rFFMPEG_CPU)) . ' -y -nostdin -hide_banner -loglevel ' . (($rSettings['ffmpeg_warnings'] ? 'warning' : 'error')) . ' -err_detect ignore_err -thread_queue_size 1024 ' . $rOptions . ' {GEN_PTS} {READ_NATIVE} ' . $rLLODInputFlags . '-probesize ' . $rProbesize . ' -analyzeduration ' . $rAnalyseDuration . ' -progress "' . $rProgressFile . '" {CONCAT} -i {STREAM_SOURCE} {LOGO} -max_muxing_queue_size 1024 ';
|
||||
|
||||
self::applyDefaultCopyCodecs($rStream['stream_info']['transcode_attributes']);
|
||||
|
||||
if (!array_key_exists('-scodec', $rStream['stream_info']['transcode_attributes'])) {
|
||||
$rStream['stream_info']['transcode_attributes']['-sn'] = '';
|
||||
}
|
||||
} else {
|
||||
$rStream['stream_info']['transcode_attributes'] = array();
|
||||
$rFFMPEG = ((stripos($rStream['stream_info']['custom_ffmpeg'], 'nvenc') !== false ? $rFFMPEG_GPU : $rFFMPEG_CPU)) . ' -y -nostdin -hide_banner -loglevel ' . (($rSettings['ffmpeg_warnings'] ? 'warning' : 'error')) . ' -progress "' . $rProgressFile . '" ' . $rStream['stream_info']['custom_ffmpeg'];
|
||||
}
|
||||
|
||||
$rLLODOptions = ($rLLOD && !$rLoopback ? '-tune zerolatency -fflags nobuffer -flags low_delay -strict experimental -threads 0' : '');
|
||||
$rOutputs = array();
|
||||
|
||||
if ($rLoopback) {
|
||||
$rOptions = '{MAP}';
|
||||
$rFLVOptions = '{MAP}';
|
||||
$rMap = '-map 0 -copy_unknown ';
|
||||
} else {
|
||||
$rOptions = '{MAP} {LLOD}';
|
||||
$rFLVOptions = '{MAP} {AAC_FILTER}';
|
||||
}
|
||||
|
||||
$rKeyFrames = ($rSettings['ignore_keyframes'] ? '+split_by_time' : '');
|
||||
// Fast start: shorten the first segment so players can begin sooner.
|
||||
// Capped at seg_time so a small seg_time never produces a longer first segment.
|
||||
$rInitTime = min(2, intval($rSegmentSettings['seg_time']));
|
||||
$rOutputs['mpegts'][] = self::buildHlsMpegtsOutput($rOptions, $rSegmentSettings, $rKeyFrames, $rInitTime, $rStreamID);
|
||||
|
||||
if ($rStream['stream_info']['rtmp_output'] == 1) {
|
||||
$rOutputs['flv'][] = self::buildFlvOutput($rFLVOptions, 'rtmp://127.0.0.1:' . intval($rServers[$rStream['server_info']['server_id']]['rtmp_port']) . '/live/' . intval($rStreamID) . '?password=' . urlencode($rSettings['live_streaming_pass']));
|
||||
}
|
||||
|
||||
if (!empty($rExternalPush[SERVER_ID])) {
|
||||
foreach ($rExternalPush[SERVER_ID] as $rPushURL) {
|
||||
$rOutputs['flv'][] = self::buildFlvOutput($rFLVOptions, escapeshellarg($rPushURL));
|
||||
}
|
||||
}
|
||||
|
||||
$rLogoOptions = self::buildLogoFilterOptions($rStream['stream_info']['transcode_attributes'], $rLoopback);
|
||||
|
||||
$rGPUOptions = (isset($rStream['stream_info']['transcode_attributes']['gpu']) ? $rStream['stream_info']['transcode_attributes']['gpu']['cmd'] : '');
|
||||
$rInputCodec = '';
|
||||
|
||||
$supportedCodecs = ['h264', 'hevc', 'mjpeg', 'mpeg1', 'mpeg2', 'mpeg4', 'vc1', 'vp8', 'vp9'];
|
||||
$videoCodec = null;
|
||||
if (isset($rFFProbeOutput) && is_array($rFFProbeOutput)) {
|
||||
$videoCodec = $rFFProbeOutput['codecs']['video']['codec_name'] ?? null;
|
||||
}
|
||||
|
||||
if (!empty($rGPUOptions) && in_array($videoCodec, $supportedCodecs)) {
|
||||
$rInputCodec = '-c:v ' . $rFFProbeOutput['codecs']['video']['codec_name'] . '_cuvid';
|
||||
}
|
||||
|
||||
// Delay-mode playlist bookkeeping (segment start + sleep window). Must run
|
||||
// before buildLive(), which consumes segmentStart / delayActive.
|
||||
$rSleepTime = 0;
|
||||
$rSegmentStart = 0;
|
||||
$rDelayActive = !(0 >= $rStream['stream_info']['delay_minutes'] || $rStream['server_info']['parent_id']);
|
||||
if ($rDelayActive) {
|
||||
@@ -1539,63 +1411,20 @@ class StreamProcess {
|
||||
$rSleepTime = self::resolveDelaySleepTime($rStream['stream_info']['delay_minutes'], $rSegmentStart);
|
||||
}
|
||||
|
||||
if (!$rDelayActive) {
|
||||
foreach ($rOutputs as $rOutputCommands) {
|
||||
foreach ($rOutputCommands as $rOutputCommand) {
|
||||
if (isset($rStream['stream_info']['transcode_attributes']['gpu'])) {
|
||||
$rFFMPEG .= '-gpu ' . intval($rStream['stream_info']['transcode_attributes']['gpu']['device']) . ' ';
|
||||
}
|
||||
|
||||
$rFFMPEG .= implode(' ', StreamUtils::parseTranscode($rStream['stream_info']['transcode_attributes'])) . ' ';
|
||||
$rFFMPEG .= $rOutputCommand;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$rFFMPEG .= implode(' ', StreamUtils::parseTranscode($rStream['stream_info']['transcode_attributes'])) . ' ';
|
||||
$rFFMPEG .= '{MAP} -individual_header_trailer 0 -f hls -hls_time ' . intval($rSegmentSettings['seg_time']) . ' -hls_list_size ' . intval($rStream['stream_info']['delay_minutes']) * 6 . ' -hls_delete_threshold 4 -start_number ' . $rSegmentStart . ' -hls_flags delete_segments+discont_start+omit_endlist -hls_segment_type mpegts -hls_segment_filename "' . DELAY_PATH . intval($rStreamID) . '_%d.ts" "' . DELAY_PATH . intval($rStreamID) . '_.m3u8" ';
|
||||
}
|
||||
|
||||
$rFFMPEG .= ' >/dev/null 2>>' . STREAMS_PATH . intval($rStreamID) . '.errors & echo $! > ' . STREAMS_PATH . intval($rStreamID) . '_.pid';
|
||||
|
||||
$ffprobeContainer = (isset($rFFProbeOutput['container']) && is_string($rFFProbeOutput['container'])) ? $rFFProbeOutput['container'] : '';
|
||||
|
||||
$audioCodec = (isset($rFFProbeOutput['codecs']['audio']['codec_name']) && is_array($rFFProbeOutput['codecs']['audio'])) ? $rFFProbeOutput['codecs']['audio']['codec_name'] : '';
|
||||
|
||||
$rFFMPEG = str_replace(
|
||||
['{FETCH_OPTIONS}', '{GEN_PTS}', '{STREAM_SOURCE}', '{MAP}', '{READ_NATIVE}', '{CONCAT}', '{AAC_FILTER}', '{GPU}', '{INPUT_CODEC}', '{LOGO}', '{LLOD}'],
|
||||
[
|
||||
empty($rStream['stream_info']['custom_ffmpeg']) ? $rFetchOptions : '',
|
||||
empty($rStream['stream_info']['custom_ffmpeg']) ? $rGenPTS : '',
|
||||
escapeshellarg($rStreamSource),
|
||||
empty($rStream['stream_info']['custom_ffmpeg']) ? $rMap : '',
|
||||
empty($rStream['stream_info']['custom_ffmpeg']) ? $rReadNative : '',
|
||||
($rStream['stream_info']['type_key'] == 'created_live' && empty($rStream['server_info']['parent_id']) ? '-safe 0 -f concat' : ''),
|
||||
self::aacBitstreamFilter($ffprobeContainer, $audioCodec, $rStream['stream_info']['transcode_attributes']['-acodec'] ?? ''),
|
||||
$rGPUOptions,
|
||||
$rInputCodec,
|
||||
$rLogoOptions,
|
||||
$rLLODOptions
|
||||
],
|
||||
$rFFMPEG
|
||||
);
|
||||
|
||||
// SHADOW: compute the pure buildLive() output alongside the inline
|
||||
// assembly and log any divergence. The inline $rFFMPEG still runs —
|
||||
// no flip yet (see FFmpegCommand::buildLive migration).
|
||||
$rShadow = self::buildLive(array(
|
||||
'stream' => $rStream, 'settings' => $rSettings, 'servers' => $rServers,
|
||||
'streamID' => $rStreamID, 'streamSource' => $rStreamSource,
|
||||
'fetchOptions' => $rFetchOptions, 'ffprobe' => $rFFProbeOutput,
|
||||
'protocol' => $rProtocol, 'source' => $rSource,
|
||||
'segmentSettings' => $rSegmentSettings, 'externalPush' => $rExternalPush,
|
||||
'probesize' => $rProbesize, 'analyseDuration' => $rAnalyseDuration,
|
||||
'llod' => $rLLOD, 'loopback' => $rLoopback,
|
||||
'segmentStart' => $rSegmentStart, 'delayActive' => $rDelayActive,
|
||||
'ffmpegCpu' => $rFFMPEG_CPU, 'ffmpegGpu' => $rFFMPEG_GPU,
|
||||
));
|
||||
if ($rShadow !== $rFFMPEG) {
|
||||
error_log('[XC_VM] FFMPEG-SHADOW-DIFF stream ' . $rStreamID . "\nOLD: " . $rFFMPEG . "\nNEW: " . $rShadow);
|
||||
}
|
||||
// Assemble the live ffmpeg command (pure). buildLive() does all the
|
||||
// transcode-attribute resolution and {TEMPLATE} substitution internally, so
|
||||
// it is fed the raw stream row.
|
||||
$rFFMPEG = self::buildLive(array(
|
||||
'stream' => $rStream, 'settings' => $rSettings, 'servers' => $rServers,
|
||||
'streamID' => $rStreamID, 'streamSource' => $rStreamSource,
|
||||
'fetchOptions' => $rFetchOptions, 'ffprobe' => $rFFProbeOutput,
|
||||
'protocol' => $rProtocol, 'source' => $rSource,
|
||||
'segmentSettings' => $rSegmentSettings, 'externalPush' => array(),
|
||||
'probesize' => $rProbesize, 'analyseDuration' => $rAnalyseDuration,
|
||||
'llod' => $rLLOD, 'loopback' => $rLoopback,
|
||||
'segmentStart' => $rSegmentStart, 'delayActive' => $rDelayActive,
|
||||
'ffmpegCpu' => $rFFMPEG_CPU, 'ffmpegGpu' => $rFFMPEG_GPU,
|
||||
));
|
||||
|
||||
shell_exec($rFFMPEG);
|
||||
file_put_contents(STREAMS_PATH . $rStreamID . '_.ffmpeg', $rFFMPEG);
|
||||
|
||||
Reference in New Issue
Block a user