2026-03-14 23:57:33 +03:00
<? php
2026-06-25 19:03:39 +03:00
namespace XcVm\Cli\Commands ;
2026-06-25 20:24:36 +03:00
use XcVm\Cli\CommandInterface ;
use XcVm\Cli\DaemonTrait ;
use XcVm\Core\Config\SettingsManager ;
use XcVm\Core\Config\SettingsRepository ;
use XcVm\Core\Database\Database ;
use XcVm\Core\Http\CurlClient ;
use XcVm\Core\Util\StreamUtils ;
use XcVm\Domain\Stream\StreamSorter ;
2026-06-25 18:55:35 +03:00
use XcVm\Streaming\Codec\FfmpegPaths ;
use XcVm\Streaming\Codec\FFprobeRunner ;
2026-06-25 20:24:36 +03:00
2026-04-05 22:38:36 +03:00
/**
* ScannerCommand — scanner command
*
* @package XC_VM_CLI_Commands
* @author Divarion_D <https://github.com/Divarion-D>
* @copyright 2025-2026 Vateron Media
2026-06-25 20:32:18 +03:00
* @link https://github.com/Vateron-Media/XC_VM
2026-04-05 22:38:36 +03:00
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
2026-03-14 23:57:33 +03:00
require_once __DIR__ . '/../DaemonTrait.php' ;
class ScannerCommand implements CommandInterface {
use DaemonTrait ;
public function getName () : string {
return 'scanner' ;
}
public function getDescription () : string {
return 'Daemon: scan on-demand sources' ;
}
public function execute ( array $rArgs ) : int {
if ( ! $this -> assertRunAsXcVm ()) {
return 1 ;
}
global $db ;
$this -> setProcessTitle ( 'XC_VM[Scanner]' );
$this -> killStaleProcesses ( 'console.php scanner' );
$this -> initDaemonMD5 ();
if ( ! SettingsManager :: getAll ()[ 'on_demand_checker' ]) {
echo "On-Demand - Source Scanner is disabled. \n " ;
return 0 ;
}
$this -> rRefreshInterval = 60 ;
while ( true && $db && $db -> ping ()) {
if ( ! $this -> shouldRefreshSettings ()) {
// skip
} else {
if ( $this -> hasFileChanged ()) {
echo "File changed! Break. \n " ;
break ;
}
SettingsManager :: set ( SettingsRepository :: getAll ( true ));
$this -> rLastCheck = time ();
}
$this -> scanOnDemandStreams ( $db );
sleep ( 60 );
break ;
}
if ( is_object ( $db )) {
$db -> close_mysql ();
}
$this -> restartDaemon ( 'scanner' );
return 0 ;
}
private function scanOnDemandStreams ( Database $db ) : void {
$rScanTime = SettingsManager :: getAll ()[ 'on_demand_scan_time' ] ?: 3600 ;
if ( ! $db -> query ( 'SELECT `streams`.* FROM `streams` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `streams_servers`.`pid` IS NULL AND `streams_servers`.`on_demand` = 1 AND `streams_servers`.`parent_id` IS NULL AND `streams`.`type` = 1 AND `streams`.`direct_source` = 0 AND `streams_servers`.`server_id` = ? AND (UNIX_TIMESTAMP() - (SELECT MAX(`date`) FROM `ondemand_check` WHERE `stream_id` = `streams`.`id` AND `server_id` = `streams_servers`.`server_id`) > ? OR (SELECT MAX(`date`) FROM `ondemand_check` WHERE `stream_id` = `streams`.`id` AND `server_id` = `streams_servers`.`server_id`) IS NULL);' , SERVER_ID , $rScanTime )) {
return ;
}
if ( $db -> num_rows () <= 0 ) {
return ;
}
foreach ( $db -> get_rows () as $rRow ) {
echo '[' . $rRow [ 'id' ] . '] - ' . $rRow [ 'stream_display_name' ] . " \n " ;
$db -> query ( 'SELECT t1.*, t2.* FROM `streams_options` t1, `streams_arguments` t2 WHERE t1.stream_id = ? AND t1.argument_id = t2.id' , $rRow [ 'id' ]);
$rStreamArguments = $db -> get_rows ();
$rProbesize = ( intval ( $rRow [ 'probesize_ondemand' ]) ?: 512000 );
$rAnalyseDuration = '10000000' ;
$rTimeout = intval ( $rAnalyseDuration / 1000000 ) + SettingsManager :: getAll ()[ 'probe_extra_wait' ];
if ( SettingsManager :: getAll ()[ 'on_demand_max_probe' ] < $rTimeout && 0 < SettingsManager :: getAll ()[ 'on_demand_max_probe' ]) {
$rTimeout = intval ( SettingsManager :: getAll ()[ 'on_demand_max_probe' ]);
}
$rFFProbee = 'timeout ' . $rTimeout . ' ' . FfmpegPaths :: probe () . ' {FETCH_OPTIONS} -probesize ' . $rProbesize . ' -analyzeduration ' . $rAnalyseDuration . ' -i {STREAM_SOURCE} -loglevel error -print_format json -show_streams -show_format 2>' . STREAMS_TMP_PATH . $rRow [ 'id' ] . '._errors' ;
$rSources = json_decode ( $rRow [ 'stream_source' ], true );
$rSourceID = 0 ;
$rErrors = null ;
$rFFProbeOutput = null ;
$rTimeTaken = null ;
foreach ( $rSources as $rSource ) {
$rProcessed = false ;
$rRealSource = $rSource ;
$rStreamSource = StreamUtils :: parseStreamURL ( $rSource );
echo 'Checking source: ' . $rSource . " \n " ;
$rURLInfo = parse_url ( $rStreamSource );
$rIsXC_VM = StreamUtils :: detectXC_VM ( $rStreamSource );
if ( $rIsXC_VM && SettingsManager :: getAll ()[ 'send_xc_vm_header' ]) {
foreach ( array_keys ( $rStreamArguments ) as $rID ) {
if ( $rStreamArguments [ $rID ][ 'argument_key' ] != 'headers' ) {
continue ;
}
2026-08-23 16:03:21 +03:00
$rStreamArguments [ $rID ][ 'value' ] .= " \r\n " . 'X-XC_VM-Detect:1' ;
2026-03-14 23:57:33 +03:00
$rProcessed = true ;
}
if ( ! $rProcessed ) {
2026-08-23 16:03:21 +03:00
$rStreamArguments [] = 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 " . "'" );
2026-03-14 23:57:33 +03:00
}
}
if ( $rIsXC_VM && SettingsManager :: getAll ()[ 'request_prebuffer' ] == 1 ) {
$rProcessed = false ;
foreach ( array_keys ( $rStreamArguments ) as $rID ) {
if ( $rStreamArguments [ $rID ][ 'argument_key' ] != 'headers' ) {
continue ;
}
2026-08-23 16:03:21 +03:00
$rStreamArguments [ $rID ][ 'value' ] .= " \r\n " . 'X-XC_VM-Prebuffer:1' ;
2026-03-14 23:57:33 +03:00
$rProcessed = true ;
}
if ( ! $rProcessed ) {
2026-08-23 16:03:21 +03:00
$rStreamArguments [] = 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 " . "'" );
2026-03-14 23:57:33 +03:00
}
}
$rProtocol = strtolower ( substr ( $rStreamSource , 0 , strpos ( $rStreamSource , '://' )));
$rFetchOptions = implode ( ' ' , StreamUtils :: getArguments ( $rStreamArguments , $rProtocol , 'fetch' ));
if ( $rIsXC_VM && SettingsManager :: getAll ()[ 'api_probe' ]) {
$rProbeURL = $rURLInfo [ 'scheme' ] . '://' . $rURLInfo [ 'host' ] . ':' . $rURLInfo [ 'port' ] . '/probe/' . base64_encode ( $rURLInfo [ 'path' ]);
$rTime = round ( microtime ( true ) * 1000 );
$rFFProbeOutput = json_decode ( CurlClient :: getURL ( $rProbeURL ), true );
$rTimeTaken = round ( microtime ( true ) * 1000 ) - $rTime ;
if ( $rFFProbeOutput && isset ( $rFFProbeOutput [ 'streams' ])) {
echo "Got stream information via API \n " ;
break ;
}
}
$rTime = round ( microtime ( true ) * 1000 );
$rFFProbeOutput = json_decode ( shell_exec ( str_replace ( array ( '{FETCH_OPTIONS}' , '{STREAM_SOURCE}' ), array ( $rFetchOptions , escapeshellarg ( $rStreamSource )), $rFFProbee )), true );
$rTimeTaken = round ( microtime ( true ) * 1000 ) - $rTime ;
if ( file_exists ( STREAMS_TMP_PATH . $rRow [ 'id' ] . '._errors' ) && 0 < filesize ( STREAMS_TMP_PATH . $rRow [ 'id' ] . '._errors' )) {
if ( ! $rErrors && $rSourceID == 0 ) {
$rErrors = file_get_contents ( STREAMS_TMP_PATH . $rRow [ 'id' ] . '._errors' );
}
2026-07-30 19:47:04 +03:00
@ unlink ( STREAMS_TMP_PATH . $rRow [ 'id' ] . '._errors' );
2026-03-14 23:57:33 +03:00
}
if ( $rFFProbeOutput && isset ( $rFFProbeOutput [ 'streams' ])) {
echo "Got stream information via ffprobe \n " ;
break ;
}
if ( ! $rRow [ 'llod' ]) {
$rSourceID ++ ;
} else {
break ;
}
}
if ( ! empty ( $rFFProbeOutput )) {
echo "Source live! \n " ;
$rFFProbeOutput = FFprobeRunner :: parseFFProbe ( $rFFProbeOutput );
$rAudioCodec = ( $rFFProbeOutput [ 'codecs' ][ 'audio' ][ 'codec_name' ] ?: null );
$rVideoCodec = ( $rFFProbeOutput [ 'codecs' ][ 'video' ][ 'codec_name' ] ?: null );
$rResolution = ( $rFFProbeOutput [ 'codecs' ][ 'video' ][ 'height' ] ?: null );
$rVideoBitrate = ( $rFFProbeOutput [ 'codecs' ][ 'video' ][ 'bit_rate' ] ?: 0 );
$rAudioBitrate = ( $rFFProbeOutput [ 'codecs' ][ 'audio' ][ 'bit_rate' ] ?: 0 );
$rFPS = ( intval ( explode ( '/' , $rFFProbeOutput [ 'codecs' ][ 'video' ][ 'r_frame_rate' ])[ 0 ]) ?: 0 );
if ( $rFPS == 0 ) {
$rFPS = ( intval ( explode ( '/' , $rFFProbeOutput [ 'codecs' ][ 'video' ][ 'avg_frame_rate' ])[ 0 ]) ?: 0 );
}
if ( $rFPS >= 1000 ) {
$rFPS = intval ( $rFPS / 1000 );
}
if ( $rResolution ) {
$rResolution = StreamSorter :: getNearest ( array ( 240 , 360 , 480 , 576 , 720 , 1080 , 1440 , 2160 ), $rResolution );
}
$rStatus = 1 ;
} else {
echo "Source down! \n " ;
$rFPS = $rAudioCodec = $rVideoCodec = $rResolution = $rTimeTaken = null ;
$rSourceID = $rStatus = 0 ;
}
$rSource = $rSources [ $rSourceID ];
$db -> query ( 'INSERT INTO `ondemand_check`(`stream_id`, `server_id`, `status`, `source_id`, `source_url`, `fps`, `video_codec`, `audio_codec`, `resolution`, `response`, `errors`, `date`) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);' , $rRow [ 'id' ], SERVER_ID , $rStatus , $rSourceID , $rSource , $rFPS , $rVideoCodec , $rAudioCodec , $rResolution , $rTimeTaken , $rErrors , time ());
$db -> query ( 'UPDATE `streams_servers` SET `ondemand_check` = ? WHERE `stream_id` = ? AND `server_id` = ?;' , $db -> last_insert_id (), $rRow [ 'id' ], SERVER_ID );
echo " \n " ;
}
}
}