diff --git a/prep-set b/prep-set new file mode 100755 index 0000000..3522348 --- /dev/null +++ b/prep-set @@ -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 diff --git a/prep-vid b/prep-vid new file mode 100755 index 0000000..2c127bf --- /dev/null +++ b/prep-vid @@ -0,0 +1,298 @@ +#!/usr/bin/env php + [ + '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...] \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(); + } + } +}