wip
This commit is contained in:
parent
4d2a4059da
commit
7b47c441fb
2 changed files with 329 additions and 0 deletions
31
prep-set
Executable file
31
prep-set
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
OPTS=""
|
||||
|
||||
for INFILE in "$@"
|
||||
do
|
||||
if [[ "$1" =~ ^--.* ]]
|
||||
then
|
||||
echo "OPTION: $1"
|
||||
OPTS="$OPTS $1"
|
||||
shift
|
||||
else
|
||||
echo "FILE: $INFILE"
|
||||
|
||||
MAPPED="prep-set-${INFILE%.mp4}.2160p.sdr.h264.mp4"
|
||||
prep-vid $OPTS "$INFILE" "$MAPPED"
|
||||
|
||||
prep-vid $OPTS "$MAPPED" "${INFILE%.mp4}.360p.sdr.thumb.jpg"
|
||||
prep-vid $OPTS "$INFILE" "${INFILE%.mp4}.360p.hdr.thumb.avif"
|
||||
|
||||
prep-vid $OPTS "$MAPPED" "${INFILE%.mp4}.720p.sdr.h264.mp4"
|
||||
prep-vid $OPTS "$INFILE" "${INFILE%.mp4}.720p.hdr.hevc.mp4"
|
||||
|
||||
prep-vid $OPTS "$MAPPED" "${INFILE%.mp4}.1080p.sdr.av1.webm"
|
||||
prep-vid $OPTS "$INFILE" "${INFILE%.mp4}.1080p.hdr.av1.webm"
|
||||
|
||||
prep-vid $OPTS "$MAPPED" "${INFILE%.mp4}.2160p.sdr.av1.webm"
|
||||
prep-vid $OPTS "$INFILE" "${INFILE%.mp4}.2160p.hdr.av1.webm"
|
||||
fi
|
||||
done
|
298
prep-vid
Executable file
298
prep-vid
Executable file
|
@ -0,0 +1,298 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
// Squishes given video input into sub-4000kb .mp4
|
||||
// Crops or pads to 16:9 (letterbox default; --crop to crop)
|
||||
// Includes stereo audio (to strip, pass --no-audio)
|
||||
// HDR to SDR tonemapping
|
||||
// Picks bitrate to match
|
||||
// Picks resolution based on bitrate target
|
||||
// 2-pass encoding with libx264 veryslow
|
||||
// fps limiter: warning requires newer ffmpeg than debian-buster
|
||||
|
||||
$args = $_SERVER['argv'];
|
||||
$self = array_shift( $args );
|
||||
|
||||
$profiles = [
|
||||
'360p.sdr.thumb.jpg' => [
|
||||
'width' => 640,
|
||||
'height' => 360,
|
||||
'codec' => 'mjpeg',
|
||||
'bitrate' => '4000k',
|
||||
'still' => true,
|
||||
],
|
||||
'360p.hdr.thumb.avif' => [
|
||||
'width' => 640,
|
||||
'height' => 360,
|
||||
'codec' => 'libsvtav1',
|
||||
'bitrate' => '2000k',
|
||||
'hdr' => true,
|
||||
'still' => true,
|
||||
],
|
||||
'2160p.hdr.av1.webm' => [
|
||||
'width' => 3840,
|
||||
'height' => 2160,
|
||||
'codec' => 'libsvtav1',
|
||||
'bitrate' => '25000k',
|
||||
'hdr' => true,
|
||||
],
|
||||
'2160p.sdr.av1.webm' => [
|
||||
'width' => 3840,
|
||||
'height' => 2160,
|
||||
'codec' => 'libsvtav1',
|
||||
'bitrate' => '20000k',
|
||||
],
|
||||
'2160p.sdr.h264.mp4' => [
|
||||
'width' => 3840,
|
||||
'height' => 2160,
|
||||
'codec' => 'libx264',
|
||||
'bitrate' => '50000k',
|
||||
],
|
||||
'1080p.hdr.av1.webm' => [
|
||||
'width' => 1920,
|
||||
'height' => 1080,
|
||||
'codec' => 'libsvtav1',
|
||||
'bitrate' => '6000k',
|
||||
'hdr' => true,
|
||||
],
|
||||
'1080p.sdr.av1.webm' => [
|
||||
'width' => 1920,
|
||||
'height' => 1080,
|
||||
'codec' => 'libsvtav1',
|
||||
'bitrate' => '4800k',
|
||||
],
|
||||
'720p.hdr.hevc.mp4' => [
|
||||
'width' => 1280,
|
||||
'height' => 720,
|
||||
'codec' => 'libx265',
|
||||
'bitrate' => '4800k',
|
||||
'hdr' => true,
|
||||
],
|
||||
'720p.sdr.h264.mp4' => [
|
||||
'width' => 1280,
|
||||
'height' => 720,
|
||||
'codec' => 'libx264',
|
||||
'bitrate' => '4800k',
|
||||
],
|
||||
];
|
||||
|
||||
$options = [
|
||||
'no-audio' => false,
|
||||
'exposure' => '0', // stops
|
||||
'peak' => '1000', // '10000' is max
|
||||
'preset' => 'medium',
|
||||
'vibrance' => 0,
|
||||
];
|
||||
|
||||
while ( count( $args ) > 0 && substr( $args[0], 0, 2 ) == '--' ) {
|
||||
$option = substr( array_shift( $args ), 2 );
|
||||
$parts = explode( '=', $option, 2 );
|
||||
if ( count( $parts ) == 2 ) {
|
||||
[ $key, $val ] = $parts;
|
||||
$options[$key] = $val;
|
||||
} else {
|
||||
$options[$option] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( count ( $args ) < 2 ) {
|
||||
die(
|
||||
"Usage: $self [options...] <srcfile.mp4> <destfile.mp4>\n" .
|
||||
"Options:\n" .
|
||||
" --no-audio strip audio\n" .
|
||||
" --exposure=n adjust exposure\n" .
|
||||
" --peak=n set HDR peak nits\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 sizify( $str ) {
|
||||
$matches = [];
|
||||
if ( preg_match( '/^(\d+(?:\.\d+)?)([kmgt]?)$/i', $str, $matches ) ) {
|
||||
[ , $digits, $suffix ] = $matches;
|
||||
$n = floatval( $digits );
|
||||
switch ( strtolower( $suffix ) ) {
|
||||
case 't': $n *= 1000; // fall through
|
||||
case 'g': $n *= 1000; // fall through
|
||||
case 'm': $n *= 1000; // fall through
|
||||
case 'k': $n *= 1000; // fall through
|
||||
default: return $n;
|
||||
}
|
||||
return $n;
|
||||
}
|
||||
die( "Unexpected size format '$str'\n" );
|
||||
}
|
||||
|
||||
function extractTracks( $streams, $type ) {
|
||||
return array_values(
|
||||
array_filter( $streams, function ( $stream ) use ( $type ) {
|
||||
return $stream->codec_type === $type;
|
||||
} )
|
||||
);
|
||||
}
|
||||
|
||||
function convert( $src, $dest, $options ) {
|
||||
global $profiles;
|
||||
$probe = ffprobe( $src );
|
||||
|
||||
$videoTracks = extractTracks( $probe->streams, 'video' );
|
||||
$audioTracks = extractTracks( $probe->streams, 'audio' );
|
||||
|
||||
if ( count( $videoTracks ) == 0 ) {
|
||||
var_dump( $probe );
|
||||
die("oh no\n");
|
||||
}
|
||||
$track = $videoTracks[0];
|
||||
|
||||
$duration = floatval( $probe->format->duration );
|
||||
$width = $track->width;
|
||||
$height = $track->height;
|
||||
// @fixme some files are missing this? trims from qt?
|
||||
//$hdr = $track->color_primaries === 'bt2020' || $options['hdr'];
|
||||
// pix_fmt: "yuv420p10le"
|
||||
$hdr = substr( $track->pix_fmt, -5 ) === 'p10le' || $options['hdr'];
|
||||
$keyframeInt = intval( ceil( $duration * 60 ) );
|
||||
|
||||
|
||||
if (!preg_match( '/(\d+p)\.(.*?)$/', $dest, $matches ) ) {
|
||||
die('nooo');
|
||||
}
|
||||
$profile = $matches[1] . '.' . $matches[2];
|
||||
|
||||
$codec = $profiles[$profile]['codec'];
|
||||
$bitrate = $profiles[$profile]['bitrate'];
|
||||
$scaleWidth = $profiles[$profile]['width'];
|
||||
$scaleHeight = $profiles[$profile]['height'];
|
||||
$tonemap = $hdr && !( $profiles[$profile]['hdr'] ?? false );
|
||||
$still = $profiles[$profile]['still'] ?? false;
|
||||
|
||||
if ( $still || $options[ 'no-audio' ] || count( $audioTracks ) == 0 ) {
|
||||
$audio = [ '-an' ];
|
||||
} else {
|
||||
$audio = [];
|
||||
}
|
||||
|
||||
|
||||
if (!$codec ) {
|
||||
die('no');
|
||||
}
|
||||
|
||||
$exposure = floatval( $options['exposure'] );
|
||||
$peakNits = floatval( $options['peak'] );
|
||||
$sdrNits = 80;
|
||||
$peak = $peakNits / $sdrNits;
|
||||
$vibrance = floatval( $options['vibrance'] );
|
||||
|
||||
$filters = [];
|
||||
$filters[] = "scale=w=$scaleWidth:h=$scaleHeight";
|
||||
if ( $tonemap) {
|
||||
$filters[] = "zscale=t=linear";
|
||||
if ( $exposure ) {
|
||||
$filters[] = "exposure=$exposure";
|
||||
}
|
||||
$filters[] = "tonemap=hable:peak=$peak:desat=0.0";
|
||||
$filters[] = "zscale=tin=linear:t=709:p=709:m=709:r=full:dither=ordered";
|
||||
if ( $vibrance ) {
|
||||
$filters[] = "vibrance=$vibrance";
|
||||
}
|
||||
$filters[] = "format=yuv420p";
|
||||
}
|
||||
$vf = implode( ',', $filters );
|
||||
|
||||
if ( $still ) {
|
||||
run( 'ffmpeg', [
|
||||
'-i', $src,
|
||||
'-vf', $vf,
|
||||
'-c:v', $codec,
|
||||
'-b:v', $bitrate,
|
||||
'-update', 1,
|
||||
'-frames:v', 1,
|
||||
'-an',
|
||||
'-y', $dest
|
||||
] );
|
||||
} else {
|
||||
$tempPrefix = 'pack-vid-passlog' . rand(0,1 << 31);
|
||||
$passlog = tempnam( '.', $tempPrefix );
|
||||
|
||||
run( 'ffmpeg',
|
||||
array_merge( [
|
||||
'-i', $src,
|
||||
'-f', 'null',
|
||||
'-vf', $vf,
|
||||
'-c:v', $codec,
|
||||
'-b:v', $bitrate,
|
||||
'-pass', '1',
|
||||
'-passlogfile', $passlog,
|
||||
'-g', $keyframeInt,
|
||||
], $audio, [
|
||||
'-y', '/dev/null'
|
||||
] )
|
||||
);
|
||||
run( 'ffmpeg',
|
||||
array_merge( [
|
||||
'-i', $src,
|
||||
'-vf', $vf,
|
||||
'-c:v', $codec,
|
||||
'-b:v', $bitrate,
|
||||
'-pass', '2',
|
||||
'-passlogfile', $passlog,
|
||||
'-g', $keyframeInt,
|
||||
], $audio, [
|
||||
'-movflags', '+faststart',
|
||||
'-y', $dest
|
||||
] )
|
||||
);
|
||||
|
||||
$len = strlen( $tempPrefix );
|
||||
if ( $len > 0 ) {
|
||||
$dir = dir( './' );
|
||||
for ( $entry = $dir->read(); $entry !== false; $entry = $dir->read() ) {
|
||||
if ( substr( $entry, 0, $len ) === $tempPrefix ) {
|
||||
print "...deleting temp file: $entry\n";
|
||||
unlink( $entry );
|
||||
}
|
||||
}
|
||||
$dir->close();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue