0b7e2d0a5b
- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
624 lines
22 KiB
PHP
624 lines
22 KiB
PHP
<?php
|
|
/*******************************************************************************************************************
|
|
| Software Name : EasyStream
|
|
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
|
|
| Software Author : (c) Sami Ahmed
|
|
|*******************************************************************************************************************
|
|
|
|
|
|*******************************************************************************************************************
|
|
| This source file is subject to the EasyStream Proprietary License Agreement.
|
|
|
|
|
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|
|
|*******************************************************************************************************************
|
|
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|
|
|*******************************************************************************************************************/
|
|
|
|
defined('_ISVALID') or header('Location: /error');
|
|
|
|
/**
|
|
* Advanced Video Processing System with FFmpeg Integration
|
|
*/
|
|
class VVideoProcessor
|
|
{
|
|
private $ffmpegPath;
|
|
private $ffprobePath;
|
|
private $tempDir;
|
|
private $outputDir;
|
|
private $logger;
|
|
private $queue;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->ffmpegPath = $this->findFFmpegPath();
|
|
$this->ffprobePath = $this->findFFprobePath();
|
|
$this->tempDir = _FPATH . 'f_data/temp/';
|
|
$this->outputDir = _FPATH . 'f_data/processed/';
|
|
$this->logger = VLogger::getInstance();
|
|
$this->queue = new VQueue();
|
|
|
|
// Create directories if they don't exist
|
|
$this->ensureDirectories();
|
|
}
|
|
|
|
/**
|
|
* Process video file with multiple formats and HLS streaming
|
|
* @param string $inputFile Input video file path
|
|
* @param string $videoKey Video unique key
|
|
* @param array $options Processing options
|
|
* @return array Processing result
|
|
*/
|
|
public function processVideo($inputFile, $videoKey, $options = [])
|
|
{
|
|
$this->logger->info('Starting video processing', [
|
|
'input_file' => $inputFile,
|
|
'video_key' => $videoKey,
|
|
'options' => $options
|
|
]);
|
|
|
|
try {
|
|
// Validate input file
|
|
if (!file_exists($inputFile)) {
|
|
throw new Exception("Input file not found: {$inputFile}");
|
|
}
|
|
|
|
// Get video information
|
|
$videoInfo = $this->getVideoInfo($inputFile);
|
|
if (!$videoInfo) {
|
|
throw new Exception("Unable to read video information");
|
|
}
|
|
|
|
// Create output directory for this video
|
|
$videoOutputDir = $this->outputDir . $videoKey . '/';
|
|
if (!is_dir($videoOutputDir)) {
|
|
mkdir($videoOutputDir, 0755, true);
|
|
}
|
|
|
|
// Process different formats
|
|
$formats = $options['formats'] ?? ['1080p', '720p', '480p', '360p'];
|
|
$results = [];
|
|
|
|
foreach ($formats as $format) {
|
|
$result = $this->processFormat($inputFile, $videoOutputDir, $format, $videoKey, $videoInfo);
|
|
$results[$format] = $result;
|
|
}
|
|
|
|
// Generate HLS playlist
|
|
$hlsResult = $this->generateHLS($videoOutputDir, $videoKey, $results);
|
|
$results['hls'] = $hlsResult;
|
|
|
|
// Generate thumbnails
|
|
$thumbnailResult = $this->generateThumbnails($inputFile, $videoOutputDir, $videoKey);
|
|
$results['thumbnails'] = $thumbnailResult;
|
|
|
|
// Generate preview/trailer
|
|
if ($options['generate_preview'] ?? true) {
|
|
$previewResult = $this->generatePreview($inputFile, $videoOutputDir, $videoKey);
|
|
$results['preview'] = $previewResult;
|
|
}
|
|
|
|
// Update database with processing results
|
|
$this->updateVideoRecord($videoKey, $results, $videoInfo);
|
|
|
|
$this->logger->info('Video processing completed', [
|
|
'video_key' => $videoKey,
|
|
'results' => array_keys($results)
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'video_key' => $videoKey,
|
|
'results' => $results,
|
|
'video_info' => $videoInfo
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Video processing failed', [
|
|
'video_key' => $videoKey,
|
|
'input_file' => $inputFile,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
// Update database with failed status
|
|
$this->updateVideoStatus($videoKey, 'failed', $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage(),
|
|
'video_key' => $videoKey
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queue video for background processing
|
|
* @param string $inputFile Input file path
|
|
* @param string $videoKey Video key
|
|
* @param array $options Processing options
|
|
* @return string|false Job ID or false on failure
|
|
*/
|
|
public function queueVideoProcessing($inputFile, $videoKey, $options = [])
|
|
{
|
|
$jobData = [
|
|
'video_key' => $videoKey,
|
|
'input_file' => $inputFile,
|
|
'output_dir' => $this->outputDir . $videoKey . '/',
|
|
'formats' => $options['formats'] ?? ['1080p', '720p', '480p', '360p'],
|
|
'generate_preview' => $options['generate_preview'] ?? true,
|
|
'generate_hls' => $options['generate_hls'] ?? true
|
|
];
|
|
|
|
$jobId = $this->queue->enqueue('VideoProcessingJob', $jobData, 'video_processing', 0, 1);
|
|
|
|
if ($jobId) {
|
|
// Update video status to queued
|
|
$this->updateVideoStatus($videoKey, 'queued');
|
|
|
|
$this->logger->info('Video queued for processing', [
|
|
'video_key' => $videoKey,
|
|
'job_id' => $jobId
|
|
]);
|
|
}
|
|
|
|
return $jobId;
|
|
}
|
|
|
|
/**
|
|
* Get video information using FFprobe
|
|
* @param string $inputFile Input file path
|
|
* @return array|false Video information or false on failure
|
|
*/
|
|
public function getVideoInfo($inputFile)
|
|
{
|
|
$command = sprintf(
|
|
'%s -v quiet -print_format json -show_format -show_streams %s',
|
|
escapeshellarg($this->ffprobePath),
|
|
escapeshellarg($inputFile)
|
|
);
|
|
|
|
$output = shell_exec($command);
|
|
|
|
if (!$output) {
|
|
return false;
|
|
}
|
|
|
|
$info = json_decode($output, true);
|
|
|
|
if (!$info) {
|
|
return false;
|
|
}
|
|
|
|
// Extract video stream information
|
|
$videoStream = null;
|
|
$audioStream = null;
|
|
|
|
foreach ($info['streams'] as $stream) {
|
|
if ($stream['codec_type'] === 'video' && !$videoStream) {
|
|
$videoStream = $stream;
|
|
} elseif ($stream['codec_type'] === 'audio' && !$audioStream) {
|
|
$audioStream = $stream;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'format' => $info['format'],
|
|
'video_stream' => $videoStream,
|
|
'audio_stream' => $audioStream,
|
|
'duration' => (float)($info['format']['duration'] ?? 0),
|
|
'size' => (int)($info['format']['size'] ?? 0),
|
|
'bitrate' => (int)($info['format']['bit_rate'] ?? 0),
|
|
'width' => (int)($videoStream['width'] ?? 0),
|
|
'height' => (int)($videoStream['height'] ?? 0),
|
|
'fps' => $this->calculateFPS($videoStream),
|
|
'codec' => $videoStream['codec_name'] ?? 'unknown'
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Process video to specific format
|
|
* @param string $inputFile Input file path
|
|
* @param string $outputDir Output directory
|
|
* @param string $format Format (1080p, 720p, etc.)
|
|
* @param string $videoKey Video key
|
|
* @param array $videoInfo Video information
|
|
* @return array Processing result
|
|
*/
|
|
private function processFormat($inputFile, $outputDir, $format, $videoKey, $videoInfo)
|
|
{
|
|
$outputFile = $outputDir . $videoKey . '_' . $format . '.mp4';
|
|
|
|
// Define format settings
|
|
$formatSettings = [
|
|
'1080p' => ['width' => 1920, 'height' => 1080, 'bitrate' => '5000k', 'audio_bitrate' => '192k'],
|
|
'720p' => ['width' => 1280, 'height' => 720, 'bitrate' => '2500k', 'audio_bitrate' => '128k'],
|
|
'480p' => ['width' => 854, 'height' => 480, 'bitrate' => '1000k', 'audio_bitrate' => '128k'],
|
|
'360p' => ['width' => 640, 'height' => 360, 'bitrate' => '750k', 'audio_bitrate' => '96k'],
|
|
'240p' => ['width' => 426, 'height' => 240, 'bitrate' => '400k', 'audio_bitrate' => '64k']
|
|
];
|
|
|
|
if (!isset($formatSettings[$format])) {
|
|
return [
|
|
'success' => false,
|
|
'error' => "Unknown format: {$format}",
|
|
'output_file' => null
|
|
];
|
|
}
|
|
|
|
$settings = $formatSettings[$format];
|
|
|
|
// Skip if input resolution is lower than target
|
|
if ($videoInfo['height'] < $settings['height']) {
|
|
$this->logger->info("Skipping {$format} - input resolution too low", [
|
|
'input_height' => $videoInfo['height'],
|
|
'target_height' => $settings['height']
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Input resolution too low',
|
|
'skipped' => true
|
|
];
|
|
}
|
|
|
|
// Build FFmpeg command with optimized settings
|
|
$command = sprintf(
|
|
'%s -i %s -c:v libx264 -preset medium -crf 23 -maxrate %s -bufsize %s -vf "scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2:color=black" -c:a aac -b:a %s -movflags +faststart -f mp4 %s 2>&1',
|
|
escapeshellarg($this->ffmpegPath),
|
|
escapeshellarg($inputFile),
|
|
$settings['bitrate'],
|
|
$this->calculateBufferSize($settings['bitrate']),
|
|
$settings['width'],
|
|
$settings['height'],
|
|
$settings['width'],
|
|
$settings['height'],
|
|
$settings['audio_bitrate'],
|
|
escapeshellarg($outputFile)
|
|
);
|
|
|
|
$this->logger->info("Processing {$format}", [
|
|
'command' => $command,
|
|
'output_file' => $outputFile
|
|
]);
|
|
|
|
$startTime = microtime(true);
|
|
$output = [];
|
|
$returnCode = 0;
|
|
|
|
exec($command, $output, $returnCode);
|
|
|
|
$processingTime = microtime(true) - $startTime;
|
|
|
|
if ($returnCode === 0 && file_exists($outputFile)) {
|
|
$fileSize = filesize($outputFile);
|
|
|
|
return [
|
|
'success' => true,
|
|
'output_file' => $outputFile,
|
|
'file_size' => $fileSize,
|
|
'format' => $format,
|
|
'processing_time' => $processingTime,
|
|
'settings' => $settings
|
|
];
|
|
} else {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'FFmpeg processing failed',
|
|
'return_code' => $returnCode,
|
|
'output' => implode("\n", $output),
|
|
'processing_time' => $processingTime
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate HLS playlist for adaptive streaming
|
|
* @param string $outputDir Output directory
|
|
* @param string $videoKey Video key
|
|
* @param array $formatResults Format processing results
|
|
* @return array HLS generation result
|
|
*/
|
|
private function generateHLS($outputDir, $videoKey, $formatResults)
|
|
{
|
|
$hlsDir = $outputDir . 'hls/';
|
|
if (!is_dir($hlsDir)) {
|
|
mkdir($hlsDir, 0755, true);
|
|
}
|
|
|
|
$masterPlaylist = "#EXTM3U\n#EXT-X-VERSION:3\n\n";
|
|
$hlsResults = [];
|
|
|
|
foreach ($formatResults as $format => $result) {
|
|
if (!$result['success'] || !isset($result['output_file'])) {
|
|
continue;
|
|
}
|
|
|
|
$segmentDir = $hlsDir . $format . '/';
|
|
if (!is_dir($segmentDir)) {
|
|
mkdir($segmentDir, 0755, true);
|
|
}
|
|
|
|
$playlistFile = $segmentDir . 'playlist.m3u8';
|
|
|
|
// Generate HLS segments
|
|
$command = sprintf(
|
|
'%s -i %s -c copy -hls_time 10 -hls_list_size 0 -hls_segment_filename %s -f hls %s 2>&1',
|
|
escapeshellarg($this->ffmpegPath),
|
|
escapeshellarg($result['output_file']),
|
|
escapeshellarg($segmentDir . 'segment_%03d.ts'),
|
|
escapeshellarg($playlistFile)
|
|
);
|
|
|
|
$output = [];
|
|
$returnCode = 0;
|
|
exec($command, $output, $returnCode);
|
|
|
|
if ($returnCode === 0 && file_exists($playlistFile)) {
|
|
$hlsResults[$format] = [
|
|
'success' => true,
|
|
'playlist_file' => $playlistFile,
|
|
'segment_dir' => $segmentDir
|
|
];
|
|
|
|
// Add to master playlist
|
|
$bandwidth = $this->calculateBandwidth($result['settings']['bitrate']);
|
|
$resolution = $result['settings']['width'] . 'x' . $result['settings']['height'];
|
|
|
|
$masterPlaylist .= "#EXT-X-STREAM-INF:BANDWIDTH={$bandwidth},RESOLUTION={$resolution}\n";
|
|
$masterPlaylist .= "{$format}/playlist.m3u8\n\n";
|
|
} else {
|
|
$hlsResults[$format] = [
|
|
'success' => false,
|
|
'error' => 'HLS generation failed',
|
|
'output' => implode("\n", $output)
|
|
];
|
|
}
|
|
}
|
|
|
|
// Write master playlist
|
|
$masterPlaylistFile = $hlsDir . 'master.m3u8';
|
|
file_put_contents($masterPlaylistFile, $masterPlaylist);
|
|
|
|
return [
|
|
'success' => !empty($hlsResults),
|
|
'master_playlist' => $masterPlaylistFile,
|
|
'formats' => $hlsResults,
|
|
'hls_dir' => $hlsDir
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate video thumbnails
|
|
* @param string $inputFile Input file path
|
|
* @param string $outputDir Output directory
|
|
* @param string $videoKey Video key
|
|
* @return array Thumbnail generation result
|
|
*/
|
|
private function generateThumbnails($inputFile, $outputDir, $videoKey)
|
|
{
|
|
$thumbnailDir = $outputDir . 'thumbnails/';
|
|
if (!is_dir($thumbnailDir)) {
|
|
mkdir($thumbnailDir, 0755, true);
|
|
}
|
|
|
|
$thumbnails = [];
|
|
|
|
// Generate main thumbnail (at 10% of duration)
|
|
$mainThumb = $thumbnailDir . $videoKey . '_thumb.jpg';
|
|
$command = sprintf(
|
|
'%s -i %s -ss 00:00:10 -vframes 1 -vf "scale=320:240:force_original_aspect_ratio=decrease,pad=320:240:(ow-iw)/2:(oh-ih)/2:color=black" -q:v 2 %s 2>&1',
|
|
escapeshellarg($this->ffmpegPath),
|
|
escapeshellarg($inputFile),
|
|
escapeshellarg($mainThumb)
|
|
);
|
|
|
|
exec($command, $output, $returnCode);
|
|
|
|
if ($returnCode === 0 && file_exists($mainThumb)) {
|
|
$thumbnails['main'] = [
|
|
'success' => true,
|
|
'file' => $mainThumb,
|
|
'size' => filesize($mainThumb)
|
|
];
|
|
}
|
|
|
|
// Generate multiple thumbnails for preview
|
|
$previewThumbs = [];
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$time = $i * 20; // Every 20 seconds
|
|
$thumbFile = $thumbnailDir . $videoKey . "_preview_{$i}.jpg";
|
|
|
|
$command = sprintf(
|
|
'%s -i %s -ss %d -vframes 1 -vf "scale=160:120:force_original_aspect_ratio=decrease,pad=160:120:(ow-iw)/2:(oh-ih)/2:color=black" -q:v 2 %s 2>&1',
|
|
escapeshellarg($this->ffmpegPath),
|
|
escapeshellarg($inputFile),
|
|
$time,
|
|
escapeshellarg($thumbFile)
|
|
);
|
|
|
|
exec($command, $output, $returnCode);
|
|
|
|
if ($returnCode === 0 && file_exists($thumbFile)) {
|
|
$previewThumbs[] = $thumbFile;
|
|
}
|
|
}
|
|
|
|
$thumbnails['preview'] = $previewThumbs;
|
|
|
|
return [
|
|
'success' => !empty($thumbnails),
|
|
'thumbnails' => $thumbnails,
|
|
'thumbnail_dir' => $thumbnailDir
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate video preview/trailer
|
|
* @param string $inputFile Input file path
|
|
* @param string $outputDir Output directory
|
|
* @param string $videoKey Video key
|
|
* @return array Preview generation result
|
|
*/
|
|
private function generatePreview($inputFile, $outputDir, $videoKey)
|
|
{
|
|
$previewFile = $outputDir . $videoKey . '_preview.mp4';
|
|
|
|
// Generate 30-second preview from multiple segments
|
|
$command = sprintf(
|
|
'%s -i %s -vf "select=\'between(t,10,20)+between(t,60,70)+between(t,120,130)\',setpts=N/FRAME_RATE/TB" -af "aselect=\'between(t,10,20)+between(t,60,70)+between(t,120,130)\',asetpts=N/SR/TB" -c:v libx264 -preset fast -crf 28 -c:a aac -b:a 128k -movflags +faststart %s 2>&1',
|
|
escapeshellarg($this->ffmpegPath),
|
|
escapeshellarg($inputFile),
|
|
escapeshellarg($previewFile)
|
|
);
|
|
|
|
$output = [];
|
|
$returnCode = 0;
|
|
exec($command, $output, $returnCode);
|
|
|
|
if ($returnCode === 0 && file_exists($previewFile)) {
|
|
return [
|
|
'success' => true,
|
|
'preview_file' => $previewFile,
|
|
'file_size' => filesize($previewFile)
|
|
];
|
|
} else {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Preview generation failed',
|
|
'output' => implode("\n", $output)
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update video record in database
|
|
* @param string $videoKey Video key
|
|
* @param array $results Processing results
|
|
* @param array $videoInfo Video information
|
|
*/
|
|
private function updateVideoRecord($videoKey, $results, $videoInfo)
|
|
{
|
|
try {
|
|
$db = VDatabase::getInstance();
|
|
|
|
$updateData = [
|
|
'processing_status' => 'completed',
|
|
'processed_at' => date('Y-m-d H:i:s'),
|
|
'duration' => $videoInfo['duration'],
|
|
'file_size' => $videoInfo['size'],
|
|
'video_width' => $videoInfo['width'],
|
|
'video_height' => $videoInfo['height'],
|
|
'video_fps' => $videoInfo['fps'],
|
|
'video_codec' => $videoInfo['codec'],
|
|
'available_formats' => json_encode(array_keys($results)),
|
|
'hls_available' => isset($results['hls']) && $results['hls']['success'] ? 1 : 0,
|
|
'thumbnails_generated' => isset($results['thumbnails']) && $results['thumbnails']['success'] ? 1 : 0
|
|
];
|
|
|
|
$db->doUpdate('db_videofiles', 'file_key', $updateData, $videoKey);
|
|
|
|
$this->logger->info('Video record updated', [
|
|
'video_key' => $videoKey,
|
|
'status' => 'completed'
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to update video record', [
|
|
'video_key' => $videoKey,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update video processing status
|
|
* @param string $videoKey Video key
|
|
* @param string $status Status
|
|
* @param string $error Error message (optional)
|
|
*/
|
|
private function updateVideoStatus($videoKey, $status, $error = null)
|
|
{
|
|
try {
|
|
$db = VDatabase::getInstance();
|
|
|
|
$updateData = [
|
|
'processing_status' => $status,
|
|
'processed_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
if ($error) {
|
|
$updateData['processing_error'] = $error;
|
|
}
|
|
|
|
$db->doUpdate('db_videofiles', 'file_key', $updateData, $videoKey);
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to update video status', [
|
|
'video_key' => $videoKey,
|
|
'status' => $status,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper methods
|
|
*/
|
|
private function findFFmpegPath()
|
|
{
|
|
$paths = ['/usr/bin/ffmpeg', '/usr/local/bin/ffmpeg', 'ffmpeg'];
|
|
|
|
foreach ($paths as $path) {
|
|
if (shell_exec("which {$path}") || file_exists($path)) {
|
|
return $path;
|
|
}
|
|
}
|
|
|
|
return 'ffmpeg'; // Fallback to PATH
|
|
}
|
|
|
|
private function findFFprobePath()
|
|
{
|
|
$paths = ['/usr/bin/ffprobe', '/usr/local/bin/ffprobe', 'ffprobe'];
|
|
|
|
foreach ($paths as $path) {
|
|
if (shell_exec("which {$path}") || file_exists($path)) {
|
|
return $path;
|
|
}
|
|
}
|
|
|
|
return 'ffprobe'; // Fallback to PATH
|
|
}
|
|
|
|
private function ensureDirectories()
|
|
{
|
|
$dirs = [$this->tempDir, $this->outputDir];
|
|
|
|
foreach ($dirs as $dir) {
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function calculateFPS($videoStream)
|
|
{
|
|
if (isset($videoStream['r_frame_rate'])) {
|
|
$parts = explode('/', $videoStream['r_frame_rate']);
|
|
if (count($parts) === 2 && $parts[1] > 0) {
|
|
return round($parts[0] / $parts[1], 2);
|
|
}
|
|
}
|
|
|
|
return 30; // Default FPS
|
|
}
|
|
|
|
private function calculateBufferSize($bitrate)
|
|
{
|
|
$bitrateNum = (int)str_replace('k', '', $bitrate);
|
|
return ($bitrateNum * 2) . 'k'; // 2x bitrate for buffer
|
|
}
|
|
|
|
private function calculateBandwidth($bitrate)
|
|
{
|
|
return (int)str_replace('k', '', $bitrate) * 1000;
|
|
}
|
|
} |