Files
Ai/video-converter-suite/src/Stream/StreamManager.php
T
Claude 6c56306873 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
2026-02-07 18:11:04 +00:00

198 lines
6.0 KiB
PHP

<?php
namespace VideoConverter\Stream;
use VideoConverter\Process\FFmpegProcess;
class StreamManager
{
private array $activeStreams = [];
private string $stateFile;
public function __construct()
{
$this->stateFile = __DIR__ . '/../../storage/temp/streams.json';
$this->load();
}
private function load(): void
{
if (file_exists($this->stateFile)) {
$this->activeStreams = json_decode(file_get_contents($this->stateFile), true) ?: [];
}
}
private function save(): void
{
$dir = dirname($this->stateFile);
if (!is_dir($dir)) mkdir($dir, 0755, true);
file_put_contents($this->stateFile, json_encode($this->activeStreams, JSON_PRETTY_PRINT));
}
public function startStream(array $params): array
{
$id = bin2hex(random_bytes(8));
$config = require __DIR__ . '/../../config/app.php';
$inputUrl = $params['input_url'] ?? '';
$outputFormat = $params['output_format'] ?? 'mp4';
$resolution = $params['resolution'] ?? null;
$preset = $params['preset'] ?? 'fast';
$formatConfig = $config['formats']['video'][$outputFormat] ?? $config['formats']['video']['mp4'];
$presetConfig = $config['presets'][$preset] ?? $config['presets']['fast'];
$outputDir = $config['storage']['outputs'];
$outputFile = "{$outputDir}/stream_{$id}.{$formatConfig['ext']}";
$cmd = $config['ffmpeg']['binary'] . " -y";
// Input
if (str_starts_with($inputUrl, 'rtmp://') || str_starts_with($inputUrl, 'rtsp://')) {
$cmd .= " -re";
}
$cmd .= " -i " . escapeshellarg($inputUrl);
// Video codec
$cmd .= " -c:v " . escapeshellarg($formatConfig['codec']);
$cmd .= " -preset " . escapeshellarg($presetConfig['preset']);
$cmd .= " -crf " . (int)$presetConfig['crf'];
// Resolution
if ($resolution && isset($config['resolutions'][$resolution])) {
$res = $config['resolutions'][$resolution];
$cmd .= " -vf scale={$res['width']}:{$res['height']}";
}
// Audio
$audioCodec = $params['audio_codec'] ?? 'aac';
$audioBitrate = $params['audio_bitrate'] ?? '128k';
$cmd .= " -c:a " . escapeshellarg($audioCodec);
$cmd .= " -b:a " . escapeshellarg($audioBitrate);
$cmd .= " " . escapeshellarg($outputFile);
$process = new FFmpegProcess($cmd, $id);
$stream = [
'id' => $id,
'input_url' => $inputUrl,
'output_file' => $outputFile,
'output_format' => $outputFormat,
'resolution' => $resolution,
'preset' => $preset,
'status' => 'starting',
'pid' => null,
'command' => $cmd,
'started_at' => date('c'),
];
if ($process->start()) {
$stream['status'] = 'running';
$stream['pid'] = $process->getPid();
} else {
$stream['status'] = 'error';
}
$this->activeStreams[$id] = $stream;
$this->save();
return $stream;
}
public function stopStream(string $id): bool
{
if (!isset($this->activeStreams[$id])) return false;
$stream = $this->activeStreams[$id];
if ($stream['pid']) {
posix_kill($stream['pid'], SIGTERM);
usleep(500000);
if (posix_kill($stream['pid'], 0)) {
posix_kill($stream['pid'], SIGKILL);
}
}
$this->activeStreams[$id]['status'] = 'stopped';
$this->activeStreams[$id]['stopped_at'] = date('c');
$this->save();
return true;
}
public function switchFormat(string $id, string $newFormat, ?string $resolution = null): array
{
if (!isset($this->activeStreams[$id])) {
return ['error' => 'Stream not found'];
}
$oldStream = $this->activeStreams[$id];
$this->stopStream($id);
// Start new stream with same input but different output format
return $this->startStream([
'input_url' => $oldStream['input_url'],
'output_format' => $newFormat,
'resolution' => $resolution ?? $oldStream['resolution'],
'preset' => $oldStream['preset'],
'audio_codec' => 'aac',
]);
}
public function getStream(string $id): ?array
{
$this->refreshStatus($id);
return $this->activeStreams[$id] ?? null;
}
public function getAllStreams(): array
{
foreach (array_keys($this->activeStreams) as $id) {
$this->refreshStatus($id);
}
return array_values($this->activeStreams);
}
public function getActiveStreams(): array
{
return array_values(array_filter($this->getAllStreams(), fn($s) => $s['status'] === 'running'));
}
private function refreshStatus(string $id): void
{
if (!isset($this->activeStreams[$id])) return;
$stream = &$this->activeStreams[$id];
if ($stream['status'] === 'running' && $stream['pid']) {
if (!posix_kill($stream['pid'], 0)) {
$stream['status'] = 'completed';
$stream['completed_at'] = date('c');
$this->save();
}
}
}
public function deleteStream(string $id): bool
{
if (isset($this->activeStreams[$id])) {
if ($this->activeStreams[$id]['status'] === 'running') {
$this->stopStream($id);
}
unset($this->activeStreams[$id]);
$this->save();
return true;
}
return false;
}
public function getStats(): array
{
$all = $this->getAllStreams();
return [
'total' => count($all),
'running' => count(array_filter($all, fn($s) => $s['status'] === 'running')),
'completed' => count(array_filter($all, fn($s) => $s['status'] === 'completed')),
'errors' => count(array_filter($all, fn($s) => $s['status'] === 'error')),
];
}
}