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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user