Files
easystream/f_core/f_classes/class.videoprocessor.php
T
SamiAhmed7777 0b7e2d0a5b feat: Add comprehensive documentation suite and reorganize project structure
- 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
2025-10-21 00:39:45 -07:00

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;
}
}