Files
XC_VM/src/Cli/Commands/ScannerCommand.php
T
Divarion_D 84dfecb33f fix(stream): send RFC-compliant X-XC_VM-* probe/scan headers
The probe endpoint and the source scanner injected the prebuffer/detect
markers with a stray backslash in the header field-name
(`X-\XC_VM-Prebuffer` / `X-\XC_VM-Detect`), a leftover from the namespace
migration. A backslash is not a valid HTTP token character, so strict
origins/CDNs (e.g. Akamai) answer 400 Bad Request and the probe fails
silently (ffprobe runs with `-v quiet`), breaking Load Maps and stream
checks for otherwise-valid sources.

Use the correct `X-XC_VM-Prebuffer` / `X-XC_VM-Detect` names, matching what
StreamProcess sends and what auth.php/live.php read back.
2026-08-23 16:03:21 +03:00

216 lines
8.4 KiB
PHP

<?php
namespace XcVm\Cli\Commands;
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;
use XcVm\Streaming\Codec\FfmpegPaths;
use XcVm\Streaming\Codec\FFprobeRunner;
/**
* ScannerCommand — scanner command
*
* @package XC_VM_CLI_Commands
* @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
*/
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;
}
$rStreamArguments[$rID]['value'] .= "\r\n" . 'X-XC_VM-Detect:1';
$rProcessed = true;
}
if (!$rProcessed) {
$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" . "'");
}
}
if ($rIsXC_VM && SettingsManager::getAll()['request_prebuffer'] == 1) {
$rProcessed = false;
foreach (array_keys($rStreamArguments) as $rID) {
if ($rStreamArguments[$rID]['argument_key'] != 'headers') {
continue;
}
$rStreamArguments[$rID]['value'] .= "\r\n" . 'X-XC_VM-Prebuffer:1';
$rProcessed = true;
}
if (!$rProcessed) {
$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" . "'");
}
}
$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');
}
@unlink(STREAMS_TMP_PATH . $rRow['id'] . '._errors');
}
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";
}
}
}