extractWithFfprobe($filePath); if (empty($metadata)) { // Fallback to basic file info error_log("VideoMetadata: ffprobe not available, using basic info"); $metadata = $this->extractBasicInfo($filePath); } return $metadata; } /** * Extract metadata using ffprobe */ private function extractWithFfprobe($filePath) { // Check if ffprobe is available exec('which ffprobe 2>&1', $output, $returnCode); if ($returnCode !== 0) { return []; } $command = 'ffprobe -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($filePath); exec($command, $output, $returnCode); if ($returnCode !== 0) { return []; } $json = implode('', $output); $data = json_decode($json, true); if (!$data) { return []; } // Extract video stream info $videoStream = null; if (isset($data['streams'])) { foreach ($data['streams'] as $stream) { if ($stream['codec_type'] === 'video') { $videoStream = $stream; break; } } } $format = $data['format'] ?? []; $metadata = [ 'width' => $videoStream['width'] ?? -1, 'height' => $videoStream['height'] ?? -1, 'duration' => $format['duration'] ?? null, 'duration_formatted' => null, 'bitrate' => $format['bit_rate'] ?? null, 'aspect_ratio' => null, 'aspect_ratio_formatted' => null, 'frame_rate' => $videoStream['r_frame_rate'] ?? null, 'codec' => $videoStream['codec_name'] ?? null ]; // Calculate aspect ratio if ($metadata['width'] > 0 && $metadata['height'] > 0) { $aspectDecimal = $metadata['width'] / $metadata['height']; $metadata['aspect_ratio'] = round($aspectDecimal, 3); // Format as ratio (16:9, 4:3, etc.) $metadata['aspect_ratio_formatted'] = $this->formatAspectRatio($metadata['width'], $metadata['height']); } // Format duration as HH:MM:SS:FF (timecode) if ($metadata['duration']) { $metadata['duration_formatted'] = $this->formatDuration($metadata['duration']); } error_log("VideoMetadata: Extracted - {$metadata['width']}x{$metadata['height']}, {$metadata['duration']}s, {$metadata['bitrate']} bps"); return $metadata; } /** * Extract basic info without ffprobe */ private function extractBasicInfo($filePath) { return [ 'width' => -1, 'height' => -1, 'duration' => null, 'bitrate' => null, 'aspect_ratio' => null, 'aspect_ratio_formatted' => null ]; } /** * Format aspect ratio as standard ratio string (16:9, 4:3, etc.) */ private function formatAspectRatio($width, $height) { if ($width <= 0 || $height <= 0) { return null; } // Common ratios $commonRatios = [ '16:9' => 1.778, '4:3' => 1.333, '1:1' => 1.0, '21:9' => 2.333, '2.35:1' => 2.35, '2.39:1' => 2.39 ]; $aspectDecimal = $width / $height; // Find closest match $closest = '16:9'; $minDiff = abs($aspectDecimal - 1.778); foreach ($commonRatios as $ratio => $value) { $diff = abs($aspectDecimal - $value); if ($diff < $minDiff) { $minDiff = $diff; $closest = $ratio; } } // If very close to a common ratio, use it if ($minDiff < 0.01) { return $closest; } // Otherwise calculate GCD for exact ratio $gcd = $this->gcd($width, $height); return ($width / $gcd) . ':' . ($height / $gcd); } /** * Greatest common divisor */ private function gcd($a, $b) { return $b ? $this->gcd($b, $a % $b) : $a; } /** * Format duration in seconds to HH:MM:SS:FF timecode */ private function formatDuration($seconds) { $hours = floor($seconds / 3600); $minutes = floor(($seconds % 3600) / 60); $secs = floor($seconds % 60); $frames = round(($seconds - floor($seconds)) * 30); // Assume 30fps return sprintf('%02d:%02d:%02d:%02d', $hours, $minutes, $secs, $frames); } }