Add PHP Video Converter Suite with live stream pipelines and nuclear control panel UI

Full-featured video conversion platform with:
- FFmpeg-based pipeline system with composable stages (transcode, scale, filter, audio, bitrate, framerate, trim, deinterlace, denoise, stabilize)
- Live stream management with real-time format switching (RTMP/RTSP/HTTP)
- Industrial/nuclear power plant control room themed UI with gauges, switches, LED indicators
- Format switchboard for instant conversion between 16+ video/audio formats
- Pipeline designer with visual flow editor and drag-and-drop stage composition
- Job queue with priority scheduling and batch conversion
- WebSocket server for real-time progress broadcasting
- REST API for all operations (upload, convert, streams, pipelines, queue)
- System monitoring (CPU, memory, disk) with animated gauge displays
- Docker Compose setup with web, websocket, and worker services

https://claude.ai/code/session_01WxmHGnVFXGm2bwbFREHkHb
This commit is contained in:
Claude
2026-02-07 18:11:04 +00:00
parent 282d8b70fc
commit 6c56306873
27 changed files with 4746 additions and 0 deletions
@@ -0,0 +1,159 @@
<?php
namespace VideoConverter\Process;
class FFmpegProcess
{
private string $command;
private ?int $pid = null;
private string $logFile;
private string $progressFile;
private float $duration = 0;
private string $status = 'pending';
private array $outputLines = [];
public function __construct(string $command, string $jobId)
{
$config = require __DIR__ . '/../../config/app.php';
$this->command = $command;
$logDir = $config['storage']['logs'];
if (!is_dir($logDir)) mkdir($logDir, 0755, true);
$this->logFile = $logDir . "/ffmpeg_{$jobId}.log";
$this->progressFile = $logDir . "/progress_{$jobId}.txt";
}
public function start(): bool
{
$cmd = $this->command
. " -progress " . escapeshellarg($this->progressFile)
. " -stats_period 0.5"
. " 2>" . escapeshellarg($this->logFile)
. " & echo $!";
$output = [];
exec($cmd, $output);
$this->pid = (int)($output[0] ?? 0);
if ($this->pid > 0) {
$this->status = 'running';
return true;
}
$this->status = 'error';
return false;
}
public function stop(): void
{
if ($this->pid && $this->isRunning()) {
posix_kill($this->pid, SIGTERM);
usleep(500000);
if ($this->isRunning()) {
posix_kill($this->pid, SIGKILL);
}
}
$this->status = 'stopped';
}
public function pause(): void
{
if ($this->pid && $this->isRunning()) {
posix_kill($this->pid, SIGSTOP);
$this->status = 'paused';
}
}
public function resume(): void
{
if ($this->pid) {
posix_kill($this->pid, SIGCONT);
$this->status = 'running';
}
}
public function isRunning(): bool
{
if (!$this->pid) return false;
return posix_kill($this->pid, 0);
}
public function getProgress(): array
{
$progress = [
'percent' => 0,
'frame' => 0,
'fps' => 0,
'speed' => '0x',
'time' => '00:00:00.00',
'bitrate' => '0kbits/s',
'size' => '0kB',
];
if (!file_exists($this->progressFile)) return $progress;
$content = file_get_contents($this->progressFile);
$lines = explode("\n", $content);
foreach ($lines as $line) {
$line = trim($line);
if (str_contains($line, '=')) {
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
switch ($key) {
case 'frame': $progress['frame'] = (int)$value; break;
case 'fps': $progress['fps'] = (float)$value; break;
case 'speed': $progress['speed'] = $value; break;
case 'out_time': $progress['time'] = $value; break;
case 'total_size': $progress['size'] = $this->formatBytes((int)$value); break;
case 'bitrate': $progress['bitrate'] = $value; break;
case 'progress':
if ($value === 'end') $progress['percent'] = 100;
break;
}
}
}
if ($this->duration > 0 && $progress['percent'] < 100) {
$currentTime = $this->timeToSeconds($progress['time']);
$progress['percent'] = min(99, round(($currentTime / $this->duration) * 100, 1));
}
return $progress;
}
public function getLog(int $lines = 50): string
{
if (!file_exists($this->logFile)) return '';
$all = file($this->logFile);
return implode('', array_slice($all, -$lines));
}
public function setDuration(float $duration): void
{
$this->duration = $duration;
}
public function getPid(): ?int { return $this->pid; }
public function getStatus(): string { return $this->status; }
public function getCommand(): string { return $this->command; }
private function timeToSeconds(string $time): float
{
$parts = explode(':', $time);
if (count($parts) !== 3) return 0;
return (int)$parts[0] * 3600 + (int)$parts[1] * 60 + (float)$parts[2];
}
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
$size = (float)$bytes;
while ($size >= 1024 && $i < count($units) - 1) {
$size /= 1024;
$i++;
}
return round($size, 1) . $units[$i];
}
}
@@ -0,0 +1,106 @@
<?php
namespace VideoConverter\Process;
class MediaProbe
{
private string $ffprobe;
public function __construct()
{
$config = require __DIR__ . '/../../config/app.php';
$this->ffprobe = $config['ffmpeg']['ffprobe'];
}
public function analyze(string $filePath): array
{
$cmd = sprintf(
'%s -v quiet -print_format json -show_format -show_streams %s',
$this->ffprobe,
escapeshellarg($filePath)
);
$output = shell_exec($cmd);
$data = json_decode($output ?: '{}', true);
if (!$data) {
return ['error' => 'Could not analyze file'];
}
return $this->parseProbeData($data);
}
public function getDuration(string $filePath): float
{
$info = $this->analyze($filePath);
return (float)($info['duration'] ?? 0);
}
public function getThumbnail(string $filePath, string $outputPath, string $time = '00:00:01'): bool
{
$config = require __DIR__ . '/../../config/app.php';
$cmd = sprintf(
'%s -y -i %s -ss %s -vframes 1 -vf scale=320:-1 %s 2>/dev/null',
$config['ffmpeg']['binary'],
escapeshellarg($filePath),
escapeshellarg($time),
escapeshellarg($outputPath)
);
exec($cmd, $output, $exitCode);
return $exitCode === 0;
}
private function parseProbeData(array $data): array
{
$result = [
'format' => $data['format']['format_long_name'] ?? 'Unknown',
'format_name' => $data['format']['format_name'] ?? '',
'duration' => (float)($data['format']['duration'] ?? 0),
'size' => (int)($data['format']['size'] ?? 0),
'bitrate' => (int)($data['format']['bit_rate'] ?? 0),
'streams' => [],
'video' => null,
'audio' => null,
];
foreach (($data['streams'] ?? []) as $stream) {
$type = $stream['codec_type'] ?? '';
$info = [
'index' => $stream['index'],
'type' => $type,
'codec' => $stream['codec_name'] ?? 'unknown',
'codec_long' => $stream['codec_long_name'] ?? '',
];
if ($type === 'video') {
$info['width'] = (int)($stream['width'] ?? 0);
$info['height'] = (int)($stream['height'] ?? 0);
$info['fps'] = $this->parseFps($stream['r_frame_rate'] ?? '0/1');
$info['pix_fmt'] = $stream['pix_fmt'] ?? '';
$info['bitrate'] = (int)($stream['bit_rate'] ?? 0);
$info['profile'] = $stream['profile'] ?? '';
$info['level'] = $stream['level'] ?? '';
if (!$result['video']) $result['video'] = $info;
} elseif ($type === 'audio') {
$info['sample_rate'] = (int)($stream['sample_rate'] ?? 0);
$info['channels'] = (int)($stream['channels'] ?? 0);
$info['channel_layout'] = $stream['channel_layout'] ?? '';
$info['bitrate'] = (int)($stream['bit_rate'] ?? 0);
if (!$result['audio']) $result['audio'] = $info;
}
$result['streams'][] = $info;
}
return $result;
}
private function parseFps(string $frac): float
{
$parts = explode('/', $frac);
if (count($parts) === 2 && (int)$parts[1] > 0) {
return round((int)$parts[0] / (int)$parts[1], 2);
}
return (float)$frac;
}
}