#!/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 = [
    '480p.sdr.thumb.jpg' => [
        'width' => 854,
        'height' => 480,
        'codec' => 'mjpeg',
        'still' => true,
    ],
    '1080p.sdr.h264.mp4' => [
        'width' => 1920,
        'height' => 1080,
        'codec' => 'libx264',
        'bitrate' => '5000k',
    ],
    '1080p.hdr.vp9.webm' => [
        'width' => 1920,
        'height' => 1080,
        'codec' => 'libvpx-vp9',
        'bitrate' => '5000k',
        'hdr' => true,
    ],
    '1080p.hdr.av1.webm' => [
        'width' => 1920,
        'height' => 1080,
        'codec' => 'libsvtav1',
        'bitrate' => '4000k',
        'hdr' => true,
    ],
    '2160p.hdr.av1.webm' => [
        'width' => 3840,
        'height' => 2160,
        'codec' => 'libsvtav1',
        'bitrate' => '25000k',
        'hdr' => true,
    ],
];

$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 ) );
    $keyframeInt = 5 * 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 ( $codec === 'libvpx-vp9' ) {
        $codecOptions = [
            //'-speed', 4,
            '-row-mt', 1,
            '-tile-columns', 2,
            '-cues_to_front', 1,
        ];
    } else if ( $codec === 'libsvtav1' ) {
        $codecOptions = [
            //'-preset', 4,
            '-svtav1-params', 'tile_columns=2',
            '-cues_to_front', 1,
        ];
    } else if ( $codec === 'libx264' ) {
        $codecOptions = [
            '-movflags', '+faststart',
        ];
    } else {
        $codecOptions = [];
    }

    if ( $still ) {
        run( 'ffmpeg',
            array_merge( [
                '-i', $src,
                '-vf', $vf,
                '-c:v', $codec,
                '-q:v', 0.95,
                '-update', 1,
                '-frames:v', 1,
                '-an',
                '-y', $dest
            ], $codecOptions )
        );
    } 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, $codecOptions, [
                '-y', '/dev/null'
            ] )
        );
        run( 'ffmpeg',
            array_merge( [
                '-i', $src,
                '-vf', $vf,
                '-c:v', $codec,
                '-b:v', $bitrate,
                '-pass', '2',
                '-passlogfile', $passlog,
                '-g', $keyframeInt,
            ], $audio, $codecOptions, [
                '-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();
        }
    }
}