Last active
December 12, 2025 14:36
-
-
Save chrispage1/aff8ecded798baf2d1aec89885cc5a77 to your computer and use it in GitHub Desktop.
PHP - FFMPEG Conversion Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| declare(strict_types=1); | |
| // usage (arguments in square braces are required) | |
| // php convert.php [filename.mp4] outputDir --noAudio --profiles=1080p,720p --segmentDuration=10 | |
| // load our CLI arguments | |
| $inputFile = $argv[1] ?? null; | |
| $outputDir = $argv[2] ?? 'output'; | |
| $outputDir = str_starts_with($outputDir, '--') ? 'output' : $outputDir; | |
| // make sure we have the input video | |
| if (! file_exists($inputFile)) { | |
| echo "❌ The video file you specified doesn't exist\n"; | |
| exit(1); | |
| } | |
| // make sure our input is an mp4 file | |
| if (! str_ends_with($inputFile, '.mp4')) { | |
| echo "❌ The video file must be in .mp4 format\n"; | |
| exit(1); | |
| } | |
| // extract additional arguments. | |
| $arguments = []; | |
| foreach ($argv as $index => $arg) { | |
| if (str_starts_with($arg, '--')) { | |
| $parts = explode('=', substr($arg, 2), 2); | |
| $arguments[$parts[0]] = $value = $parts[1] ?? true; | |
| } | |
| } | |
| // argument helpers | |
| $hasArgument = fn (string $arg): bool => array_key_exists($arg, $arguments); | |
| $argument = fn (string $arg, mixed $default = null) => $arguments[$arg] ?? $default; | |
| // make sure we have our output directory | |
| if (! is_dir($outputDir)) { | |
| mkdir($outputDir); | |
| } elseif (count(glob($outputDir.'/*')) > 0) { | |
| echo "❌ The $outputDir directory isn't empty\n"; | |
| exit(1); | |
| } | |
| // define our arguments for processing | |
| $withAudio = ! $hasArgument('noAudio'); // whether we have the --noAudio flag | |
| $segmentDuration = $argument('segmentDuration', 3); // the duration of each chunk | |
| // determine the profiles we'll encode into | |
| $presets = explode(',', $argument('profiles', '1080p,720p,480p')); | |
| $presets = array_map('trim', $presets); | |
| // define our encoding presets | |
| // structure: [Width, Bitrate (k), MaxRate (k), BufferSize (k), Profile, Level, Audio Bitrate (k)] | |
| $availablePresets = [ | |
| '240p' => [426, 400, 420, 600, 'main', '3.0', 48], | |
| '360p' => [640, 600, 630, 900, 'main', '3.0', 64], | |
| '480p' => [854, 1000, 1050, 1500, 'main', '3.1', 64], | |
| '720p' => [1280, 2500, 2650, 3750, 'high', '4.0', 96], // HD | |
| '1080p' => [1920, 5000, 5300, 7500, 'high', '4.1', 128], // FHD | |
| '1440p' => [2560, 9000, 9500, 13500, 'high', '4.2', 192], // QHD | |
| '2160p' => [3840, 16000, 17000, 24000, 'high', '5.1', 192], // 4K UHD | |
| ]; | |
| // determine the presets we'll use based on our supplied profiles | |
| $activePresets = array_filter($availablePresets, fn (string $preset): bool => in_array($preset, $presets, true), ARRAY_FILTER_USE_KEY); | |
| if (count($activePresets) === 0) { | |
| echo '❌ You need to choose at least one profile from: '.implode(', ', array_keys($availablePresets))."\n"; | |
| exit(1); | |
| } | |
| // build our command | |
| $command = implode(' ', ['ffmpeg', '-i', escapeshellarg($inputFile)]); | |
| $streamIndex = 0; | |
| $maps = []; | |
| // --- 3. Iterate Profiles and Append Stream Options --- | |
| foreach ($activePresets as $name => $p) { | |
| [$width, $bitRate, $maxRate, $bufferSize, $profile, $level, $audioBitrate] = $p; | |
| $streamDir = "{$outputDir}/{$name}"; | |
| $audioBitrate = $withAudio ? $audioBitrate : 0; | |
| // create our stream output directory | |
| if (! is_dir($streamDir)) { | |
| mkdir($streamDir, 0777, true); | |
| } | |
| // define our video stream map | |
| $mapName = implode('', ["v:$streamIndex", $withAudio ? ",a:$streamIndex" : null, ",name:$name"]); | |
| $maps[$mapName] = [ | |
| '0:v:0', // input 0, output 0 | |
| $withAudio ? '-map 0:a:0' : null, | |
| "-b:v:$streamIndex", "{$bitRate}k", // bitrate | |
| "-vcodec:v:$streamIndex", 'libx264', // codec | |
| "-maxrate:v:$streamIndex", "{$maxRate}k", // maxrate | |
| "-bufsize:v:$streamIndex", "{$bufferSize}k", // buffer size | |
| "-filter:v:$streamIndex", "scale=$width:-2", // width | |
| "-profile:v:$streamIndex", $profile, // profile | |
| "-level:v:$streamIndex", $level, // level | |
| ]; | |
| if ($withAudio) { | |
| array_push($maps[$mapName], | |
| "-c:a:$streamIndex", | |
| 'aac',// AAC audio codec | |
| "-b:a:$streamIndex", | |
| "{$audioBitrate}k",// audio bitrate | |
| '-ar 48000' | |
| ); | |
| } | |
| // increment our index for the next item. | |
| $streamIndex++; | |
| } | |
| // output our first frame as poster.jpg | |
| $command .= " -map 0:v:0 -frames:v 1 -filter:v scale=1920:-1 -q:v 2 " . escapeshellarg($outputDir . '/poster.jpg'); | |
| foreach ($maps as $values) { | |
| // add each map to our command | |
| $command .= ' -map '.implode(' ', $values); | |
| } | |
| // build up the remainder of our command including the stream map | |
| $command .= " -f hls -hls_playlist_type vod -hls_flags independent_segments -hls_time $segmentDuration -hls_list_size 0"; | |
| $command .= ' -hls_segment_filename ' . escapeshellarg($outputDir . '/%v/segment_%03d.ts'); | |
| $command .= ' -var_stream_map '.escapeshellarg(implode(' ', array_keys($maps))); | |
| $command .= ' -master_pl_name output.m3u8'; | |
| $command .= " '$outputDir/%v/output.m3u8'"; | |
| // output our ffmpeg command | |
| echo "--- Generated Command --- \n\n"; | |
| echo $command."\n\n"; | |
| // now execute our command | |
| echo "--- Compiling ---\n"; | |
| $output = []; | |
| $returnCode = 0; | |
| exec($command.' 2>&1', $output, $returnCode); | |
| if ($returnCode === 0) { | |
| echo "✅ Success! HLS streams generated in the '{$outputDir}' directory.\n"; | |
| echo "Master playlist: $outputDir/output.m3u8\n"; | |
| echo "You can view the output by serving this directory with a web server.\n"; | |
| } else { | |
| echo "❌ Error during FFmpeg execution (Return Code: {$returnCode}).\n"; | |
| echo "FFmpeg Output:\n"; | |
| echo implode("\n", $output); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment