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,127 @@
|
||||
<?php
|
||||
|
||||
namespace VideoConverter\Pipeline;
|
||||
|
||||
class Pipeline
|
||||
{
|
||||
private string $id;
|
||||
private string $name;
|
||||
private array $stages = [];
|
||||
private string $status = 'idle'; // idle, running, paused, error, completed
|
||||
private ?string $inputSource = null;
|
||||
private array $metadata = [];
|
||||
private float $progress = 0;
|
||||
private ?int $pid = null;
|
||||
private string $createdAt;
|
||||
|
||||
public function __construct(string $name, ?string $id = null)
|
||||
{
|
||||
$this->id = $id ?? bin2hex(random_bytes(8));
|
||||
$this->name = $name;
|
||||
$this->createdAt = date('c');
|
||||
}
|
||||
|
||||
public function getId(): string { return $this->id; }
|
||||
public function getName(): string { return $this->name; }
|
||||
public function getStatus(): string { return $this->status; }
|
||||
public function getProgress(): float { return $this->progress; }
|
||||
public function getPid(): ?int { return $this->pid; }
|
||||
public function getStages(): array { return $this->stages; }
|
||||
public function getInputSource(): ?string { return $this->inputSource; }
|
||||
|
||||
public function setStatus(string $status): void { $this->status = $status; }
|
||||
public function setProgress(float $progress): void { $this->progress = min(100, max(0, $progress)); }
|
||||
public function setPid(?int $pid): void { $this->pid = $pid; }
|
||||
public function setInputSource(string $source): void { $this->inputSource = $source; }
|
||||
|
||||
public function addStage(PipelineStage $stage): self
|
||||
{
|
||||
$this->stages[] = $stage;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeStage(int $index): self
|
||||
{
|
||||
if (isset($this->stages[$index])) {
|
||||
array_splice($this->stages, $index, 1);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function insertStage(int $index, PipelineStage $stage): self
|
||||
{
|
||||
array_splice($this->stages, $index, 0, [$stage]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMetadata(string $key, mixed $value): void
|
||||
{
|
||||
$this->metadata[$key] = $value;
|
||||
}
|
||||
|
||||
public function getMetadata(?string $key = null): mixed
|
||||
{
|
||||
if ($key === null) return $this->metadata;
|
||||
return $this->metadata[$key] ?? null;
|
||||
}
|
||||
|
||||
public function buildFFmpegCommand(string $inputPath, string $outputPath): string
|
||||
{
|
||||
$config = require __DIR__ . '/../../config/app.php';
|
||||
$cmd = $config['ffmpeg']['binary'];
|
||||
$parts = ["-y -i " . escapeshellarg($inputPath)];
|
||||
|
||||
foreach ($this->stages as $stage) {
|
||||
$parts[] = $stage->toFFmpegArgs();
|
||||
}
|
||||
|
||||
$parts[] = escapeshellarg($outputPath);
|
||||
return $cmd . ' ' . implode(' ', $parts);
|
||||
}
|
||||
|
||||
public function buildStreamCommand(string $inputUrl, string $outputUrl): string
|
||||
{
|
||||
$config = require __DIR__ . '/../../config/app.php';
|
||||
$cmd = $config['ffmpeg']['binary'];
|
||||
$parts = ["-re -i " . escapeshellarg($inputUrl)];
|
||||
|
||||
foreach ($this->stages as $stage) {
|
||||
$parts[] = $stage->toFFmpegArgs();
|
||||
}
|
||||
|
||||
$parts[] = "-f flv " . escapeshellarg($outputUrl);
|
||||
return $cmd . ' ' . implode(' ', $parts);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'status' => $this->status,
|
||||
'progress' => $this->progress,
|
||||
'pid' => $this->pid,
|
||||
'input_source' => $this->inputSource,
|
||||
'stages' => array_map(fn(PipelineStage $s) => $s->toArray(), $this->stages),
|
||||
'metadata' => $this->metadata,
|
||||
'created_at' => $this->createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$pipeline = new self($data['name'], $data['id']);
|
||||
$pipeline->status = $data['status'] ?? 'idle';
|
||||
$pipeline->progress = $data['progress'] ?? 0;
|
||||
$pipeline->pid = $data['pid'] ?? null;
|
||||
$pipeline->inputSource = $data['input_source'] ?? null;
|
||||
$pipeline->metadata = $data['metadata'] ?? [];
|
||||
$pipeline->createdAt = $data['created_at'] ?? date('c');
|
||||
|
||||
foreach (($data['stages'] ?? []) as $stageData) {
|
||||
$pipeline->addStage(PipelineStage::fromArray($stageData));
|
||||
}
|
||||
|
||||
return $pipeline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace VideoConverter\Pipeline;
|
||||
|
||||
class PipelineManager
|
||||
{
|
||||
private string $stateFile;
|
||||
private array $pipelines = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->stateFile = __DIR__ . '/../../storage/temp/pipelines.json';
|
||||
$this->load();
|
||||
}
|
||||
|
||||
private function load(): void
|
||||
{
|
||||
if (file_exists($this->stateFile)) {
|
||||
$data = json_decode(file_get_contents($this->stateFile), true);
|
||||
foreach (($data['pipelines'] ?? []) as $pData) {
|
||||
$this->pipelines[$pData['id']] = Pipeline::fromArray($pData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$dir = dirname($this->stateFile);
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
$data = ['pipelines' => []];
|
||||
foreach ($this->pipelines as $pipeline) {
|
||||
$data['pipelines'][] = $pipeline->toArray();
|
||||
}
|
||||
file_put_contents($this->stateFile, json_encode($data, JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
public function create(string $name): Pipeline
|
||||
{
|
||||
$pipeline = new Pipeline($name);
|
||||
$this->pipelines[$pipeline->getId()] = $pipeline;
|
||||
$this->save();
|
||||
return $pipeline;
|
||||
}
|
||||
|
||||
public function get(string $id): ?Pipeline
|
||||
{
|
||||
return $this->pipelines[$id] ?? null;
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->pipelines;
|
||||
}
|
||||
|
||||
public function delete(string $id): bool
|
||||
{
|
||||
if (isset($this->pipelines[$id])) {
|
||||
$pipeline = $this->pipelines[$id];
|
||||
if ($pipeline->getStatus() === 'running' && $pipeline->getPid()) {
|
||||
posix_kill($pipeline->getPid(), SIGTERM);
|
||||
}
|
||||
unset($this->pipelines[$id]);
|
||||
$this->save();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getRunningCount(): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($this->pipelines as $p) {
|
||||
if ($p->getStatus() === 'running') $count++;
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function getByStatus(string $status): array
|
||||
{
|
||||
return array_filter($this->pipelines, fn(Pipeline $p) => $p->getStatus() === $status);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_map(fn(Pipeline $p) => $p->toArray(), array_values($this->pipelines));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace VideoConverter\Pipeline;
|
||||
|
||||
class PipelineStage
|
||||
{
|
||||
private string $id;
|
||||
private string $type; // transcode, scale, filter, audio, watermark, trim, split
|
||||
private array $params;
|
||||
private bool $enabled;
|
||||
private string $label;
|
||||
|
||||
public function __construct(string $type, array $params = [], string $label = '', bool $enabled = true)
|
||||
{
|
||||
$this->id = bin2hex(random_bytes(4));
|
||||
$this->type = $type;
|
||||
$this->params = $params;
|
||||
$this->label = $label ?: ucfirst($type);
|
||||
$this->enabled = $enabled;
|
||||
}
|
||||
|
||||
public function getId(): string { return $this->id; }
|
||||
public function getType(): string { return $this->type; }
|
||||
public function getParams(): array { return $this->params; }
|
||||
public function isEnabled(): bool { return $this->enabled; }
|
||||
public function getLabel(): string { return $this->label; }
|
||||
|
||||
public function setEnabled(bool $enabled): void { $this->enabled = $enabled; }
|
||||
public function setParams(array $params): void { $this->params = $params; }
|
||||
|
||||
public function toFFmpegArgs(): string
|
||||
{
|
||||
if (!$this->enabled) return '';
|
||||
|
||||
return match ($this->type) {
|
||||
'transcode' => $this->buildTranscodeArgs(),
|
||||
'scale' => $this->buildScaleArgs(),
|
||||
'filter' => $this->buildFilterArgs(),
|
||||
'audio' => $this->buildAudioArgs(),
|
||||
'watermark' => $this->buildWatermarkArgs(),
|
||||
'trim' => $this->buildTrimArgs(),
|
||||
'bitrate' => $this->buildBitrateArgs(),
|
||||
'framerate' => $this->buildFramerateArgs(),
|
||||
'deinterlace' => '-vf yadif',
|
||||
'denoise' => '-vf hqdn3d',
|
||||
'stabilize' => '-vf deshake',
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
private function buildTranscodeArgs(): string
|
||||
{
|
||||
$args = [];
|
||||
if (isset($this->params['video_codec'])) {
|
||||
$args[] = "-c:v " . escapeshellarg($this->params['video_codec']);
|
||||
}
|
||||
if (isset($this->params['audio_codec'])) {
|
||||
$args[] = "-c:a " . escapeshellarg($this->params['audio_codec']);
|
||||
}
|
||||
if (isset($this->params['preset'])) {
|
||||
$args[] = "-preset " . escapeshellarg($this->params['preset']);
|
||||
}
|
||||
if (isset($this->params['crf'])) {
|
||||
$args[] = "-crf " . (int)$this->params['crf'];
|
||||
}
|
||||
return implode(' ', $args);
|
||||
}
|
||||
|
||||
private function buildScaleArgs(): string
|
||||
{
|
||||
$w = (int)($this->params['width'] ?? -1);
|
||||
$h = (int)($this->params['height'] ?? -1);
|
||||
$algo = $this->params['algorithm'] ?? 'lanczos';
|
||||
return "-vf scale={$w}:{$h}:flags={$algo}";
|
||||
}
|
||||
|
||||
private function buildFilterArgs(): string
|
||||
{
|
||||
$filters = [];
|
||||
if (isset($this->params['brightness'])) {
|
||||
$filters[] = "eq=brightness=" . (float)$this->params['brightness'];
|
||||
}
|
||||
if (isset($this->params['contrast'])) {
|
||||
$filters[] = "eq=contrast=" . (float)$this->params['contrast'];
|
||||
}
|
||||
if (isset($this->params['saturation'])) {
|
||||
$filters[] = "eq=saturation=" . (float)$this->params['saturation'];
|
||||
}
|
||||
if (isset($this->params['gamma'])) {
|
||||
$filters[] = "eq=gamma=" . (float)$this->params['gamma'];
|
||||
}
|
||||
if (isset($this->params['custom'])) {
|
||||
$filters[] = $this->params['custom'];
|
||||
}
|
||||
return $filters ? '-vf ' . escapeshellarg(implode(',', $filters)) : '';
|
||||
}
|
||||
|
||||
private function buildAudioArgs(): string
|
||||
{
|
||||
$args = [];
|
||||
if (isset($this->params['codec'])) {
|
||||
$args[] = "-c:a " . escapeshellarg($this->params['codec']);
|
||||
}
|
||||
if (isset($this->params['bitrate'])) {
|
||||
$args[] = "-b:a " . escapeshellarg($this->params['bitrate']);
|
||||
}
|
||||
if (isset($this->params['sample_rate'])) {
|
||||
$args[] = "-ar " . (int)$this->params['sample_rate'];
|
||||
}
|
||||
if (isset($this->params['channels'])) {
|
||||
$args[] = "-ac " . (int)$this->params['channels'];
|
||||
}
|
||||
if (isset($this->params['volume'])) {
|
||||
$args[] = "-af volume=" . (float)$this->params['volume'];
|
||||
}
|
||||
return implode(' ', $args);
|
||||
}
|
||||
|
||||
private function buildWatermarkArgs(): string
|
||||
{
|
||||
$image = $this->params['image'] ?? '';
|
||||
$position = $this->params['position'] ?? 'topright';
|
||||
$overlay = match ($position) {
|
||||
'topleft' => 'overlay=10:10',
|
||||
'topright' => 'overlay=W-w-10:10',
|
||||
'bottomleft' => 'overlay=10:H-h-10',
|
||||
'bottomright' => 'overlay=W-w-10:H-h-10',
|
||||
'center' => 'overlay=(W-w)/2:(H-h)/2',
|
||||
default => 'overlay=W-w-10:10',
|
||||
};
|
||||
return "-i " . escapeshellarg($image) . " -filter_complex \"{$overlay}\"";
|
||||
}
|
||||
|
||||
private function buildTrimArgs(): string
|
||||
{
|
||||
$args = [];
|
||||
if (isset($this->params['start'])) {
|
||||
$args[] = "-ss " . escapeshellarg($this->params['start']);
|
||||
}
|
||||
if (isset($this->params['duration'])) {
|
||||
$args[] = "-t " . escapeshellarg($this->params['duration']);
|
||||
}
|
||||
if (isset($this->params['end'])) {
|
||||
$args[] = "-to " . escapeshellarg($this->params['end']);
|
||||
}
|
||||
return implode(' ', $args);
|
||||
}
|
||||
|
||||
private function buildBitrateArgs(): string
|
||||
{
|
||||
$args = [];
|
||||
if (isset($this->params['video'])) {
|
||||
$args[] = "-b:v " . escapeshellarg($this->params['video']);
|
||||
}
|
||||
if (isset($this->params['audio'])) {
|
||||
$args[] = "-b:a " . escapeshellarg($this->params['audio']);
|
||||
}
|
||||
if (isset($this->params['maxrate'])) {
|
||||
$args[] = "-maxrate " . escapeshellarg($this->params['maxrate']);
|
||||
$args[] = "-bufsize " . escapeshellarg($this->params['bufsize'] ?? $this->params['maxrate']);
|
||||
}
|
||||
return implode(' ', $args);
|
||||
}
|
||||
|
||||
private function buildFramerateArgs(): string
|
||||
{
|
||||
$fps = (float)($this->params['fps'] ?? 30);
|
||||
return "-r {$fps}";
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'type' => $this->type,
|
||||
'label' => $this->label,
|
||||
'params' => $this->params,
|
||||
'enabled' => $this->enabled,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$stage = new self(
|
||||
$data['type'],
|
||||
$data['params'] ?? [],
|
||||
$data['label'] ?? '',
|
||||
$data['enabled'] ?? true
|
||||
);
|
||||
if (isset($data['id'])) {
|
||||
// Use reflection to set the id for deserialization
|
||||
$ref = new \ReflectionProperty($stage, 'id');
|
||||
$ref->setValue($stage, $data['id']);
|
||||
}
|
||||
return $stage;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user