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