New Features:
1. VideoMetadataExtractor class
- Uses ffprobe to extract video technical metadata
- Width, height, duration, bitrate, aspect ratio
- Frame rate, codec info
- Formatted duration (HH:MM:SS:FF timecode)
- Formatted aspect ratio (16:9, 4:3, etc.)
2. Updated MetadataExtractorMVP
- Now accepts local file path parameter
- Extracts video metadata from downloaded file
- Updates fields from V2 filename:
* ARTESIA.FIELD.ASSET DESCRIPTION ← subject_title from filename
* FERRERO.FIELD.STATE ← Always "Local" for uploads
* FERRERO.FIELD.MKTG.ASSET TYPE ← asset_type from filename
* MAIN_LANGUAGES ← language_code from filename
- Adds FERRERO.MARKETING.CREATOR ← "Oliver Agency"
- Logs video dimensions and technical data
3. Workflow integration
- Pass local file path to buildMVPAssetRepresentation()
- Video analysis happens after Box download
- File metadata extracted before upload
Field Updates Summary:
✅ Description → From filename (TEST-JOB11)
✅ State → Local (forced)
✅ Creator → Oliver Agency
✅ Main Languages → From filename (de → DE)
✅ Asset Type → From filename (OLV)
✅ Video metadata extracted (width, height, duration, bitrate)
Next: Test ffprobe extraction and determine where to inject video
technical fields in upload structure (content_info vs metadata).
🤖 Generated with Claude Code
194 lines
5.4 KiB
PHP
194 lines
5.4 KiB
PHP
<?php
|
|
|
|
/**
|
|
* VideoMetadataExtractor - Extract technical metadata from video files
|
|
*
|
|
* Uses ffprobe to extract:
|
|
* - Duration
|
|
* - Bitrate
|
|
* - Frame dimensions (width/height)
|
|
* - Aspect ratio
|
|
* - Frame rate
|
|
* - Codec info
|
|
*/
|
|
class VideoMetadataExtractor
|
|
{
|
|
/**
|
|
* Extract video metadata using ffprobe
|
|
*
|
|
* @param string $filePath Path to video file
|
|
* @return array Video metadata or empty array if not video/failed
|
|
*/
|
|
public function extractVideoMetadata($filePath)
|
|
{
|
|
if (!file_exists($filePath)) {
|
|
error_log("VideoMetadata: File not found: $filePath");
|
|
return [];
|
|
}
|
|
|
|
$mimeType = mime_content_type($filePath);
|
|
if (strpos($mimeType, 'video') === false) {
|
|
error_log("VideoMetadata: Not a video file: $mimeType");
|
|
return [];
|
|
}
|
|
|
|
// Try ffprobe first (most accurate)
|
|
$metadata = $this->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);
|
|
}
|
|
}
|