ferrero-opentext/src/VideoMetadataExtractor.php
DJP 5b050c2483 Add field updates from V2 filename and video file analysis
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
2025-10-30 14:57:54 -04:00

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