mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-18 04:02:59 +02:00
feat(ffmpeg): dynamic build discovery + capabilities, wire ffmpeg_gpu, drop 4.0 hardcoding
Discover the bundled FFmpeg builds from bin/ffmpeg_bin/<version>/ instead of a
hardcoded version list, and expose what each build can do.
- FfmpegBinaries (new): scans real version folders (skips *.backup.*), probes each
binary's hardware encoders (nvenc/qsv/vaapi/…), flags GPU capability, caches the
result to tmp/ffmpeg_capabilities.json keyed by a folder-mtime fingerprint. UI-only.
- FfmpegPaths::resolve() rewritten: builds paths dynamically from the version string
(is_file check, no shell — stays on the bootstrap hot path) and takes a separate GPU
version; the switch and the per-version constants are gone. Unknown CPU version falls
back to the legacy 4.0 build; an empty/invalid GPU version falls back to the CPU build.
- Wire the previously-dead ffmpeg_gpu column: LegacyInitializer passes it to resolve();
settings.php lists versions from the scanner and adds an ffmpeg_gpu <select> limited to
GPU-capable builds ("Same as CPU" = empty). No migration — the column already exists.
- Info tab: a per-build FFmpeg table (version, build banner, GPU badge, hw encoders).
- De-hardcode: drop the unused FFMPEG_BIN_71/80 + FFPROBE_BIN_71/80 constants; keep only
the 4.0 anchor (DTS override in StreamProcess + FfmpegPaths fallback). Llod/Loopback/
Record now use FfmpegPaths::cpu()/probe() (config-selected version) with the 4.0
constant as fallback, matching Scanner/Created/Thumbnail.
This commit is contained in:
@@ -56,8 +56,8 @@ class LlodCommand implements CommandInterface {
|
||||
if (!defined('STREAMS_PATH')) define('STREAMS_PATH', MAIN_HOME . 'content/streams/');
|
||||
if (!defined('CACHE_TMP_PATH')) define('CACHE_TMP_PATH', MAIN_HOME . 'tmp/cache/');
|
||||
if (!defined('CONS_TMP_PATH')) define('CONS_TMP_PATH', MAIN_HOME . 'tmp/opened_cons/');
|
||||
if (!defined('FFMPEG')) define('FFMPEG', MAIN_HOME . 'bin/ffmpeg_bin/4.0/ffmpeg');
|
||||
if (!defined('FFPROBE')) define('FFPROBE', MAIN_HOME . 'bin/ffmpeg_bin/4.0/ffprobe');
|
||||
if (!defined('FFMPEG')) define('FFMPEG', \XcVm\Streaming\Codec\FfmpegPaths::cpu() ?: FFMPEG_BIN_40);
|
||||
if (!defined('FFPROBE')) define('FFPROBE', \XcVm\Streaming\Codec\FfmpegPaths::probe() ?: FFPROBE_BIN_40);
|
||||
if (!defined('PACKET_SIZE')) define('PACKET_SIZE', 188);
|
||||
if (!defined('BUFFER_SIZE')) define('BUFFER_SIZE', 12032);
|
||||
if (!defined('TIMEOUT')) define('TIMEOUT', 20);
|
||||
|
||||
@@ -44,8 +44,8 @@ class LoopbackCommand implements CommandInterface {
|
||||
|
||||
if (!defined('MAIN_HOME')) define('MAIN_HOME', '/home/xc_vm/');
|
||||
if (!defined('STREAMS_PATH')) define('STREAMS_PATH', MAIN_HOME . 'content/streams/');
|
||||
if (!defined('FFMPEG')) define('FFMPEG', MAIN_HOME . 'bin/ffmpeg_bin/4.0/ffmpeg');
|
||||
if (!defined('FFPROBE')) define('FFPROBE', MAIN_HOME . 'bin/ffmpeg_bin/4.0/ffprobe');
|
||||
if (!defined('FFMPEG')) define('FFMPEG', \XcVm\Streaming\Codec\FfmpegPaths::cpu() ?: FFMPEG_BIN_40);
|
||||
if (!defined('FFPROBE')) define('FFPROBE', \XcVm\Streaming\Codec\FfmpegPaths::probe() ?: FFPROBE_BIN_40);
|
||||
if (!defined('CACHE_TMP_PATH')) define('CACHE_TMP_PATH', MAIN_HOME . 'tmp/cache/');
|
||||
if (!defined('CONFIG_PATH')) define('CONFIG_PATH', MAIN_HOME . 'config/');
|
||||
// PAT_HEADER restored to the real PAT header bytes (0xB0 0x0D) derived from the stream.
|
||||
|
||||
@@ -186,7 +186,7 @@ class RecordCommand implements CommandInterface {
|
||||
}
|
||||
|
||||
$rInsertID = $db->last_insert_id();
|
||||
shell_exec(FFMPEG_BIN_40 . " -i '" . ARCHIVE_PATH . $recordingID . '.ts' . "' -c:v copy -c:a copy '" . VOD_PATH . $rInsertID . '.mp4' . "'");
|
||||
shell_exec((\XcVm\Streaming\Codec\FfmpegPaths::cpu() ?: FFMPEG_BIN_40) . " -i '" . ARCHIVE_PATH . $recordingID . '.ts' . "' -c:v copy -c:a copy '" . VOD_PATH . $rInsertID . '.mp4' . "'");
|
||||
@unlink(ARCHIVE_PATH . $recordingID . '.ts');
|
||||
|
||||
if (!file_exists(VOD_PATH . $rInsertID . '.mp4')) {
|
||||
|
||||
@@ -24,12 +24,11 @@ define('GEOLITE2_BIN', BIN_PATH . 'maxmind/GeoLite2-Country.mmdb');
|
||||
define('GEOLITE2C_BIN', BIN_PATH . 'maxmind/GeoLite2-City.mmdb');
|
||||
define('GEOISP_BIN', BIN_PATH . 'maxmind/GeoIP2-ISP.mmdb');
|
||||
|
||||
// ── FFmpeg (по версиям) ───────────────────────────────────────
|
||||
define('FFMPEG_BIN_40', BIN_PATH . 'ffmpeg_bin/4.0/ffmpeg');
|
||||
define('FFMPEG_BIN_71', BIN_PATH . 'ffmpeg_bin/7.1/ffmpeg');
|
||||
define('FFMPEG_BIN_80', BIN_PATH . 'ffmpeg_bin/8.0/ffmpeg');
|
||||
|
||||
// ── FFprobe (по версиям) ──────────────────────────────────────
|
||||
// ── FFmpeg/FFprobe — legacy 4.0 "DTS" build anchor ────────────
|
||||
// Only the 4.0 build is pinned as a constant: it is the legacy/DTS-safe binary
|
||||
// (StreamProcess `-nofix_dts`, RecordCommand, and the FfmpegPaths fallback). Every
|
||||
// other version is resolved dynamically from bin/ffmpeg_bin/<version>/ by
|
||||
// FfmpegPaths (paths) and FfmpegBinaries (discovery + capabilities) — no more
|
||||
// per-version constants to keep in sync with what actually ships in the folder.
|
||||
define('FFMPEG_BIN_40', BIN_PATH . 'ffmpeg_bin/4.0/ffmpeg');
|
||||
define('FFPROBE_BIN_40', BIN_PATH . 'ffmpeg_bin/4.0/ffprobe');
|
||||
define('FFPROBE_BIN_71', BIN_PATH . 'ffmpeg_bin/7.1/ffprobe');
|
||||
define('FFPROBE_BIN_80', BIN_PATH . 'ffmpeg_bin/8.0/ffprobe');
|
||||
|
||||
@@ -67,7 +67,7 @@ class LegacyInitializer {
|
||||
SettingsManager::update('on_demand_wait_time', 15);
|
||||
}
|
||||
|
||||
\XcVm\Streaming\Codec\FfmpegPaths::resolve(SettingsManager::get('ffmpeg_cpu'));
|
||||
\XcVm\Streaming\Codec\FfmpegPaths::resolve(SettingsManager::get('ffmpeg_cpu'), SettingsManager::get('ffmpeg_gpu'));
|
||||
|
||||
if (!$rUseCache) {
|
||||
\XcVm\Domain\Server\ServerRepository::getAll();
|
||||
@@ -150,7 +150,7 @@ class LegacyInitializer {
|
||||
$GLOBALS['rSettings']['on_demand_wait_time'] = 15;
|
||||
}
|
||||
|
||||
\XcVm\Streaming\Codec\FfmpegPaths::resolve($GLOBALS['rSettings']['ffmpeg_cpu']);
|
||||
\XcVm\Streaming\Codec\FfmpegPaths::resolve($GLOBALS['rSettings']['ffmpeg_cpu'], $GLOBALS['rSettings']['ffmpeg_gpu'] ?? null);
|
||||
|
||||
$GLOBALS['rCached'] = \XcVm\Infrastructure\Cache\CacheReader::isReady($GLOBALS['rSettings']);
|
||||
$GLOBALS['rServers'] = \XcVm\Infrastructure\Cache\CacheReader::get('servers') ?: array();
|
||||
|
||||
@@ -6,7 +6,8 @@ use XcVm\Core\Config\SettingsManager;
|
||||
use XcVm\Core\GeoIP\MaxMindUpdater;
|
||||
use XcVm\Core\Localization\Translator;
|
||||
use XcVm\Core\Util\AdminHelpers;
|
||||
use XcVm\Domain\Stream\StreamConfigRepository;// Code reconstruction by Squallp
|
||||
use XcVm\Domain\Stream\StreamConfigRepository;
|
||||
use XcVm\Streaming\Codec\FfmpegBinaries;// Code reconstruction by Squallp
|
||||
|
||||
if (!isset($__settingsViewMode)):
|
||||
|
||||
@@ -1954,16 +1955,43 @@ $isAjaxRequest = (
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$rFfmpegGpuBuilds = FfmpegBinaries::gpuCapable();
|
||||
$rCpuOpts = array_keys(FfmpegBinaries::available()) ?: ["8.0", "7.1", "4.0"];
|
||||
if (!empty($rSettings["ffmpeg_cpu"]) && !in_array($rSettings["ffmpeg_cpu"], $rCpuOpts, true)) {
|
||||
$rCpuOpts[] = $rSettings["ffmpeg_cpu"];
|
||||
}
|
||||
$rGpuOpts = array_keys($rFfmpegGpuBuilds);
|
||||
if (!empty($rSettings["ffmpeg_gpu"]) && !in_array($rSettings["ffmpeg_gpu"], $rGpuOpts, true)) {
|
||||
$rGpuOpts[] = $rSettings["ffmpeg_gpu"];
|
||||
}
|
||||
?>
|
||||
<div class="form-group row mb-4">
|
||||
<label class="col-md-4 col-form-label" for="ffmpeg_cpu">
|
||||
FFMPEG Version
|
||||
FFMPEG Version (CPU)
|
||||
<i title="<?= $language::get('which_version_of_ffmpeg_to_tooltip') ?>" class="tooltip text-secondary far fa-circle"></i>
|
||||
</label>
|
||||
|
||||
<div class="col-md-2">
|
||||
<select name="ffmpeg_cpu" id="ffmpeg_cpu" class="form-control" data-toggle="select2">
|
||||
<?php foreach (["8.0", "7.1", "4.0"] as $rValue): ?>
|
||||
<?php foreach ($rCpuOpts as $rValue): ?>
|
||||
<option value="<?= $rValue ?>" <?= $rSettings["ffmpeg_cpu"] == $rValue ? ' selected' : '' ?>>
|
||||
v<?= $rValue ?><?= isset($rFfmpegGpuBuilds[$rValue]) ? ' (GPU)' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label class="col-md-4 col-form-label" for="ffmpeg_gpu">
|
||||
FFMPEG Version (GPU)
|
||||
<i title="<?= $language::get('which_version_of_ffmpeg_to_tooltip') ?>" class="tooltip text-secondary far fa-circle"></i>
|
||||
</label>
|
||||
|
||||
<div class="col-md-2">
|
||||
<select name="ffmpeg_gpu" id="ffmpeg_gpu" class="form-control" data-toggle="select2">
|
||||
<option value="" <?= empty($rSettings["ffmpeg_gpu"]) ? ' selected' : '' ?>>Same as CPU</option>
|
||||
<?php foreach ($rGpuOpts as $rValue): ?>
|
||||
<option value="<?= $rValue ?>" <?= ($rSettings["ffmpeg_gpu"] ?? '') === $rValue ? ' selected' : '' ?>>
|
||||
v<?= $rValue ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
@@ -2741,7 +2769,46 @@ $isAjaxRequest = (
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4 class="card-title mb-4"><?= $language::get('support_project') ?></h4>
|
||||
<h4 class="card-title mb-4">FFmpeg</h4>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th class="text-center" style="font-size: 0.85rem;">Version</th>
|
||||
<th class="text-center" style="font-size: 0.85rem;">Build</th>
|
||||
<th class="text-center" style="font-size: 0.85rem;">GPU</th>
|
||||
<th class="text-center" style="font-size: 0.85rem;">Hardware encoders</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<?php $rFfmpegBuilds = FfmpegBinaries::available(); ?>
|
||||
<?php if (empty($rFfmpegBuilds)): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center" style="font-size: 0.85rem;">N/A</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($rFfmpegBuilds as $rVer => $rInfo): ?>
|
||||
<tr>
|
||||
<td class="text-center" style="font-size: 0.85rem;">
|
||||
<button type="button" class="btn btn-info btn-sm" style="font-size: 0.85rem;">v<?= htmlspecialchars($rVer) ?></button>
|
||||
</td>
|
||||
<td class="text-center" style="font-size: 0.8rem;"><?= htmlspecialchars($rInfo['banner']) ?></td>
|
||||
<td class="text-center">
|
||||
<?php if ($rInfo['gpu']): ?>
|
||||
<span class="badge badge-success">GPU</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary">CPU</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-center text-monospace small"><?= $rInfo['encoders'] ? htmlspecialchars(implode(', ', $rInfo['encoders'])) : '—' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4 class="card-title mb-4"><?= $language::get('support_project') ?></h4>
|
||||
|
||||
<table class="table table-striped table-bordered text-center">
|
||||
<thead class="thead-light">
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace XcVm\Streaming\Codec;
|
||||
|
||||
/**
|
||||
* FfmpegBinaries — discovery + capability probe for the bundled FFmpeg builds.
|
||||
*
|
||||
* Scans BIN_PATH/ffmpeg_bin/<version>/ for real version directories (named
|
||||
* `<major>.<minor>`, so `*.backup.*` folders are ignored) that ship an
|
||||
* executable `ffmpeg`, and records per build: the version banner, whether it can
|
||||
* encode on the GPU, and the list of hardware encoders it exposes.
|
||||
*
|
||||
* Probing shells out to each binary (`-hwaccels`, `-encoders`), which is slow, so
|
||||
* the result is cached to a JSON file keyed by a fingerprint of the directory set
|
||||
* and their mtimes — a binary swap (e.g. by BinariesCommand) changes the mtime and
|
||||
* transparently invalidates the cache. This class is UI-only (settings page); the
|
||||
* streaming hot path resolves paths cheaply via {@see FfmpegPaths} without scanning.
|
||||
*
|
||||
* @package XC_VM_Streaming_Codec
|
||||
* @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
|
||||
*/
|
||||
class FfmpegBinaries {
|
||||
|
||||
/** Encoder-name substrings that mark a hardware (GPU/accelerated) encoder. */
|
||||
private const HW_ENCODER_MARKERS = ['_nvenc', 'nvenc_', '_qsv', '_vaapi', '_amf', '_videotoolbox', '_mf'];
|
||||
|
||||
/** @var array<string, array{version: string, gpu: bool, encoders: string[], banner: string}>|null */
|
||||
private static ?array $cache = null;
|
||||
|
||||
/** Base directory holding the per-version ffmpeg build folders. */
|
||||
private static function baseDir(): string {
|
||||
return BIN_PATH . 'ffmpeg_bin/';
|
||||
}
|
||||
|
||||
/** Absolute path to the capabilities cache file. */
|
||||
private static function cacheFile(): string {
|
||||
return (defined('TMP_PATH') ? TMP_PATH : sys_get_temp_dir() . '/') . 'ffmpeg_capabilities.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* All discovered builds, newest version first.
|
||||
*
|
||||
* @return array<string, array{version: string, gpu: bool, encoders: string[], banner: string}>
|
||||
* Keyed by version string (the folder name, e.g. '8.0').
|
||||
*/
|
||||
public static function available(): array {
|
||||
if (self::$cache !== null) {
|
||||
return self::$cache;
|
||||
}
|
||||
|
||||
$rFingerprint = self::fingerprint();
|
||||
$rCached = self::readCache();
|
||||
if ($rCached !== null && ($rCached['fingerprint'] ?? null) === $rFingerprint && isset($rCached['versions']) && is_array($rCached['versions'])) {
|
||||
return self::$cache = $rCached['versions'];
|
||||
}
|
||||
|
||||
$rVersions = self::scan();
|
||||
self::writeCache($rFingerprint, $rVersions);
|
||||
return self::$cache = $rVersions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only the builds that can encode on the GPU, newest first.
|
||||
*
|
||||
* @return array<string, array{version: string, gpu: bool, encoders: string[], banner: string}>
|
||||
*/
|
||||
public static function gpuCapable(): array {
|
||||
return array_filter(self::available(), static fn (array $rInfo): bool => $rInfo['gpu']);
|
||||
}
|
||||
|
||||
/** Whether a given version folder exists with an executable ffmpeg. */
|
||||
public static function has(string $rVersion): bool {
|
||||
return isset(self::available()[$rVersion]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cheap fingerprint of the build folders (names + mtimes) — no shelling out.
|
||||
* A binary swap bumps the folder mtime and changes this, invalidating the cache.
|
||||
*/
|
||||
private static function fingerprint(): string {
|
||||
$rParts = array();
|
||||
foreach (self::versionDirs() as $rVersion => $rDir) {
|
||||
$rParts[] = $rVersion . ':' . (@filemtime($rDir . 'ffmpeg') ?: 0);
|
||||
}
|
||||
sort($rParts);
|
||||
return md5(implode('|', $rParts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Version folders (name matches `<int>.<int>`) that ship an executable ffmpeg.
|
||||
*
|
||||
* @return array<string, string> version => absolute directory path (trailing slash)
|
||||
*/
|
||||
private static function versionDirs(): array {
|
||||
$rBase = self::baseDir();
|
||||
$rOut = array();
|
||||
foreach (glob($rBase . '*', GLOB_ONLYDIR) ?: array() as $rDir) {
|
||||
$rVersion = basename($rDir);
|
||||
if (!preg_match('/^\d+\.\d+$/', $rVersion)) {
|
||||
continue; // skip backups / non-version folders
|
||||
}
|
||||
$rBinary = $rDir . '/ffmpeg';
|
||||
if (is_file($rBinary)) {
|
||||
$rOut[$rVersion] = $rDir . '/';
|
||||
}
|
||||
}
|
||||
uksort($rOut, static fn (string $a, string $b): int => version_compare($b, $a));
|
||||
return $rOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe every build's capabilities by shelling out. Slow — callers cache it.
|
||||
*
|
||||
* @return array<string, array{version: string, gpu: bool, encoders: string[], banner: string}>
|
||||
*/
|
||||
private static function scan(): array {
|
||||
$rOut = array();
|
||||
foreach (self::versionDirs() as $rVersion => $rDir) {
|
||||
$rBinary = $rDir . 'ffmpeg';
|
||||
$rEncoders = self::hwEncoders($rBinary);
|
||||
$rOut[$rVersion] = array(
|
||||
'version' => $rVersion,
|
||||
'gpu' => count($rEncoders) > 0,
|
||||
'encoders' => $rEncoders,
|
||||
'banner' => self::banner($rBinary),
|
||||
);
|
||||
}
|
||||
return $rOut;
|
||||
}
|
||||
|
||||
/** First line of `ffmpeg -version` (the human-readable build banner). */
|
||||
private static function banner(string $rBinary): string {
|
||||
$rOut = (string) @shell_exec('timeout 5 ' . escapeshellarg($rBinary) . ' -hide_banner -version 2>/dev/null');
|
||||
$rLine = trim(strtok($rOut, "\n") ?: '');
|
||||
return $rLine !== '' ? $rLine : 'ffmpeg';
|
||||
}
|
||||
|
||||
/**
|
||||
* Hardware encoder names exposed by a build (nvenc/qsv/vaapi/amf/…).
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private static function hwEncoders(string $rBinary): array {
|
||||
$rOut = (string) @shell_exec('timeout 5 ' . escapeshellarg($rBinary) . ' -hide_banner -encoders 2>/dev/null');
|
||||
$rEncoders = array();
|
||||
foreach (explode("\n", $rOut) as $rRow) {
|
||||
// ` V....D h264_nvenc NVIDIA NVENC H.264 encoder`
|
||||
if (!preg_match('/^\s*[VASFXBD.]{6}\s+(\S+)/', $rRow, $rMatch)) {
|
||||
continue;
|
||||
}
|
||||
$rName = $rMatch[1];
|
||||
foreach (self::HW_ENCODER_MARKERS as $rMarker) {
|
||||
if (strpos($rName, $rMarker) !== false) {
|
||||
$rEncoders[] = $rName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sort($rEncoders);
|
||||
return array_values(array_unique($rEncoders));
|
||||
}
|
||||
|
||||
/** @return array{fingerprint?: string, versions?: array}|null */
|
||||
private static function readCache(): ?array {
|
||||
$rFile = self::cacheFile();
|
||||
if (!is_file($rFile)) {
|
||||
return null;
|
||||
}
|
||||
$rDecoded = json_decode((string) @file_get_contents($rFile), true);
|
||||
return is_array($rDecoded) ? $rDecoded : null;
|
||||
}
|
||||
|
||||
/** Best-effort atomic cache write; failure is non-fatal (rescans next time). */
|
||||
private static function writeCache(string $rFingerprint, array $rVersions): void {
|
||||
$rJson = json_encode(array('fingerprint' => $rFingerprint, 'versions' => $rVersions), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
if ($rJson === false) {
|
||||
return;
|
||||
}
|
||||
$rFile = self::cacheFile();
|
||||
$rTmp = $rFile . '.tmp';
|
||||
if (@file_put_contents($rTmp, $rJson, LOCK_EX) !== false) {
|
||||
@rename($rTmp, $rFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,36 +21,50 @@ class FfmpegPaths {
|
||||
private static $resolved = false;
|
||||
|
||||
/**
|
||||
* Resolve paths from an ffmpeg_cpu version string.
|
||||
* Resolve the CPU/GPU/probe binary paths from the configured version strings.
|
||||
*
|
||||
* Paths are built dynamically from BIN_PATH/ffmpeg_bin/<version>/ so any build
|
||||
* dropped into that folder is usable without touching this class — no version
|
||||
* switch to maintain. This runs on the bootstrap hot path, so it stays cheap:
|
||||
* a format check plus an is_file() stat, never a shell probe (that lives in
|
||||
* {@see FfmpegBinaries}). An unknown/missing CPU version falls back to the
|
||||
* legacy 4.0 build; the GPU binary falls back to the resolved CPU binary when
|
||||
* ffmpeg_gpu is empty or points at a missing build.
|
||||
*
|
||||
* Called once during bootstrap; subsequent calls are no-ops.
|
||||
*
|
||||
* @param string $version e.g. '8.0', '7.1', '4.0'
|
||||
* @param string $cpuVersion e.g. '8.0', '7.1', '4.0'
|
||||
* @param string|null $gpuVersion GPU ffmpeg version, or null to reuse the CPU build
|
||||
*/
|
||||
public static function resolve($version) {
|
||||
public static function resolve($cpuVersion, $gpuVersion = null) {
|
||||
if (self::$resolved) {
|
||||
return;
|
||||
}
|
||||
switch ($version) {
|
||||
case '8.0':
|
||||
self::$cpu = FFMPEG_BIN_80;
|
||||
self::$probe = FFPROBE_BIN_80;
|
||||
self::$gpu = FFMPEG_BIN_80;
|
||||
break;
|
||||
case '7.1':
|
||||
self::$cpu = FFMPEG_BIN_71;
|
||||
self::$probe = FFPROBE_BIN_71;
|
||||
self::$gpu = FFMPEG_BIN_71;
|
||||
break;
|
||||
default:
|
||||
self::$cpu = FFMPEG_BIN_40;
|
||||
self::$probe = FFPROBE_BIN_40;
|
||||
self::$gpu = FFMPEG_BIN_40;
|
||||
break;
|
||||
}
|
||||
|
||||
self::$cpu = self::binary($cpuVersion, 'ffmpeg') ?? FFMPEG_BIN_40;
|
||||
self::$probe = self::binary($cpuVersion, 'ffprobe') ?? FFPROBE_BIN_40;
|
||||
|
||||
$rGpu = ($gpuVersion !== null && $gpuVersion !== '') ? self::binary($gpuVersion, 'ffmpeg') : null;
|
||||
self::$gpu = $rGpu ?? self::$cpu;
|
||||
|
||||
self::$resolved = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a binary path for a version folder, or null when the version string is
|
||||
* malformed or the binary is absent.
|
||||
*
|
||||
* @param string $version Version folder name (e.g. '8.0')
|
||||
* @param string $name 'ffmpeg' or 'ffprobe'
|
||||
*/
|
||||
private static function binary($version, $name): ?string {
|
||||
if (!is_string($version) || !preg_match('/^\d+\.\d+$/', $version)) {
|
||||
return null;
|
||||
}
|
||||
$rPath = BIN_PATH . 'ffmpeg_bin/' . $version . '/' . $name;
|
||||
return is_file($rPath) ? $rPath : null;
|
||||
}
|
||||
|
||||
/** @return string Path to CPU-optimized ffmpeg binary */
|
||||
public static function cpu() {
|
||||
return self::$cpu;
|
||||
|
||||
Reference in New Issue
Block a user