Skip to content

Instantly share code, notes, and snippets.

@chrispage1
Last active December 12, 2025 14:36
Show Gist options
  • Select an option

  • Save chrispage1/aff8ecded798baf2d1aec89885cc5a77 to your computer and use it in GitHub Desktop.

Select an option

Save chrispage1/aff8ecded798baf2d1aec89885cc5a77 to your computer and use it in GitHub Desktop.
PHP - FFMPEG Conversion Script
<?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