pack-vid/pack-vid

180 lines
4.3 KiB
PHP
Executable file

#!/usr/bin/env php
<?php
// Squishes given video input into sub-4000kb .mp4
// Crops or pads to 16:9 (crop default; --letterbox to pad)
// Strips audio (to override, pass --audio for 96 kbps AAC)
// HDR to SDR tonemapping
// Picks bitrate to match
// Picks resolution based on bitrate target
// 2-pass encoding with libx264 veryslow
$args = $_SERVER['argv'];
$self = array_shift( $args );
$options = [
'letterbox' => false,
'audio' => false,
];
while ( count( $args ) > 0 && substr( $args[0], 0, 2 ) == '--' ) {
$option = substr( array_shift( $args ), 2 );
$options[$option] = true;
}
if ( count ( $args ) < 2 ) {
die(
"Usage: $self [options...] <srcfile.mp4> <destfile.mp4>\n" .
"Options:\n" .
" --letterbox pad instead of cropping\n" .
" --audio include audio\n"
);
}
[ $src, $dest ] = $args;
convert( $src, $dest, $options );
exit(0);
//
function run( $cmd, $args ) {
$commandLine = implode( ' ',
array_merge(
[ escapeshellcmd( $cmd ) ],
array_map( 'escapeshellarg', $args )
)
);
echo "$commandLine\n";
$output = shell_exec( $commandLine );
if ( $output === false ) {
throw new Error( "Failed to run $cmd" );
}
return $output;
}
function ffprobe( $path ) {
$output = run( 'ffprobe', [
'-hide_banner',
'-show_format',
'-show_streams',
'-print_format',
'json',
'--',
$path
] );
$data = json_decode( $output );
if ( $data === null ) {
throw new Error( "Failed to read JSON from ffprobe: $output" );
}
return $data;
}
function evenize( $n ) {
$n = ceil( $n );
if ( $n & 1 ) {
$n++;
}
return $n;
}
function convert( $src, $dest, $options ) {
$maxBits = 4000 * 1000 * 8; // fit in 4Mb
$maxBits = $maxBits * 7 / 8; // leave some headroom
$probe = ffprobe( $src );
$videoTracks = array_filter( $probe->streams, function ( $stream ) {
return $stream->codec_type === 'video';
} );
$track = $videoTracks[0];
$duration = floatval( $track->duration );
$width = $track->width;
$height = $track->height;
$hdr = $track->color_primaries === 'bt2020';
$keyframeInt = ceil( $duration * 60 );
$bitrate = floor( $maxBits / $duration );
if ( $options[ 'audio' ] ) {
$audioBitrate = 96 * 1000;
$audio = [ '-b:a', $audioBitrate ];
$bitrate -= $audioBitrate;
} else {
$audio = [ '-an' ];
}
$mbits = 1000 * 1000;
if ( $bitrate < $mbits ) {
$frameWidth = 640;
$frameHeight = 360;
} elseif ( $bitrate < 2 * $mbits ) {
$frameWidth = 854;
$frameHeight = 480;
} elseif ( $bitrate < 4 * $mbits ) {
$frameWidth = 1280;
$frameHeight = 720;
} else {
$frameWidth = 1920;
$frameHeight = 1080;
}
if ( $options['letterbox'] ) {
$scaleWidth = $frameWidth;
$scaleHeight = evenize( $height * $frameWidth / $width );
} else {
$scaleHeight = $frameHeight;
$scaleWidth = evenize( $width * $frameHeight / $height );
}
$filters = [ "scale=w=$scaleWidth:h=$scaleHeight" ];
if ( $hdr ) {
$filters[] = "zscale=t=linear:p=bt709";
$filters[] = "tonemap=hable:peak=8";
$filters[] = "zscale=t=bt709:m=bt709:r=full";
}
$filters[] = "format=yuv420p";
if ( $options['letterbox'] ) {
$offset = round( ( $frameHeight - $scaleHeight) / 2 );
$filters[] = "pad=h=$frameHeight:y=$offset";
} else {
$filters[] = "crop=w=$frameWidth";
}
$vf = implode( ',', $filters );
$fps = 30;
run( 'ffmpeg',
array_merge( [
'-i', $src,
'-f', 'mp4',
'-r', $fps,
'-vf', $vf,
'-c:v', 'libx264',
'-b:v', $bitrate,
'-preset', 'veryslow',
'-pass', '1',
'-g', $keyframeInt,
], $audio, [
'-y', '/dev/null'
] )
);
run( 'ffmpeg',
array_merge( [
'-i', $src,
'-vf', $vf,
'-r', $fps,
'-c:v', 'libx264',
'-b:v', $bitrate,
'-preset', 'veryslow',
'-pass', '2',
'-g', $keyframeInt,
], $audio, [
'-movflags', '+faststart',
'-y', $dest
] )
);
}