first(); self::$cache[$key] = $row ? $row->value : $default; } return self::$cache[$key]; } public static function set(string $key, mixed $value): void { static::updateOrCreate(['key' => $key], ['value' => (string) $value]); self::$cache[$key] = (string) $value; } /** Returns the configured FFmpeg binary path, falling back to the config file default. */ public static function ffmpegBinary(): string { return static::get('ffmpeg_binary', config('ffmpeg.ffmpeg', '/usr/bin/ffmpeg')); } /** Raw setting: is GPU encoding switched on in the admin panel? */ public static function gpuEnabled(): bool { return static::get('gpu_enabled', 'true') === 'true'; } public static function gpuDevice(): int { return (int) static::get('gpu_device', '0'); } /** * Runtime gate used by every encode path: the GPU is switched on AND it is * actually reachable and able to encode right now. Unlike gpuEnabled() this * runs a live smoke-test (cached briefly) so a misconfigured / unplugged / * driver-mismatched GPU automatically routes work to the CPU instead of * producing a hung or failed encode. */ public static function gpuUsable(): bool { if (! static::gpuEnabled()) { return false; } // Cache the smoke-test result briefly, but never let a cache backend problem // (e.g. an unwritable cache dir) break an encode — fall back to a direct probe. try { return (bool) Cache::remember('gpu_usable_probe', 60, fn () => static::probeGpu()); } catch (\Throwable $e) { return static::probeGpu(); } } /** Forget the cached probe result — call this whenever GPU settings change. */ public static function flushGpuProbe(): void { try { Cache::forget('gpu_usable_probe'); } catch (\Throwable $e) { // ignore — a missing/unwritable cache just means the next call re-probes } } /** * Smoke-test the GPU encode path. Two checks: * 1. the device is visible to the NVIDIA driver (nvidia-smi), and * 2. the encoder actually produces a frame. * Step 2 is the decisive one — it catches CUDA / driver-version mismatches * that nvidia-smi cannot see. Returns true only if the GPU can really encode. * * @param string|null $encoder Encoder to test; defaults to the configured one. */ public static function probeGpu(?string $encoder = null): bool { $encoder = $encoder ?: static::get('gpu_encoder', 'h264_nvenc'); $device = static::gpuDevice(); $isNvenc = str_contains($encoder, 'nvenc'); // 1) Device visible to the driver? (catches unplugged card / unloaded module) if ($isNvenc) { @exec('nvidia-smi -i ' . (int) $device . ' --query-gpu=name --format=csv,noheader,nounits 2>/dev/null', $smi, $smiExit); if ($smiExit !== 0 || trim(implode('', $smi)) === '') { Log::warning('GPU probe: device not visible via nvidia-smi — using CPU', [ 'device' => $device, ]); return false; } } // 2) Encoder actually works? (encode a single throwaway frame) $ffmpeg = static::ffmpegBinary(); $tmp = sys_get_temp_dir() . '/gpu_probe_' . getmypid() . '_' . uniqid() . '.mp4'; $gpuArg = $isNvenc ? ' -gpu ' . (int) $device : ''; // 256x144 is the smallest 16:9 size NVENC will accept (it rejects tiny frames // such as 128x72 with "Frame Dimension less than the minimum supported value"). @exec( escapeshellcmd($ffmpeg) . ' -hide_banner -loglevel error' . ' -f lavfi -i color=c=black:s=256x144:r=1 -frames:v 1' . ' -c:v ' . escapeshellarg($encoder) . $gpuArg . ' -y ' . escapeshellarg($tmp) . ' 2>/dev/null', $o, $exit ); $ok = ($exit === 0 && is_file($tmp) && filesize($tmp) > 0); @unlink($tmp); if (! $ok) { Log::warning('GPU probe: encode smoke-test failed — using CPU', [ 'encoder' => $encoder, 'device' => $device, 'binary' => $ffmpeg, ]); } return $ok; } public static function gpuEncoder(): string { return static::gpuUsable() ? static::get('gpu_encoder', 'h264_nvenc') : 'libx264'; } public static function gpuPreset(): string { return static::gpuUsable() ? static::get('gpu_preset', 'p4') : 'fast'; } public static function gpuHwaccel(): string { return static::get('gpu_hwaccel', 'cuda'); } /** Returns the full video codec flags for FFmpeg shell commands. */ public static function ffmpegVideoFlags(bool $stillImage = false): string { if (static::gpuUsable()) { $enc = static::get('gpu_encoder', 'h264_nvenc'); $preset = static::get('gpu_preset', 'p4'); $device = static::gpuDevice(); $gpuFlag = str_contains($enc, 'nvenc') ? " -gpu {$device}" : ''; return "-c:v {$enc} -preset {$preset} -rc vbr -cq 23{$gpuFlag} -pix_fmt yuv420p"; } $tune = $stillImage ? ' -tune stillimage' : ''; return "-c:v libx264 -preset fast -crf 23{$tune} -pix_fmt yuv420p"; } /** CPU-only video codec flags — used as automatic fallback when GPU encoding fails. */ public static function ffmpegVideoFlagsCpu(bool $stillImage = false): string { $tune = $stillImage ? ' -tune stillimage' : ''; return "-c:v libx264 -preset fast -crf 23{$tune} -pix_fmt yuv420p"; } /** Returns hwaccel decode flags when the input source is a video file. */ public static function ffmpegHwaccelFlags(bool $inputIsVideo): string { if (! $inputIsVideo || ! static::gpuUsable()) return ''; $hwaccel = static::get('gpu_hwaccel', 'cuda'); $device = static::gpuDevice(); return "-hwaccel {$hwaccel} -hwaccel_device {$device} "; } }