2026-02-21 19:59:51 +03:00
<? php
2026-06-25 18:39:28 +03:00
namespace XcVm\Domain\Stream ;
2026-06-25 20:24:36 +03:00
use XcVm\Core\Config\SettingsManager ;
2026-06-24 22:35:33 +03:00
use XcVm\Core\Diagnostics\DiagnosticsService ;
2026-06-24 21:32:10 +03:00
use XcVm\Core\Http\CurlClient ;
2026-06-25 20:24:36 +03:00
use XcVm\Core\Util\StreamUtils ;
2026-04-05 22:38:36 +03:00
/**
* StreamProcess — stream process
*
* @package XC_VM_Domain_Stream
* @author Divarion_D <https://github.com/Divarion-D>
* @copyright 2025-2026 Vateron Media
* @link https://github.com/Vateron-Media/XC_VM
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
2026-02-21 19:59:51 +03:00
class StreamProcess {
2026-06-28 13:48:35 +03:00
use \XcVm\Infrastructure\Database\DatabaseAware ;
2026-03-07 21:53:45 +03:00
/**
* Write stream action log to file
*
* Migrated from CoreUtilities::streamLog()
*
* @param int $rStreamID
* @param int $rServerID
* @param string $rAction
* @param string $rSource
*/
public static function streamLog ( $rStreamID , $rServerID , $rAction , $rSource = '' ) {
if ( SettingsManager :: getAll ()[ 'save_restart_logs' ] != 0 ) {
$rData = array ( 'server_id' => $rServerID , 'stream_id' => $rStreamID , 'action' => $rAction , 'source' => $rSource , 'time' => time ());
file_put_contents ( LOGS_TMP_PATH . 'stream_log.log' , base64_encode ( json_encode ( $rData )) . " \n " , FILE_APPEND );
}
}
2026-06-22 11:42:26 +03:00
/**
* Clear cached runtime data for the given stream sources.
*
* @param array $rSources Source identifiers.
* @return void
*/
2026-02-21 19:59:51 +03:00
public static function deleteCache ( $rSources ) {
if ( ! empty ( $rSources )) {
foreach ( $rSources as $rSource ) {
if ( ! file_exists ( CACHE_TMP_PATH . md5 ( $rSource ))) {
} else {
unlink ( CACHE_TMP_PATH . md5 ( $rSource ));
}
}
} else {
2026-06-26 19:26:18 +03:00
return ;
2026-02-21 19:59:51 +03:00
}
}
2026-06-22 11:42:26 +03:00
/**
* Queue a channel to be started (optionally on a specific server).
*
* @param int $rStreamID Stream id.
* @param int|null $rServerID Target server id, or null for the default.
* @return mixed Queue result.
*/
2026-02-27 22:32:39 +03:00
public static function queueChannel ( $rStreamID , $rServerID = null ) {
2026-06-15 18:07:36 +03:00
$db = self :: db ();
2026-02-21 19:59:51 +03:00
if ( $rServerID ) {
} else {
$rServerID = SERVER_ID ;
}
$db -> query ( 'SELECT `id` FROM `queue` WHERE `stream_id` = ? AND `server_id` = ?;' , $rStreamID , $rServerID );
if ( $db -> num_rows () != 0 ) {
} else {
$db -> query ( "INSERT INTO `queue`(`type`, `stream_id`, `server_id`, `added`) VALUES('channel', ?, ?, ?);" , $rStreamID , $rServerID , time ());
}
}
2026-06-22 11:42:26 +03:00
/**
* Create the runtime channel entry for a stream.
*
* @param int $rStreamID Stream id.
* @return mixed Creation result.
*/
2026-02-21 19:59:51 +03:00
public static function createChannel ( $rStreamID ) {
2026-03-14 23:57:33 +03:00
shell_exec ( PHP_BIN . ' ' . MAIN_HOME . 'console.php created ' . intval ( $rStreamID ) . ' >/dev/null 2>/dev/null &' );
2026-02-21 19:59:51 +03:00
return true ;
}
2026-06-22 11:42:26 +03:00
/**
* Start the monitor process for a stream.
*
* @param int $rStreamID Stream id.
* @param int $rRestart Restart flag/counter.
* @return mixed Start result.
*/
2026-02-21 19:59:51 +03:00
public static function startMonitor ( $rStreamID , $rRestart = 0 ) {
2026-03-14 23:57:33 +03:00
shell_exec ( PHP_BIN . ' ' . MAIN_HOME . 'console.php monitor ' . intval ( $rStreamID ) . ' ' . intval ( $rRestart ) . ' >/dev/null 2>/dev/null &' );
2026-02-21 19:59:51 +03:00
return true ;
}
2026-06-22 11:42:26 +03:00
/**
* Start the proxy process for a stream.
*
* @param int $rStreamID Stream id.
* @return mixed Start result.
*/
2026-02-21 19:59:51 +03:00
public static function startProxy ( $rStreamID ) {
2026-03-14 23:57:33 +03:00
shell_exec ( PHP_BIN . ' ' . MAIN_HOME . 'console.php proxy ' . intval ( $rStreamID ) . ' >/dev/null 2>/dev/null &' );
2026-02-21 19:59:51 +03:00
return true ;
}
2026-06-22 11:42:26 +03:00
/**
* Start thumbnail generation for a stream.
*
* @param int $rStreamID Stream id.
* @return mixed Start result.
*/
2026-02-21 19:59:51 +03:00
public static function startThumbnail ( $rStreamID ) {
2026-03-14 23:57:33 +03:00
shell_exec ( PHP_BIN . ' ' . MAIN_HOME . 'console.php thumbnail ' . intval ( $rStreamID ) . ' >/dev/null 2>/dev/null &' );
2026-02-21 19:59:51 +03:00
return true ;
}
2026-06-22 11:42:26 +03:00
/**
* Push a stream's configuration update to its server.
*
* @param int $rStreamID Stream id.
* @param bool $rForce Force the update even if unchanged.
* @return mixed Update result.
*/
2026-03-07 21:53:45 +03:00
public static function updateStream ( $rStreamID , $rForce = false ) {
2026-06-15 18:07:36 +03:00
$db = self :: db ();
2026-03-07 21:53:45 +03:00
$rCached = SettingsManager :: getAll ()[ 'enable_cache' ];
$rMainID = ConnectionTracker :: getMainID ();
2026-02-21 19:59:51 +03:00
if ( $rCached ) {
$db -> query ( 'SELECT COUNT(*) AS `count` FROM `signals` WHERE `server_id` = ? AND `cache` = 1 AND `custom_data` = ?;' , $rMainID , json_encode ( array ( 'type' => 'update_stream' , 'id' => $rStreamID )));
2026-07-30 19:47:05 +03:00
if (( $db -> get_row ()[ 'count' ] ?? 0 ) != 0 ) {
2026-02-21 19:59:51 +03:00
} else {
$db -> query ( 'INSERT INTO `signals`(`server_id`, `cache`, `time`, `custom_data`) VALUES(?, 1, ?, ?);' , $rMainID , time (), json_encode ( array ( 'type' => 'update_stream' , 'id' => $rStreamID )));
}
return true ;
}
return false ;
}
2026-06-22 11:42:26 +03:00
/**
* Push configuration updates for multiple streams.
*
* @param int[] $rStreamIDs Stream ids.
* @return void
*/
2026-03-07 21:53:45 +03:00
public static function updateStreams ( $rStreamIDs ) {
2026-06-15 18:07:36 +03:00
$db = self :: db ();
2026-03-07 21:53:45 +03:00
$rCached = SettingsManager :: getAll ()[ 'enable_cache' ];
$rMainID = ConnectionTracker :: getMainID ();
2026-02-21 19:59:51 +03:00
if ( $rCached ) {
$db -> query ( 'SELECT COUNT(*) AS `count` FROM `signals` WHERE `server_id` = ? AND `cache` = 1 AND `custom_data` = ?;' , $rMainID , json_encode ( array ( 'type' => 'update_streams' , 'id' => $rStreamIDs )));
2026-07-30 19:47:05 +03:00
if (( $db -> get_row ()[ 'count' ] ?? 0 ) != 0 ) {
2026-02-21 19:59:51 +03:00
} else {
$db -> query ( 'INSERT INTO `signals`(`server_id`, `cache`, `time`, `custom_data`) VALUES(?, 1, ?, ?);' , $rMainID , time (), json_encode ( array ( 'type' => 'update_streams' , 'id' => $rStreamIDs )));
}
2026-06-26 19:26:18 +03:00
return ;
2026-02-21 19:59:51 +03:00
}
2026-06-26 19:26:18 +03:00
return ;
2026-02-21 19:59:51 +03:00
}
2026-08-07 18:51:07 +03:00
/**
* Build the `-filter_complex` logo/overlay input from transcode attributes.
*
* Attribute 16 carries the logo (val/pos), 17 enables deinterlace (yadif) and
* 9 an optional scale. Attribute 16 is consumed (unset in place) so it is not
* also emitted as a plain ffmpeg flag. Shared by createChannelItem, startMovie
* and startStream, which previously inlined this block verbatim.
*
* @param array $rTranscodeAttributes Transcode attributes (attr 16 unset in place).
* @param bool $rLoopback Loopback streams never overlay a logo.
* @return string `-i <logo> -filter_complex "..."`, or '' when no logo applies.
*/
private static function buildLogoFilterOptions ( array & $rTranscodeAttributes , $rLoopback ) {
if ( ! isset ( $rTranscodeAttributes [ 16 ]) || $rLoopback ) {
return '' ;
}
$rAttr = $rTranscodeAttributes ;
$rLogoPath = $rAttr [ 16 ][ 'val' ];
$rPos = ( isset ( $rAttr [ 16 ][ 'pos' ]) && $rAttr [ 16 ][ 'pos' ] !== '10:10' ) ? $rAttr [ 16 ][ 'pos' ] : '10:main_h-overlay_h-10' ;
$rChain = array ();
$rBase = '[0:v]' ;
$rVideoFilters = array ();
if ( isset ( $rAttr [ 17 ])) {
$rVideoFilters [] = 'yadif' ;
}
if ( isset ( $rAttr [ 9 ][ 'val' ]) && strlen ( $rAttr [ 9 ][ 'val' ]) > 0 ) {
$rVideoFilters [] = 'scale=' . $rAttr [ 9 ][ 'val' ];
}
if ( ! empty ( $rVideoFilters )) {
$rChain [] = $rBase . implode ( ',' , $rVideoFilters ) . '[bg]' ;
$rBase = '[bg]' ;
}
$rChain [] = '[1:v]scale=250:-1[logo]' ;
$rChain [] = $rBase . '[logo]overlay=' . $rPos ;
unset ( $rTranscodeAttributes [ 16 ]);
return '-i ' . escapeshellarg ( $rLogoPath ) . ' -filter_complex "' . implode ( '; ' , $rChain ) . '"' ;
}
/**
* Detect a CUVID hardware decoder for the source when GPU transcoding is on.
*
* Returns `-c:v <codec>_cuvid` for a known GPU-decodable codec, otherwise ''.
* Shared by createChannelItem and startMovie.
*
* @param string $rGpuOptions GPU command from the profile ('' = CPU path).
* @param string $rSourcePath Source URL/path to probe.
* @return string CUVID input-codec flag, or ''.
*/
private static function resolveGpuInputCodec ( $rGpuOptions , $rSourcePath ) {
if ( empty ( $rGpuOptions )) {
return '' ;
}
$rFFProbeOutput = \XcVm\Streaming\Codec\FFprobeRunner :: probeStream ( $rSourcePath );
if ( in_array ( $rFFProbeOutput [ 'codecs' ][ 'video' ][ 'codec_name' ], array ( 'h264' , 'hevc' , 'mjpeg' , 'mpeg1' , 'mpeg2' , 'mpeg4' , 'vc1' , 'vp8' , 'vp9' ))) {
return '-c:v ' . $rFFProbeOutput [ 'codecs' ][ 'video' ][ 'codec_name' ] . '_cuvid' ;
}
return '' ;
}
/**
* Default audio and video codecs to stream copy when the profile left them unset.
* Shared by createChannelItem, startMovie and startStream.
*
* @param array $rTranscodeAttributes Transcode attributes (modified in place).
*/
private static function applyDefaultCopyCodecs ( array & $rTranscodeAttributes ) {
if ( ! array_key_exists ( '-acodec' , $rTranscodeAttributes )) {
$rTranscodeAttributes [ '-acodec' ] = 'copy' ;
}
if ( ! array_key_exists ( '-vcodec' , $rTranscodeAttributes )) {
$rTranscodeAttributes [ '-vcodec' ] = 'copy' ;
}
}
2026-06-22 11:42:26 +03:00
/**
* Build the runtime channel item (ffmpeg command/config) for a source.
*
* @param int $rStreamID Stream id.
* @param mixed $rSource Source definition.
* @return mixed The constructed channel item.
*/
2026-08-07 18:56:00 +03:00
/**
* Build the ffmpeg subtitle import + metadata options for a VOD movie.
*
2026-08-07 19:04:16 +03:00
* Imports every configured subtitle as an extra input and maps each one into
* the output. Inputs are 0 = main source and 1..N = subtitles, so metadata
* targets `-map <i+1>`.
*
* Previously the metadata loop was nested inside the import loop and reused
* the same `$i`, which made the import loop run only once (first subtitle) yet
* still emit `-map` for every file — so multi-subtitle movies imported one
* track but mapped non-existent inputs. The two loops are now siblings.
2026-08-07 18:56:00 +03:00
*
* @param string $rSubtitlesJson `movie_subtitles` JSON from the stream row.
* @param array $rServers Server registry (for remote subtitle fetch).
* @return array{0:string,1:string} [$rSubtitlesImport, $rSubtitlesMetadata].
*/
private static function buildSubtitleImport ( $rSubtitlesJson , $rServers ) {
$rSubtitles = json_decode ( $rSubtitlesJson , true );
$rSubtitlesImport = '' ;
$rSubtitlesMetadata = '' ;
if ( ! empty ( $rSubtitles ) && ! empty ( $rSubtitles [ 'files' ]) && is_array ( $rSubtitles [ 'files' ])) {
2026-08-07 19:04:16 +03:00
$rCount = count ( $rSubtitles [ 'files' ]);
for ( $i = 0 ; $i < $rCount ; $i ++ ) {
2026-08-07 18:56:00 +03:00
$rSubtitleFile = escapeshellarg ( $rSubtitles [ 'files' ][ $i ]);
$rInputCharset = escapeshellarg ( $rSubtitles [ 'charset' ][ $i ]);
if ( $rSubtitles [ 'location' ] == SERVER_ID ) {
$rSubtitlesImport .= '-sub_charenc ' . $rInputCharset . ' -i ' . $rSubtitleFile . ' ' ;
} else {
$rSubtitlesImport .= '-sub_charenc ' . $rInputCharset . ' -i "' . $rServers [ $rSubtitles [ 'location' ]][ 'api_url' ] . '&action=getFile&filename=' . urlencode ( $rSubtitleFile ) . '" ' ;
}
2026-08-07 19:04:16 +03:00
}
for ( $i = 0 ; $i < $rCount ; $i ++ ) {
$rSubtitlesMetadata .= '-map ' . ( $i + 1 ) . ' -metadata:s:s:' . $i . ' title=' . escapeshellcmd ( $rSubtitles [ 'names' ][ $i ]) . ' -metadata:s:s:' . $i . ' language=' . escapeshellcmd ( $rSubtitles [ 'names' ][ $i ]) . ' ' ;
2026-08-07 18:56:00 +03:00
}
}
return array ( $rSubtitlesImport , $rSubtitlesMetadata );
}
2026-08-07 19:31:18 +03:00
/**
* Resolve the ffmpeg `-map` selection for a VOD transcode.
*
* A custom map (when set) wins; otherwise strip subtitles on request, else
* copy everything. Extracted from startMovie.
*
* @param string|null $rCustomMap Admin custom map, or empty for the default.
* @param mixed $rRemoveSubtitles Truthy (== 1) to drop subtitle streams.
* @return string The `-map ...` fragment.
*/
private static function resolveOutputMap ( $rCustomMap , $rRemoveSubtitles ) {
if ( ! empty ( $rCustomMap )) {
return escapeshellcmd ( $rCustomMap ) . ' -copy_unknown ' ;
}
if ( $rRemoveSubtitles == 1 ) {
return '-map 0:a -map 0:v' ;
}
return '-map 0 -copy_unknown ' ;
}
/**
* Pick the subtitle codec for a VOD target container. Extracted from startMovie.
*
* @param string $rContainer Target container (mp4/mkv/…).
* @return string ffmpeg subtitle codec: mov_text (mp4), srt (mkv), else copy.
*/
private static function subtitleCodecForContainer ( $rContainer ) {
if ( $rContainer == 'mp4' ) {
return 'mov_text' ;
}
if ( $rContainer == 'mkv' ) {
return 'srt' ;
}
return 'copy' ;
}
/**
* Resolve a created-channel source string into [serverId, sourcePath].
*
* A plain string is a local path on this server. An `s:<serverId>:<path>`
* string references a file on another server; when that server is known it is
* rewritten to its getFile API URL, otherwise the raw path is kept. Extracted
* from createChannelItem. The `explode(':', …, 3)` limit keeps colons in the
* path intact.
*
* @param string $rSource Source string (`path` or `s:<serverId>:<path>`).
* @param mixed $rServers Server registry (array keyed by server id).
* @return array{0:int,1:string} [serverId, sourcePath]
*/
private static function resolveChannelSource ( $rSource , $rServers ) {
if ( substr ( $rSource , 0 , 2 ) == 's:' ) {
$rSplit = explode ( ':' , $rSource , 3 );
$rServerID = intval ( $rSplit [ 1 ]);
$rSourcePath = $rSplit [ 2 ];
if ( $rServerID != SERVER_ID ) {
if ( is_array ( $rServers ) && isset ( $rServers [ $rServerID ])) {
$rSourcePath = $rServers [ $rServerID ][ 'api_url' ] . '&action=getFile&filename=' . urlencode ( $rSplit [ 2 ]);
} else {
$rSourcePath = $rSplit [ 2 ];
}
}
} else {
$rServerID = SERVER_ID ;
$rSourcePath = $rSource ;
}
return array ( $rServerID , $rSourcePath );
}
/**
* Assemble the HLS/mpegts segmenter output arguments for a live stream.
*
* Pure string builder extracted from startStream. The caller still computes
* the conditional $rOptions, $rKeyFrames and $rInitTime and passes them in, so
* the control flow is unchanged.
*
* @param string $rOptions Leading option placeholders ({MAP} {LLOD}).
* @param array $rSegmentSettings seg_time / seg_list_size / seg_delete_threshold.
* @param string $rKeyFrames Extra hls_flags (e.g. '+split_by_time') or ''.
* @param int $rInitTime hls_init_time seconds.
* @param int $rStreamID Stream id (segment/playlist filenames).
* @return string The `-f hls …` output fragment.
*/
private static function buildHlsMpegtsOutput ( $rOptions , $rSegmentSettings , $rKeyFrames , $rInitTime , $rStreamID ) {
return $rOptions . ' -individual_header_trailer 0 -f hls -hls_init_time ' . $rInitTime
. ' -hls_time ' . intval ( $rSegmentSettings [ 'seg_time' ])
. ' -hls_list_size ' . intval ( $rSegmentSettings [ 'seg_list_size' ])
. ' -hls_delete_threshold ' . intval ( $rSegmentSettings [ 'seg_delete_threshold' ])
. ' -hls_flags delete_segments+discont_start+omit_endlist' . $rKeyFrames
. ' -hls_segment_type mpegts -hls_segment_filename "' . STREAMS_PATH . intval ( $rStreamID ) . '_%d.ts" "'
. STREAMS_PATH . intval ( $rStreamID ) . '_.m3u8" ' ;
}
/**
* Wrap an FLV output target (local RTMP relay or external push URL) with the
* shared `-f flv -flvflags no_duration_filesize` options. Extracted from the
* two identical FLV output lines in startStream.
*
* @param string $rFLVOptions Leading option placeholders ({MAP} {AAC_FILTER}).
* @param string $rTarget The rtmp:// URL or escaped push URL.
* @return string The `… -f flv … <target> ` output fragment.
*/
private static function buildFlvOutput ( $rFLVOptions , $rTarget ) {
return $rFLVOptions . ' -f flv -flvflags no_duration_filesize ' . $rTarget . ' ' ;
}
2026-03-07 21:53:45 +03:00
public static function createChannelItem ( $rStreamID , $rSource ) {
2026-06-15 18:07:36 +03:00
global $rSettings , $rServers , $rFFMPEG_CPU , $rFFMPEG_GPU ;
$db = self :: db ();
2026-02-21 19:59:51 +03:00
$rStream = array ();
$rLoopback = false ;
$db -> query ( 'SELECT * FROM `streams` t1 INNER JOIN `streams_types` t2 ON t2.type_id = t1.type AND t1.type = 3 LEFT JOIN `profiles` t4 ON t1.transcode_profile_id = t4.profile_id WHERE t1.direct_source = 0 AND t1.id = ?' , $rStreamID );
if ( $db -> num_rows () > 0 ) {
$rStream [ 'stream_info' ] = $db -> get_row ();
$db -> query ( 'SELECT * FROM `streams_servers` WHERE stream_id = ? AND `server_id` = ?' , $rStreamID , SERVER_ID );
if ( $db -> num_rows () > 0 ) {
$rStream [ 'server_info' ] = $db -> get_row ();
$rMD5 = md5 ( $rSource );
2026-08-07 19:31:18 +03:00
list ( $rServerID , $rSourcePath ) = self :: resolveChannelSource ( $rSource , $rServers );
2026-02-21 19:59:51 +03:00
if ( $rServerID == SERVER_ID && intval ( $rStream [ 'stream_info' ][ 'movie_symlink' ]) == 1 ) {
$rExtension = pathinfo ( $rSource )[ 'extension' ];
if ( strlen ( $rExtension ) == 0 ) {
$rExtension = 'mp4' ;
}
$rCommand = 'ln -sfn ' . escapeshellarg ( $rSourcePath ) . ' "' . CREATED_PATH . intval ( $rStreamID ) . '_' . $rMD5 . '.' . escapeshellcmd ( $rExtension ) . '" >/dev/null 2>/dev/null & echo $! > "' . CREATED_PATH . intval ( $rStreamID ) . '_' . $rMD5 . '.pid"' ;
} else {
$rStream [ 'stream_info' ][ 'transcode_attributes' ] = json_decode ( $rStream [ 'stream_info' ][ 'profile_options' ], true );
if ( ! is_array ( $rStream [ 'stream_info' ][ 'transcode_attributes' ])) {
$rStream [ 'stream_info' ][ 'transcode_attributes' ] = array ();
}
2026-08-07 18:51:07 +03:00
$rLogoOptions = self :: buildLogoFilterOptions ( $rStream [ 'stream_info' ][ 'transcode_attributes' ], $rLoopback );
2026-02-21 19:59:51 +03:00
$rGPUOptions = ( isset ( $rStream [ 'stream_info' ][ 'transcode_attributes' ][ 'gpu' ]) ? $rStream [ 'stream_info' ][ 'transcode_attributes' ][ 'gpu' ][ 'cmd' ] : '' );
2026-08-07 18:51:07 +03:00
$rInputCodec = self :: resolveGpuInputCodec ( $rGPUOptions , $rSourcePath );
2026-02-21 19:59:51 +03:00
2026-07-10 19:19:25 +03:00
$rCommand = (( 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 -progress "' . CREATED_PATH . intval ( $rStreamID ) . '_' . $rMD5 . '.progress" {GPU} -fflags +genpts -async 1 -i {STREAM_SOURCE} {LOGO} ' ;
2026-02-21 19:59:51 +03:00
2026-08-07 18:51:07 +03:00
self :: applyDefaultCopyCodecs ( $rStream [ 'stream_info' ][ 'transcode_attributes' ]);
2026-02-21 19:59:51 +03:00
if ( isset ( $rStream [ 'stream_info' ][ 'transcode_attributes' ][ 'gpu' ])) {
$rCommand .= '-gpu ' . intval ( $rStream [ 'stream_info' ][ 'transcode_attributes' ][ 'gpu' ][ 'device' ]) . ' ' ;
}
2026-03-07 21:53:45 +03:00
$rCommand .= implode ( ' ' , StreamUtils :: parseTranscode ( $rStream [ 'stream_info' ][ 'transcode_attributes' ])) . ' ' ;
2026-02-21 19:59:51 +03:00
$rCommand .= '-strict -2 -mpegts_flags +initial_discontinuity -f mpegts "' . CREATED_PATH . intval ( $rStreamID ) . '_' . $rMD5 . '.ts"' ;
$rCommand .= ' >/dev/null 2>"' . CREATED_PATH . intval ( $rStreamID ) . '_' . $rMD5 . '.errors" & echo $! > "' . CREATED_PATH . intval ( $rStreamID ) . '_' . $rMD5 . '.pid"' ;
$rCommand = str_replace ( array ( '{GPU}' , '{INPUT_CODEC}' , '{LOGO}' , '{STREAM_SOURCE}' ), array ( $rGPUOptions , $rInputCodec , $rLogoOptions , escapeshellarg ( $rSourcePath )), $rCommand );
}
shell_exec ( $rCommand );
return intval ( file_get_contents ( CREATED_PATH . intval ( $rStreamID ) . '_' . $rMD5 . '.pid' ));
}
return false ;
}
return false ;
}
2026-06-22 11:42:26 +03:00
/**
* Stop a running channel stream.
*
* @param int $rStreamID Stream id.
* @param bool $rStop Mark the stream as fully stopped (not just restarting).
* @return mixed Stop result.
*/
2026-02-27 22:32:39 +03:00
public static function stopStream ( $rStreamID , $rStop = false ) {
2026-06-15 18:07:36 +03:00
$db = self :: db ();
2026-02-21 19:59:51 +03:00
if ( file_exists ( STREAMS_PATH . $rStreamID . '_.monitor' )) {
$rMonitor = intval ( file_get_contents ( STREAMS_PATH . $rStreamID . '_.monitor' ));
} else {
$db -> query ( 'SELECT `monitor_pid` FROM `streams_servers` WHERE `server_id` = ? AND `stream_id` = ? LIMIT 1;' , SERVER_ID , $rStreamID );
2026-04-23 21:28:51 +03:00
$rStreamServer = $db -> get_row ();
$rMonitor = intval ( $rStreamServer [ 'monitor_pid' ] ?? 0 );
2026-02-21 19:59:51 +03:00
}
2026-06-25 18:55:35 +03:00
if ( 0 < $rMonitor && \XcVm\Streaming\Health\ProcessChecker :: checkPID ( $rMonitor , array ( 'XC_VM[' . $rStreamID . ']' , 'XC_VMProxy[' . $rStreamID . ']' )) && is_numeric ( $rMonitor )) {
2026-02-21 19:59:51 +03:00
posix_kill ( $rMonitor , 9 );
}
if ( file_exists ( STREAMS_PATH . $rStreamID . '_.pid' )) {
$rPID = intval ( file_get_contents ( STREAMS_PATH . $rStreamID . '_.pid' ));
} else {
$db -> query ( 'SELECT `pid` FROM `streams_servers` WHERE `server_id` = ? AND `stream_id` = ? LIMIT 1;' , SERVER_ID , $rStreamID );
2026-04-23 21:28:51 +03:00
$rStreamServer = $db -> get_row ();
$rPID = intval ( $rStreamServer [ 'pid' ] ?? 0 );
2026-02-21 19:59:51 +03:00
}
2026-06-25 18:55:35 +03:00
if ( 0 < $rPID && \XcVm\Streaming\Health\ProcessChecker :: checkPID ( $rPID , array ( $rStreamID . '_.m3u8' , $rStreamID . '_%d.ts' , 'LLOD[' . $rStreamID . ']' , 'XC_VMProxy[' . $rStreamID . ']' , 'Loopback[' . $rStreamID . ']' )) && is_numeric ( $rPID )) {
2026-02-21 19:59:51 +03:00
posix_kill ( $rPID , 9 );
}
if ( file_exists ( SIGNALS_TMP_PATH . 'queue_' . intval ( $rStreamID ))) {
unlink ( SIGNALS_TMP_PATH . 'queue_' . intval ( $rStreamID ));
}
2026-03-07 21:53:45 +03:00
self :: streamLog ( $rStreamID , SERVER_ID , 'STREAM_STOP' );
2026-02-21 19:59:51 +03:00
shell_exec ( 'rm -f ' . STREAMS_PATH . intval ( $rStreamID ) . '_*' );
if ( $rStop ) {
shell_exec ( 'rm -f ' . DELAY_PATH . intval ( $rStreamID ) . '_*' );
$db -> query ( 'UPDATE `streams_servers` SET `bitrate` = NULL,`current_source` = NULL,`to_analyze` = 0,`pid` = NULL,`stream_started` = NULL,`stream_info` = NULL,`audio_codec` = NULL,`video_codec` = NULL,`resolution` = NULL,`compatible` = 0,`stream_status` = 0,`monitor_pid` = NULL WHERE `stream_id` = ? AND `server_id` = ?' , $rStreamID , SERVER_ID );
2026-03-07 21:53:45 +03:00
self :: updateStream ( $rStreamID );
2026-02-21 19:59:51 +03:00
}
}
2026-06-22 11:42:26 +03:00
/**
* Stop a running movie (VOD) stream.
*
* @param int $rStreamID Stream id.
* @param bool $rForce Force stop.
* @return mixed Stop result.
*/
2026-02-27 22:32:39 +03:00
public static function stopMovie ( $rStreamID , $rForce = false ) {
2026-06-15 18:07:36 +03:00
$db = self :: db ();
2026-02-27 22:32:39 +03:00
shell_exec ( "kill -9 `ps -ef | grep '/" . intval ( $rStreamID ) . ".' | grep -v grep | awk '{print \$ 2}'`;" );
2026-02-21 19:59:51 +03:00
if ( $rForce ) {
exec ( 'rm ' . MAIN_HOME . 'content/vod/' . intval ( $rStreamID ) . '.*' );
} else {
$db -> query ( 'INSERT INTO `signals`(`server_id`, `time`, `custom_data`, `cache`) VALUES(?, ?, ?, 1);' , SERVER_ID , time (), json_encode ( array ( 'type' => 'delete_vod' , 'id' => $rStreamID )));
}
$db -> query ( 'UPDATE `streams_servers` SET `bitrate` = NULL,`current_source` = NULL,`to_analyze` = 0,`pid` = NULL,`stream_started` = NULL,`stream_info` = NULL,`audio_codec` = NULL,`video_codec` = NULL,`resolution` = NULL,`compatible` = 0,`stream_status` = 0 WHERE `stream_id` = ? AND `server_id` = ?' , $rStreamID , SERVER_ID );
2026-03-07 21:53:45 +03:00
self :: updateStream ( $rStreamID );
2026-02-21 19:59:51 +03:00
}
2026-06-22 11:42:26 +03:00
/**
* Queue a movie (VOD) to be started.
*
* @param int $rStreamID Stream id.
* @param int|null $rServerID Target server id, or null for the default.
* @return mixed Queue result.
*/
2026-02-27 22:32:39 +03:00
public static function queueMovie ( $rStreamID , $rServerID = null ) {
2026-06-15 18:07:36 +03:00
$db = self :: db ();
2026-02-21 19:59:51 +03:00
if ( $rServerID ) {
} else {
$rServerID = SERVER_ID ;
}
$db -> query ( 'DELETE FROM `queue` WHERE `stream_id` = ? AND `server_id` = ?;' , $rStreamID , $rServerID );
$db -> query ( "INSERT INTO `queue`(`type`, `stream_id`, `server_id`, `added`) VALUES('movie', ?, ?, ?);" , $rStreamID , $rServerID , time ());
}
2026-06-22 11:42:26 +03:00
/**
* Queue multiple movies to be started.
*
* @param int[] $rStreamIDs Stream ids.
* @param int|null $rServerID Target server id, or null for the default.
* @return void
*/
2026-02-27 22:32:39 +03:00
public static function queueMovies ( $rStreamIDs , $rServerID = null ) {
2026-06-15 18:07:36 +03:00
$db = self :: db ();
2026-02-21 19:59:51 +03:00
if ( $rServerID ) {
} else {
$rServerID = SERVER_ID ;
}
if ( 0 >= count ( $rStreamIDs )) {
} else {
$db -> query ( 'DELETE FROM `queue` WHERE `stream_id` IN (' . implode ( ',' , array_map ( 'intval' , $rStreamIDs )) . ') AND `server_id` = ?;' , $rServerID );
$rQuery = '' ;
foreach ( $rStreamIDs as $rStreamID ) {
if ( 0 >= $rStreamID ) {
} else {
$rQuery .= "('movie', " . intval ( $rStreamID ) . ', ' . intval ( $rServerID ) . ', ' . time () . '),' ;
}
}
if ( empty ( $rQuery )) {
} else {
$rQuery = rtrim ( $rQuery , ',' );
$db -> query ( 'INSERT INTO `queue`(`type`, `stream_id`, `server_id`, `added`) VALUES ' . $rQuery . ';' );
}
}
}
2026-06-22 11:42:26 +03:00
/**
* Refresh movie metadata/processing for the given ids.
*
* @param int[] $rIDs Stream ids.
* @param int $rType Refresh type.
* @return void
*/
2026-02-27 22:32:39 +03:00
public static function refreshMovies ( $rIDs , $rType = 1 ) {
2026-06-15 18:07:36 +03:00
$db = self :: db ();
2026-02-21 19:59:51 +03:00
if ( 0 >= count ( $rIDs )) {
} else {
$db -> query ( 'DELETE FROM `watch_refresh` WHERE `type` = ? AND `stream_id` IN (' . implode ( ',' , array_map ( 'intval' , $rIDs )) . ');' , $rType );
$rQuery = '' ;
foreach ( $rIDs as $rID ) {
if ( 0 >= $rID ) {
} else {
$rQuery .= '(' . intval ( $rType ) . ', ' . intval ( $rID ) . ', 0),' ;
}
}
if ( empty ( $rQuery )) {
} else {
$rQuery = rtrim ( $rQuery , ',' );
$db -> query ( 'INSERT INTO `watch_refresh`(`type`, `stream_id`, `status`) VALUES ' . $rQuery . ';' );
}
}
}
2026-06-22 11:42:26 +03:00
/**
* Start a movie (VOD) stream.
*
* @param int $rStreamID Stream id.
* @return mixed Start result.
*/
2026-03-07 21:53:45 +03:00
public static function startMovie ( $rStreamID ) {
2026-06-15 18:07:36 +03:00
global $rSettings , $rServers , $rFFMPEG_CPU , $rFFMPEG_GPU ;
$db = self :: db ();
2026-02-21 19:59:51 +03:00
$rStream = array ();
$rLoopback = false ;
$db -> query ( 'SELECT * FROM `streams` t1 INNER JOIN `streams_types` t2 ON t2.type_id = t1.type AND t2.live = 0 LEFT JOIN `profiles` t4 ON t1.transcode_profile_id = t4.profile_id WHERE t1.direct_source = 0 AND t1.id = ?' , $rStreamID );
if ( $db -> num_rows () > 0 ) {
$rStream [ 'stream_info' ] = $db -> get_row ();
$db -> query ( 'SELECT * FROM `streams_servers` WHERE stream_id = ? AND `server_id` = ?' , $rStreamID , SERVER_ID );
if ( $db -> num_rows () > 0 ) {
$rStream [ 'server_info' ] = $db -> get_row ();
$db -> query ( 'SELECT t1.*, t2.* FROM `streams_options` t1, `streams_arguments` t2 WHERE t1.stream_id = ? AND t1.argument_id = t2.id' , $rStreamID );
$rStream [ 'stream_arguments' ] = $db -> get_rows ();
list ( $rStreamSource ) = json_decode ( $rStream [ 'stream_info' ][ 'stream_source' ], true );
if ( substr ( $rStreamSource , 0 , 2 ) == 's:' ) {
$rMovieSource = explode ( ':' , $rStreamSource , 3 );
$rMovieServerID = $rMovieSource [ 1 ];
if ( $rMovieServerID != SERVER_ID ) {
$rMoviePath = $rServers [ $rMovieServerID ][ 'api_url' ] . '&action=getFile&filename=' . urlencode ( $rMovieSource [ 2 ]);
} else {
$rMoviePath = $rMovieSource [ 2 ];
}
$rProtocol = null ;
} else {
if ( substr ( $rStreamSource , 0 , 1 ) == '/' ) {
$rMovieServerID = SERVER_ID ;
$rMoviePath = $rStreamSource ;
$rProtocol = null ;
} else {
$rProtocol = substr ( $rStreamSource , 0 , strpos ( $rStreamSource , '://' ));
$rMoviePath = str_replace ( ' ' , '%20' , $rStreamSource );
2026-03-07 21:53:45 +03:00
$rFetchOptions = implode ( ' ' , StreamUtils :: getArguments ( $rStream [ 'stream_arguments' ], $rProtocol , 'fetch' ));
2026-02-21 19:59:51 +03:00
}
}
if (( isset ( $rMovieServerID ) && $rMovieServerID == SERVER_ID || file_exists ( $rMoviePath )) && $rStream [ 'stream_info' ][ 'movie_symlink' ] == 1 ) {
$rFFMPEG = 'ln -sfn ' . escapeshellarg ( $rMoviePath ) . ' ' . VOD_PATH . intval ( $rStreamID ) . '.' . escapeshellcmd ( pathinfo ( $rMoviePath )[ 'extension' ]) . ' >/dev/null 2>/dev/null & echo $! > ' . VOD_PATH . intval ( $rStreamID ) . '_.pid' ;
} else {
2026-08-07 18:56:00 +03:00
list ( $rSubtitlesImport , $rSubtitlesMetadata ) = self :: buildSubtitleImport ( $rStream [ 'stream_info' ][ 'movie_subtitles' ], $rServers );
2026-02-21 19:59:51 +03:00
$rReadNative = ( $rStream [ 'stream_info' ][ 'read_native' ] == 1 ? '-re' : '' );
if ( $rStream [ 'stream_info' ][ 'enable_transcode' ] == 1 ) {
if ( $rStream [ 'stream_info' ][ 'transcode_profile_id' ] == - 1 ) {
$rDecoded = json_decode ( $rStream [ 'stream_info' ][ 'transcode_attributes' ], true );
2026-03-07 21:53:45 +03:00
$rStream [ 'stream_info' ][ 'transcode_attributes' ] = array_merge ( StreamUtils :: getArguments ( $rStream [ 'stream_arguments' ], $rProtocol , 'transcode' ), ( is_array ( $rDecoded ) ? $rDecoded : array ()));
2026-02-21 19:59:51 +03:00
} else {
$rDecoded = json_decode ( $rStream [ 'stream_info' ][ 'profile_options' ], true );
$rStream [ 'stream_info' ][ 'transcode_attributes' ] = ( is_array ( $rDecoded ) ? $rDecoded : array ());
}
} else {
$rStream [ 'stream_info' ][ 'transcode_attributes' ] = array ();
}
2026-08-07 18:51:07 +03:00
$rLogoOptions = self :: buildLogoFilterOptions ( $rStream [ 'stream_info' ][ 'transcode_attributes' ], $rLoopback );
2026-02-21 19:59:51 +03:00
$rGPUOptions = ( isset ( $rStream [ 'stream_info' ][ 'transcode_attributes' ][ 'gpu' ]) ? $rStream [ 'stream_info' ][ 'transcode_attributes' ][ 'gpu' ][ 'cmd' ] : '' );
2026-08-07 18:51:07 +03:00
$rInputCodec = self :: resolveGpuInputCodec ( $rGPUOptions , $rMoviePath );
2026-03-09 00:03:18 +03:00
$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 {GPU} {FETCH_OPTIONS} -fflags +genpts -async 1 {READ_NATIVE} -i {STREAM_SOURCE} {LOGO} ' . $rSubtitlesImport ;
2026-08-07 19:31:18 +03:00
$rMap = self :: resolveOutputMap ( $rStream [ 'stream_info' ][ 'custom_map' ], $rStream [ 'stream_info' ][ 'remove_subtitles' ]);
2026-08-07 18:51:07 +03:00
self :: applyDefaultCopyCodecs ( $rStream [ 'stream_info' ][ 'transcode_attributes' ]);
2026-08-07 19:31:18 +03:00
$rStream [ 'stream_info' ][ 'transcode_attributes' ][ '-scodec' ] = self :: subtitleCodecForContainer ( $rStream [ 'stream_info' ][ 'target_container' ]);
2026-02-21 19:59:51 +03:00
$rOutputs = array ();
$rOutputs [ $rStream [ 'stream_info' ][ 'target_container' ]] = '-movflags +faststart -dn ' . $rMap . ' -ignore_unknown ' . $rSubtitlesMetadata . ' ' . VOD_PATH . intval ( $rStreamID ) . '.' . escapeshellcmd ( $rStream [ 'stream_info' ][ 'target_container' ]);
foreach ( $rOutputs as $rOutputCommand ) {
2026-03-07 21:53:45 +03:00
$rFFMPEG .= implode ( ' ' , StreamUtils :: parseTranscode ( $rStream [ 'stream_info' ][ 'transcode_attributes' ])) . ' ' ;
2026-02-21 19:59:51 +03:00
$rFFMPEG .= $rOutputCommand ;
}
$rFFMPEG .= ' >/dev/null 2>' . VOD_PATH . intval ( $rStreamID ) . '.errors & echo $! > ' . VOD_PATH . intval ( $rStreamID ) . '_.pid' ;
$rFFMPEG = str_replace ( array ( '{GPU}' , '{INPUT_CODEC}' , '{LOGO}' , '{FETCH_OPTIONS}' , '{STREAM_SOURCE}' , '{READ_NATIVE}' ), array ( $rGPUOptions , $rInputCodec , $rLogoOptions , ( empty ( $rFetchOptions ) ? '' : $rFetchOptions ), escapeshellarg ( $rMoviePath ), ( empty ( $rStream [ 'stream_info' ][ 'custom_ffmpeg' ]) ? $rReadNative : '' )), $rFFMPEG );
}
shell_exec ( $rFFMPEG );
file_put_contents ( VOD_PATH . $rStreamID . '_.ffmpeg' , $rFFMPEG );
$rPID = intval ( file_get_contents ( VOD_PATH . $rStreamID . '_.pid' ));
$db -> query ( 'UPDATE `streams_servers` SET `to_analyze` = 1,`stream_started` = ?,`stream_status` = 0,`pid` = ? WHERE `stream_id` = ? AND `server_id` = ?' , time (), $rPID , $rStreamID , SERVER_ID );
2026-03-07 21:53:45 +03:00
self :: updateStream ( $rStreamID );
2026-02-21 19:59:51 +03:00
return $rPID ;
}
return false ;
}
return false ;
}
2026-06-22 11:42:26 +03:00
/**
* Start a loopback stream.
*
* @param int $rStreamID Stream id.
* @return mixed Start result.
*/
2026-03-07 21:53:45 +03:00
public static function startLoopback ( $rStreamID ) {
2026-06-15 18:07:36 +03:00
global $rSettings , $rServers ;
$db = self :: db ();
2026-02-21 19:59:51 +03:00
shell_exec ( 'rm -f ' . STREAMS_PATH . intval ( $rStreamID ) . '_*.ts' );
if ( ! file_exists ( STREAMS_PATH . $rStreamID . '_.pid' )) {
} else {
unlink ( STREAMS_PATH . $rStreamID . '_.pid' );
}
$rStream = array ();
$db -> query ( 'SELECT * FROM `streams` WHERE direct_source = 0 AND id = ?' , $rStreamID );
if ( $db -> num_rows () > 0 ) {
$rStream [ 'stream_info' ] = $db -> get_row ();
$db -> query ( 'SELECT * FROM `streams_servers` WHERE stream_id = ? AND `server_id` = ?' , $rStreamID , SERVER_ID );
if ( $db -> num_rows () > 0 ) {
$rStream [ 'server_info' ] = $db -> get_row ();
if ( $rStream [ 'server_info' ][ 'parent_id' ] != 0 ) {
2026-03-14 23:57:33 +03:00
shell_exec ( PHP_BIN . ' ' . MAIN_HOME . 'console.php loopback ' . intval ( $rStreamID ) . ' ' . intval ( $rStream [ 'server_info' ][ 'parent_id' ]) . ' >/dev/null 2>/dev/null & echo $! > ' . STREAMS_PATH . intval ( $rStreamID ) . '_.pid' );
2026-02-21 19:59:51 +03:00
$rPID = intval ( file_get_contents ( STREAMS_PATH . $rStreamID . '_.pid' ));
$rLoopURL = ( ! is_null ( $rServers [ SERVER_ID ][ 'private_url_ip' ]) && ! is_null ( $rServers [ $rStream [ 'server_info' ][ 'parent_id' ]][ 'private_url_ip' ]) ? $rServers [ $rStream [ 'server_info' ][ 'parent_id' ]][ 'private_url_ip' ] : $rServers [ $rStream [ 'server_info' ][ 'parent_id' ]][ 'public_url_ip' ]);
$rCurrentSource = $rLoopURL . 'admin/live?stream=' . intval ( $rStreamID ) . '&password=' . urlencode ( $rSettings [ 'live_streaming_pass' ]) . '&extension=ts' ;
$rKey = openssl_random_pseudo_bytes ( 16 );
file_put_contents ( STREAMS_PATH . $rStreamID . '_.key' , $rKey );
$rIVSize = openssl_cipher_iv_length ( 'AES-128-CBC' );
$rIV = openssl_random_pseudo_bytes ( $rIVSize );
file_put_contents ( STREAMS_PATH . $rStreamID . '_.iv' , $rIV );
$db -> query ( 'UPDATE `streams_servers` SET `delay_available_at` = ?,`to_analyze` = 0,`stream_started` = ?,`stream_info` = ?,`stream_status` = 2,`pid` = ?,`progress_info` = ?,`current_source` = ? WHERE `stream_id` = ? AND `server_id` = ?' , null , time (), null , $rPID , json_encode ( array ()), $rCurrentSource , $rStreamID , SERVER_ID );
2026-03-07 21:53:45 +03:00
self :: updateStream ( $rStreamID );
2026-02-21 19:59:51 +03:00
return array ( 'main_pid' => $rPID , 'stream_source' => $rLoopURL . 'admin/live?stream=' . intval ( $rStreamID ) . '&password=' . urlencode ( $rSettings [ 'live_streaming_pass' ]) . '&extension=ts' , 'delay_enabled' => false , 'parent_id' => 0 , 'delay_start_at' => null , 'playlist' => STREAMS_PATH . $rStreamID . '_.m3u8' , 'transcode' => false , 'offset' => 0 );
}
return 0 ;
}
return false ;
}
return false ;
}
2026-06-22 11:42:26 +03:00
/**
* Start a live-on-demand (LLOD) stream.
*
* @param int $rStreamID Stream id.
* @param array $rStreamInfo Stream metadata.
* @param array $rStreamArguments ffmpeg/stream arguments.
* @param string|null $rForceSource Force a specific source URL.
* @return mixed Start result.
*/
2026-02-27 22:32:39 +03:00
public static function startLLOD ( $rStreamID , $rStreamInfo , $rStreamArguments , $rForceSource = null ) {
2026-06-15 18:07:36 +03:00
$db = self :: db ();
2026-02-21 19:59:51 +03:00
shell_exec ( 'rm -f ' . STREAMS_PATH . intval ( $rStreamID ) . '_*.ts' );
if ( file_exists ( STREAMS_PATH . $rStreamID . '_.pid' )) {
unlink ( STREAMS_PATH . $rStreamID . '_.pid' );
}
$rSources = ( $rForceSource ? array ( $rForceSource ) : json_decode ( $rStreamInfo [ 'stream_source' ], true ));
$rArgumentMap = array ();
foreach ( $rStreamArguments as $rStreamArgument ) {
$rArgumentMap [ $rStreamArgument [ 'argument_key' ]] = array ( 'value' => $rStreamArgument [ 'value' ], 'argument_default_value' => $rStreamArgument [ 'argument_default_value' ]);
}
2026-03-14 23:57:33 +03:00
shell_exec ( PHP_BIN . ' ' . MAIN_HOME . 'console.php llod ' . intval ( $rStreamID ) . ' "' . base64_encode ( json_encode ( $rSources )) . '" "' . base64_encode ( json_encode ( $rArgumentMap )) . '" >/dev/null 2>/dev/null & echo $! > ' . STREAMS_PATH . intval ( $rStreamID ) . '_.pid' );
2026-02-21 19:59:51 +03:00
$rPID = intval ( file_get_contents ( STREAMS_PATH . $rStreamID . '_.pid' ));
$rKey = openssl_random_pseudo_bytes ( 16 );
file_put_contents ( STREAMS_PATH . $rStreamID . '_.key' , $rKey );
$rIVSize = openssl_cipher_iv_length ( 'AES-128-CBC' );
$rIV = openssl_random_pseudo_bytes ( $rIVSize );
file_put_contents ( STREAMS_PATH . $rStreamID . '_.iv' , $rIV );
$db -> query ( 'UPDATE `streams_servers` SET `delay_available_at` = ?,`to_analyze` = 0,`stream_started` = ?,`stream_info` = ?,`stream_status` = 2,`pid` = ?,`progress_info` = ?,`current_source` = ? WHERE `stream_id` = ? AND `server_id` = ?' , null , time (), null , $rPID , json_encode ( array ()), $rSources [ 0 ], $rStreamID , SERVER_ID );
2026-03-07 21:53:45 +03:00
self :: updateStream ( $rStreamID );
2026-02-21 19:59:51 +03:00
return array ( 'main_pid' => $rPID , 'stream_source' => $rSources [ 0 ], 'delay_enabled' => false , 'parent_id' => 0 , 'delay_start_at' => null , 'playlist' => STREAMS_PATH . $rStreamID . '_.m3u8' , 'transcode' => false , 'offset' => 0 );
}
2026-06-22 11:42:26 +03:00
/**
* Start a live stream (main entry point for channel start-up).
*
* Selects a source, builds the ffmpeg command and launches the process.
*
* @param int $rStreamID Stream id.
* @param bool $rFromCache Use cached stream info.
* @param string|null $rForceSource Force a specific source URL.
* @param bool $rLLOD Treat as live-on-demand.
* @param int $rStartPos Start position/offset.
* @return mixed Start result.
*/
2026-03-07 21:53:45 +03:00
public static function startStream ( $rStreamID , $rFromCache = false , $rForceSource = null , $rLLOD = false , $rStartPos = 0 ) {
2026-06-15 18:07:36 +03:00
global $rSettings , $rServers , $rFFMPEG_CPU , $rFFMPEG_GPU , $rFFPROBE ;
$db = self :: db ();
2026-03-07 21:53:45 +03:00
$rSegmentSettings = array ( 'seg_time' => intval ( $rSettings [ 'seg_time' ]), 'seg_list_size' => intval ( $rSettings [ 'seg_list_size' ]), 'seg_delete_threshold' => intval ( $rSettings [ 'seg_delete_threshold' ]));
2026-07-30 19:47:05 +03:00
@ unlink ( STREAMS_PATH . $rStreamID . '_.pid' );
2026-02-21 19:59:51 +03:00
$rStream = array ();
$db -> query ( 'SELECT * FROM `streams` t1 INNER JOIN `streams_types` t2 ON t2.type_id = t1.type AND t2.live = 1 LEFT JOIN `profiles` t4 ON t1.transcode_profile_id = t4.profile_id WHERE t1.direct_source = 0 AND t1.id = ?' , $rStreamID );
if ( $db -> num_rows () > 0 ) {
$rStream [ 'stream_info' ] = $db -> get_row ();
$db -> query ( 'SELECT * FROM `streams_servers` WHERE stream_id = ? AND `server_id` = ?' , $rStreamID , SERVER_ID );
if ( $db -> num_rows () > 0 ) {
$rStream [ 'server_info' ] = $db -> get_row ();
$db -> query ( 'SELECT t1.*, t2.* FROM `streams_options` t1, `streams_arguments` t2 WHERE t1.stream_id = ? AND t1.argument_id = t2.id' , $rStreamID );
$rStream [ 'stream_arguments' ] = $db -> get_rows ();
if ( $rStream [ 'server_info' ][ 'on_demand' ] == 1 ) {
2026-04-29 20:03:23 +03:00
$rProbesize = intval ( $rStream [ 'stream_info' ][ 'probesize_ondemand' ]) ?: 1000000 ;
$rAnalyseDuration = ( $rLLOD ? '500000' : '10000000' );
2026-02-21 19:59:51 +03:00
} else {
$rAnalyseDuration = abs ( intval ( $rSettings [ 'stream_max_analyze' ]));
$rProbesize = abs ( intval ( $rSettings [ 'probesize' ]));
}
$rTimeout = intval ( $rAnalyseDuration / 1000000 ) + $rSettings [ 'probe_extra_wait' ];
$rFFProbee = 'timeout ' . $rTimeout . ' ' . $rFFPROBE . ' {FETCH_OPTIONS} -probesize ' . $rProbesize . ' -analyzeduration ' . $rAnalyseDuration . ' {CONCAT} -i {STREAM_SOURCE} -v quiet -print_format json -show_streams -show_format' ;
2026-03-09 00:03:18 +03:00
$rFetchOptions = '' ;
2026-02-21 19:59:51 +03:00
$rLoopback = false ;
$rOffset = 0 ;
if ( ! $rStream [ 'server_info' ][ 'parent_id' ]) {
if ( $rStream [ 'stream_info' ][ 'type_key' ] == 'created_live' ) {
$rSources = array ( CREATED_PATH . $rStreamID . '_.list' );
if ( $rStartPos > 0 ) {
$rCCOutput = array ();
$rCCDuration = array ();
2026-07-10 19:19:25 +03:00
$rCCInfo = json_decode ( $rStream [ 'server_info' ][ 'cc_info' ], true ) ?: array ();
2026-02-21 19:59:51 +03:00
foreach ( $rCCInfo as $rItem ) {
$rCCDuration [ $rItem [ 'path' ]] = intval ( explode ( '.' , $rItem [ 'seconds' ])[ 0 ]);
}
$rTimer = 0 ;
$rValid = true ;
foreach ( explode ( " \n " , file_get_contents ( CREATED_PATH . $rStreamID . '_.list' )) as $rItem ) {
2026-07-10 19:19:25 +03:00
$rItemParts = explode ( "file '" , $rItem );
if ( ! isset ( $rItemParts [ 1 ])) {
continue ;
}
list ( $rPath ) = explode ( "'" , $rItemParts [ 1 ]);
2026-02-21 19:59:51 +03:00
if ( $rPath ) {
2026-07-10 19:19:25 +03:00
if ( ! empty ( $rCCDuration [ $rPath ])) {
2026-02-21 19:59:51 +03:00
$rDuration = $rCCDuration [ $rPath ];
if ( $rTimer <= $rStartPos && $rStartPos < $rTimer + $rDuration ) {
$rOffset = $rTimer ;
$rCCOutput [] = $rPath ;
} else {
if ( $rStartPos < $rTimer + $rDuration ) {
$rCCOutput [] = $rPath ;
}
}
$rTimer += $rDuration ;
} else {
$rValid = false ;
}
}
}
if ( $rValid ) {
$rSources = array ( CREATED_PATH . $rStreamID . '_.tlist' );
$rTList = '' ;
foreach ( $rCCOutput as $rItem ) {
$rTList .= "file '" . $rItem . "'" . " \n " ;
}
file_put_contents ( CREATED_PATH . $rStreamID . '_.tlist' , $rTList );
}
}
} else {
$rSources = json_decode ( $rStream [ 'stream_info' ][ 'stream_source' ], true );
}
if ( count ( $rSources ) > 0 ) {
if ( ! empty ( $rForceSource )) {
$rSources = array ( $rForceSource );
} else {
if ( $rSettings [ 'priority_backup' ] != 1 ) {
if ( ! empty ( $rStream [ 'server_info' ][ 'current_source' ])) {
$k = array_search ( $rStream [ 'server_info' ][ 'current_source' ], $rSources );
if ( $k !== false ) {
$i = 0 ;
while ( $i <= $k ) {
$rTemp = $rSources [ $i ];
unset ( $rSources [ $i ]);
array_push ( $rSources , $rTemp );
$i ++ ;
}
$rSources = array_values ( $rSources );
}
}
}
}
}
} else {
$rLoopback = true ;
if ( $rStream [ 'server_info' ][ 'on_demand' ]) {
$rLLOD = true ;
}
$rLoopURL = ( ! is_null ( $rServers [ SERVER_ID ][ 'private_url_ip' ]) && ! is_null ( $rServers [ $rStream [ 'server_info' ][ 'parent_id' ]][ 'private_url_ip' ]) ? $rServers [ $rStream [ 'server_info' ][ 'parent_id' ]][ 'private_url_ip' ] : $rServers [ $rStream [ 'server_info' ][ 'parent_id' ]][ 'public_url_ip' ]);
$rSources = array ( $rLoopURL . 'admin/live?stream=' . intval ( $rStreamID ) . '&password=' . urlencode ( $rSettings [ 'live_streaming_pass' ]) . '&extension=ts' );
}
if ( $rStream [ 'stream_info' ][ 'type_key' ] == 'created_live' && file_exists ( CREATED_PATH . $rStreamID . '_.info' )) {
$db -> query ( 'UPDATE `streams_servers` SET `cc_info` = ? WHERE `server_id` = ? AND `stream_id` = ?;' , file_get_contents ( CREATED_PATH . $rStreamID . '_.info' ), SERVER_ID , $rStreamID );
}
if ( ! $rFromCache ) {
self :: deleteCache ( $rSources );
}
foreach ( $rSources as $rSource ) {
$rProcessed = false ;
$rRealSource = $rSource ;
2026-03-07 21:53:45 +03:00
$rStreamSource = StreamUtils :: parseStreamURL ( $rSource );
2026-02-21 19:59:51 +03:00
echo 'Checking source: ' . $rSource . " \n " ;
$rURLInfo = parse_url ( $rStreamSource );
2026-03-07 21:53:45 +03:00
$rIsXC_VM = ( $rLoopback ? true : StreamUtils :: detectXC_VM ( $rStreamSource ));
2026-02-21 19:59:51 +03:00
if ( $rIsXC_VM && ! $rLoopback && $rSettings [ 'send_xc_vm_header' ]) {
foreach ( array_keys ( $rStream [ 'stream_arguments' ]) as $rID ) {
if ( $rStream [ 'stream_arguments' ][ $rID ][ 'argument_key' ] == 'headers' ) {
$rStream [ 'stream_arguments' ][ $rID ][ 'value' ] .= " \r\n " . 'X-XC_VM-Detect:1' ;
$rProcessed = true ;
}
}
if ( ! $rProcessed ) {
$rStream [ 'stream_arguments' ][] = array ( 'value' => 'X-XC_VM-Detect:1' , 'argument_key' => 'headers' , 'argument_cat' => 'fetch' , 'argument_wprotocol' => 'http' , 'argument_type' => 'text' , 'argument_cmd' => "-headers '%s" . " \r\n " . "'" );
}
}
$rProbeArguments = $rStream [ 'stream_arguments' ];
if ( $rIsXC_VM && $rStream [ 'server_info' ][ 'on_demand' ] == 1 && $rSettings [ 'request_prebuffer' ] == 1 ) {
foreach ( array_keys ( $rStream [ 'stream_arguments' ]) as $rID ) {
if ( $rStream [ 'stream_arguments' ][ $rID ][ 'argument_key' ] == 'headers' ) {
$rStream [ 'stream_arguments' ][ $rID ][ 'value' ] .= " \r\n " . 'X-XC_VM-Prebuffer:1' ;
$rProcessed = true ;
}
}
if ( ! $rProcessed ) {
$rStream [ 'stream_arguments' ][] = array ( 'value' => 'X-XC_VM-Prebuffer:1' , 'argument_key' => 'headers' , 'argument_cat' => 'fetch' , 'argument_wprotocol' => 'http' , 'argument_type' => 'text' , 'argument_cmd' => "-headers '%s" . " \r\n " . "'" );
}
}
foreach ( array_keys ( $rProbeArguments ) as $rID ) {
if ( $rProbeArguments [ $rID ][ 'argument_key' ] == 'headers' ) {
$rProbeArguments [ $rID ][ 'value' ] .= " \r\n " . 'X-XC_VM-Prebuffer:1' ;
$rProcessed = true ;
}
}
if ( ! $rProcessed ) {
$rProbeArguments [] = array ( 'value' => 'X-XC_VM-Prebuffer:1' , 'argument_key' => 'headers' , 'argument_cat' => 'fetch' , 'argument_wprotocol' => 'http' , 'argument_type' => 'text' , 'argument_cmd' => "-headers '%s" . " \r\n " . "'" );
}
$rProtocol = strtolower ( substr ( $rStreamSource , 0 , strpos ( $rStreamSource , '://' )));
2026-03-07 21:53:45 +03:00
$rProbeOptions = implode ( ' ' , StreamUtils :: getArguments ( $rProbeArguments , $rProtocol , 'fetch' ));
$rFetchOptions = implode ( ' ' , StreamUtils :: getArguments ( $rStream [ 'stream_arguments' ], $rProtocol , 'fetch' ));
2026-02-21 19:59:51 +03:00
$rSkipFFProbe = false ;
foreach ( $rStream [ 'stream_arguments' ] as $rArg ) {
if ( $rArg [ 'argument_key' ] == 'skip_ffprobe' && $rArg [ 'value' ] == 1 ) {
$rSkipFFProbe = true ;
break ;
}
}
if ( $rSkipFFProbe ) {
$rFFProbeOutput = array (
'codecs' => array (
'video' => array ( 'codec_name' => 'h264' , 'codec_type' => 'video' , 'height' => 1080 ),
'audio' => array ( 'codec_name' => 'aac' , 'codec_type' => 'audio' )
),
'container' => 'mpegts'
);
error_log ( '[XC_VM] Stream ' . $rStreamID . ': FFProbe skipped' );
echo 'Got stream information via skip_ffprobe (assumed h264/aac)' . " \n " ;
if ( empty ( $rSource )) {
$rSource = is_array ( $rSources ) && count ( $rSources ) > 0 ? $rSources [ 0 ] : $rStreamSource ;
}
break ;
}
if ( $rFromCache && file_exists ( CACHE_TMP_PATH . md5 ( $rSource )) && time () - filemtime ( CACHE_TMP_PATH . md5 ( $rSource )) <= 300 ) {
$rFFProbeOutput = igbinary_unserialize ( file_get_contents ( CACHE_TMP_PATH . md5 ( $rStreamSource )));
if ( $rFFProbeOutput && ( isset ( $rFFProbeOutput [ 'streams' ]) || isset ( $rFFProbeOutput [ 'codecs' ]))) {
echo 'Got stream information via cache' . " \n " ;
break ;
}
} else {
if ( $rFromCache && file_exists ( CACHE_TMP_PATH . md5 ( $rSource ))) {
$rFromCache = false ;
}
}
if ( ! ( $rStream [ 'server_info' ][ 'on_demand' ] && $rLLOD )) {
if ( $rIsXC_VM && $rSettings [ 'api_probe' ]) {
2026-07-08 12:17:54 +03:00
$rProbeURL = $rURLInfo [ 'scheme' ] . '://' . $rURLInfo [ 'host' ] . ( isset ( $rURLInfo [ 'port' ]) ? ':' . $rURLInfo [ 'port' ] : '' ) . '/probe/' . base64_encode ( $rURLInfo [ 'path' ] ?? '' );
2026-03-07 21:53:45 +03:00
$rFFProbeOutput = json_decode ( CurlClient :: getURL ( $rProbeURL ), true );
2026-02-21 19:59:51 +03:00
if ( $rFFProbeOutput && isset ( $rFFProbeOutput [ 'codecs' ])) {
echo 'Got stream information via API' . " \n " ;
break ;
}
}
$rProbeCmd = str_replace ( array ( '{FETCH_OPTIONS}' , '{CONCAT}' , '{STREAM_SOURCE}' ), array ( $rProbeOptions , ( $rStream [ 'stream_info' ][ 'type_key' ] == 'created_live' && ! $rStream [ 'server_info' ][ 'parent_id' ] ? '-safe 0 -f concat' : '' ), escapeshellarg ( $rStreamSource )), $rFFProbee );
$rFFProbeOutput = json_decode ( shell_exec ( $rProbeCmd ), true );
if ( $rFFProbeOutput && isset ( $rFFProbeOutput [ 'streams' ])) {
echo 'Got stream information via ffprobe' . " \n " ;
break ;
}
}
}
if ( ! ( $rStream [ 'server_info' ][ 'on_demand' ] && $rLLOD )) {
if ( ! isset ( $rFFProbeOutput [ 'codecs' ])) {
2026-06-25 18:55:35 +03:00
$rFFProbeOutput = \XcVm\Streaming\Codec\FFprobeRunner :: parseFFProbe ( $rFFProbeOutput );
2026-02-21 19:59:51 +03:00
}
if ( empty ( $rFFProbeOutput )) {
$db -> query ( "UPDATE `streams_servers` SET `progress_info` = '',`to_analyze` = 0,`pid` = -1,`stream_status` = 1 WHERE `server_id` = ? AND `stream_id` = ?" , SERVER_ID , $rStreamID );
return 0 ;
}
if ( ! $rFromCache ) {
file_put_contents ( CACHE_TMP_PATH . md5 ( $rSource ), igbinary_serialize ( $rFFProbeOutput ));
}
}
$externalPushJson = $rStream [ 'stream_info' ][ 'external_push' ] ?? '[]' ;
$rExternalPush = json_decode ( $externalPushJson , true );
2026-08-01 23:35:05 +03:00
// ffmpeg writes progress reports to this local file; StreamsCronJob tails it.
2026-08-07 16:54:08 +03:00
// (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 ' : '' );
2026-02-21 19:59:51 +03:00
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' ]) {
2026-03-09 00:03:18 +03:00
$rFFMPEG_CPU = FFMPEG_BIN_40 ;
2026-02-21 19:59:51 +03:00
$rFFPROBE = FFPROBE_BIN_40 ;
}
2026-03-09 00:03:18 +03:00
$rNoFix = ( $rFFMPEG_CPU == FFMPEG_BIN_40 ? '-nofix_dts' : '' );
2026-02-21 19:59:51 +03:00
$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 ) {
2026-07-08 12:17:54 +03:00
$rStream [ 'stream_info' ][ 'transcode_attributes' ] = array_merge ( StreamUtils :: getArguments ( $rStream [ 'stream_arguments' ], $rProtocol , 'transcode' ), json_decode (( string ) $rStream [ 'stream_info' ][ 'transcode_attributes' ], true ) ?: array ());
2026-02-21 19:59:51 +03:00
} else {
2026-07-08 12:17:54 +03:00
$rStream [ 'stream_info' ][ 'transcode_attributes' ] = json_decode (( string ) $rStream [ 'stream_info' ][ 'profile_options' ], true ) ?: array ();
2026-02-21 19:59:51 +03:00
}
} else {
$rStream [ 'stream_info' ][ 'transcode_attributes' ] = array ();
}
2026-08-01 23:35:05 +03:00
$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 ' ;
2026-02-21 19:59:51 +03:00
2026-08-07 18:51:07 +03:00
self :: applyDefaultCopyCodecs ( $rStream [ 'stream_info' ][ 'transcode_attributes' ]);
2026-02-21 19:59:51 +03:00
if ( ! array_key_exists ( '-scodec' , $rStream [ 'stream_info' ][ 'transcode_attributes' ])) {
$rStream [ 'stream_info' ][ 'transcode_attributes' ][ '-sn' ] = '' ;
}
} else {
$rStream [ 'stream_info' ][ 'transcode_attributes' ] = array ();
2026-08-01 23:35:05 +03:00
$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' ];
2026-02-21 19:59:51 +03:00
}
2026-06-28 14:26:14 +03:00
$rLLODOptions = ( $rLLOD && ! $rLoopback ? '-tune zerolatency -fflags nobuffer -flags low_delay -strict experimental -threads 0' : '' );
2026-02-21 19:59:51 +03:00
$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' : '' );
2026-06-26 19:52:54 +03:00
// 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' ]));
2026-08-07 19:31:18 +03:00
$rOutputs [ 'mpegts' ][] = self :: buildHlsMpegtsOutput ( $rOptions , $rSegmentSettings , $rKeyFrames , $rInitTime , $rStreamID );
2026-02-21 19:59:51 +03:00
if ( $rStream [ 'stream_info' ][ 'rtmp_output' ] == 1 ) {
2026-08-07 19:31:18 +03:00
$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' ]));
2026-02-21 19:59:51 +03:00
}
if ( ! empty ( $rExternalPush [ SERVER_ID ])) {
foreach ( $rExternalPush [ SERVER_ID ] as $rPushURL ) {
2026-08-07 19:31:18 +03:00
$rOutputs [ 'flv' ][] = self :: buildFlvOutput ( $rFLVOptions , escapeshellarg ( $rPushURL ));
2026-02-21 19:59:51 +03:00
}
}
2026-08-07 18:51:07 +03:00
$rLogoOptions = self :: buildLogoFilterOptions ( $rStream [ 'stream_info' ][ 'transcode_attributes' ], $rLoopback );
2026-02-21 19:59:51 +03:00
$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' ;
}
if ( 0 >= $rStream [ 'stream_info' ][ 'delay_minutes' ] || $rStream [ 'server_info' ][ 'parent_id' ]) {
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' ]) . ' ' ;
}
2026-03-07 21:53:45 +03:00
$rFFMPEG .= implode ( ' ' , StreamUtils :: parseTranscode ( $rStream [ 'stream_info' ][ 'transcode_attributes' ])) . ' ' ;
2026-02-21 19:59:51 +03:00
$rFFMPEG .= $rOutputCommand ;
}
}
} else {
$rSegmentStart = 0 ;
$m3u8File = DELAY_PATH . $rStreamID . '_.m3u8' ;
$oldM3u8File = DELAY_PATH . intval ( $rStreamID ) . '_.m3u8_old' ;
if ( file_exists ( $m3u8File )) {
$rFile = file ( $m3u8File , FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
if ( ! is_array ( $rFile ) || count ( $rFile ) < 2 ) {
return ;
}
$lastLine = $rFile [ count ( $rFile ) - 1 ];
$prevLine = $rFile [ count ( $rFile ) - 2 ];
if ( stristr ( $lastLine , $rStreamID . '_' )) {
if ( preg_match ( '/_(.*?)\.ts/' , $lastLine , $rMatches )) {
$rSegmentStart = intval ( $rMatches [ 1 ]) + 1 ;
}
} else {
if ( preg_match ( '/_(.*?)\.ts/' , $prevLine , $rMatches )) {
$rSegmentStart = intval ( $rMatches [ 1 ]) + 1 ;
}
}
if ( file_exists ( $oldM3u8File )) {
file_put_contents ( $oldM3u8File , file_get_contents ( $oldM3u8File ) . file_get_contents ( $m3u8File ));
shell_exec ( "sed -i '/EXTINF \\ |.ts/!d' " . escapeshellarg ( $oldM3u8File ));
} else {
copy ( $m3u8File , $oldM3u8File );
}
}
2026-03-07 21:53:45 +03:00
$rFFMPEG .= implode ( ' ' , StreamUtils :: parseTranscode ( $rStream [ 'stream_info' ][ 'transcode_attributes' ])) . ' ' ;
2026-02-21 19:59:51 +03:00
$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" ' ;
$rSleepTime = $rStream [ 'stream_info' ][ 'delay_minutes' ] * 60 ;
if ( $rSegmentStart > 0 ) {
$rSleepTime -= ( $rSegmentStart - 1 ) * 10 ;
if ( $rSleepTime > 0 ) {
} else {
$rSleepTime = 0 ;
}
}
}
$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' : '' ),
( ! stristr ( $ffprobeContainer , 'flv' ) && $audioCodec === 'aac' && ( $rStream [ 'stream_info' ][ 'transcode_attributes' ][ '-acodec' ] ?? '' ) === 'copy' ? '-bsf:a aac_adtstoasc' : '' ),
$rGPUOptions ,
$rInputCodec ,
$rLogoOptions ,
$rLLODOptions
],
$rFFMPEG
);
shell_exec ( $rFFMPEG );
file_put_contents ( STREAMS_PATH . $rStreamID . '_.ffmpeg' , $rFFMPEG );
$rKey = openssl_random_pseudo_bytes ( 16 );
file_put_contents ( STREAMS_PATH . $rStreamID . '_.key' , $rKey );
$rIVSize = openssl_cipher_iv_length ( 'AES-128-CBC' );
$rIV = openssl_random_pseudo_bytes ( $rIVSize );
file_put_contents ( STREAMS_PATH . $rStreamID . '_.iv' , $rIV );
2026-06-12 14:39:49 +03:00
// Wait briefly for PID file to be written, with retry
$rPID = 0 ;
$rPIDRetries = 0 ;
while ( $rPIDRetries < 10 ) {
usleep ( 50000 ); // 50ms
if ( file_exists ( STREAMS_PATH . $rStreamID . '_.pid' )) {
$rPID = intval ( file_get_contents ( STREAMS_PATH . $rStreamID . '_.pid' ));
if ( $rPID > 0 ) {
break ;
}
}
$rPIDRetries ++ ;
}
2026-02-21 19:59:51 +03:00
if ( $rStream [ 'stream_info' ][ 'tv_archive_server_id' ] == SERVER_ID ) {
2026-03-14 23:57:33 +03:00
shell_exec ( PHP_BIN . ' ' . MAIN_HOME . 'console.php archive ' . intval ( $rStreamID ) . ' >/dev/null 2>/dev/null & echo $!' );
2026-02-21 19:59:51 +03:00
}
if ( $rStream [ 'stream_info' ][ 'vframes_server_id' ] == SERVER_ID ) {
self :: startThumbnail ( $rStreamID );
}
$rDelayEnabled = 0 < $rStream [ 'stream_info' ][ 'delay_minutes' ] && ! $rStream [ 'server_info' ][ 'parent_id' ];
$rDelayStartAt = ( $rDelayEnabled ? time () + $rSleepTime : 0 );
if ( $rStream [ 'stream_info' ][ 'enable_transcode' ]) {
$rFFProbeOutput = array ();
}
$rCompatible = 0 ;
$rAudioCodec = $rVideoCodec = $rResolution = null ;
if ( isset ( $rFFProbeOutput ) && is_array ( $rFFProbeOutput ) && isset ( $rFFProbeOutput [ 'codecs' ]) && is_array ( $rFFProbeOutput [ 'codecs' ])) {
2026-03-07 21:53:45 +03:00
$rCompatible = intval ( DiagnosticsService :: checkCompatibility ( $rFFProbeOutput , SettingsManager :: getAll ()[ 'player_allow_hevc' ]));
2026-04-05 22:09:22 +03:00
$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 ;
2026-02-21 19:59:51 +03:00
if ( $rResolution ) {
2026-03-07 21:53:45 +03:00
$rResolution = StreamSorter :: getNearest ( array ( 240 , 360 , 480 , 576 , 720 , 1080 , 1440 , 2160 ), $rResolution );
2026-02-21 19:59:51 +03:00
}
}
$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 );
2026-03-07 21:53:45 +03:00
self :: updateStream ( $rStreamID );
2026-02-21 19:59:51 +03:00
$rPlaylist = ( ! $rDelayEnabled ? STREAMS_PATH . $rStreamID . '_.m3u8' : DELAY_PATH . $rStreamID . '_.m3u8' );
return array ( 'main_pid' => $rPID , 'stream_source' => $rRealSource , 'delay_enabled' => $rDelayEnabled , 'parent_id' => $rStream [ 'server_info' ][ 'parent_id' ], 'delay_start_at' => $rDelayStartAt , 'playlist' => $rPlaylist , 'transcode' => $rStream [ 'stream_info' ][ 'enable_transcode' ], 'offset' => $rOffset );
} else {
return false ;
}
} else {
return false ;
}
}
}