#!/usr/bin/env php 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...] \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 ); } $peakNits = 2000; $sdrNits = 80; $peak = $peakNits / $sdrNits; $filters = [ "scale=w=$scaleWidth:h=$scaleHeight" ]; if ( $hdr ) { $filters[] = "zscale=t=linear:p=bt709"; $filters[] = "tonemap=hable:peak=$peak"; $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; $passlog = tempnam( '.', 'pack-vid-passlog' ); run( 'ffmpeg', array_merge( [ '-i', $src, '-f', 'mp4', '-r', $fps, '-vf', $vf, '-c:v', 'libx264', '-b:v', $bitrate, '-preset', 'veryslow', '-pass', '1', '-passlogfile', $passlog, '-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', '-passlogfile', $passlog, '-g', $keyframeInt, ], $audio, [ '-movflags', '+faststart', '-y', $dest ] ) ); }