<?php

function run( $prog, $params ) {
    $cmd = escapeshellcmd( $prog ) . " " . implode( ' ', array_map( 'escapeshellarg', $params ) );
    echo "\n$cmd\n\n";
    $output = [];
    $code = 0;
    if ( exec($cmd, $output, $code) === false ) {
        throw new Exception( 'failed to exec ffmpeg' );
    }
    if ( $code ) {
        throw new Exception( "ffmpeg returned coded $code" );
    }
    return $output;
}

function ffprobe( $file ) {
    $output = run( 'ffprobe', [ '-hide_banner', '-show_format', '-show_streams', '-print_format', 'json', '--', $file ] );
    $json = implode( "\n", $output );
    return json_decode( $json );
}

class Audio {
    public const FORMATS = [
        'aac' => [
            'options' => [
                '-acodec', 'aac',
                '-ar', 44100,
                '-ac', 2,
                '-b:a', '112k',
            ],
        ],
        'opus' => [
            'options' => [
                '-acodec', 'libopus',
                '-ar', 48000,
                '-ac', 2,
                '-b:a', '96k',
            ],
        ],
        'mp3' => [
            'options' => [
                '-acodec', 'libmp3lame',
                '-ar', 44100,
                '-ac', 2,
                '-b:a', '128k',
            ],
        ],
        'alac' => [
            'options' => [
                '-acodec', 'alac',
                '-ar', 11025,
                '-ac', 2,
            ],
        ],
        'vorbis' => [
            'options' => [
                '-acodec', 'libvorbis',
                '-ar', 44100,
                '-ac', 2,
                '-b:a', '112k',
            ],
        ],
    ];
}

class Video {
    // Normalize input frame rates to the next up of these.
    // Lets us ensure that keyframes are places where they belong.
    public const RATES = [
        15, 24, 25, 30, 48, 50, 60
    ];

    public const FORMATS = [
        'vp9' => [
            'options' => [
                'common' => [
                    '-vcodec', 'libvpx-vp9',
                    '-row-mt', '1',
                ],
                'fast' => [
                    '-quality', 'realtime',
                    '-cpu-used', '5',
                ],
                'pass1' => [
                    '-quality', 'good',
                    '-cpu-used', '2',
                    '-pass', '1',
                ],
                'pass2' => [
                    '-quality', 'good',
                    '-cpu-used', '1',
                    '-pass', '2',
                ]
            ],
            'resolutions' => [
                '240p' => [
                    'width' => 426,
                    'height' => 240,
                    'bitrate' => '150k',
                ],
                '360p' => [
                    'width' => 640,
                    'height' => 360,
                    'bitrate' => '250k',
                ],
                '480p' => [
                    'width' => 854,
                    'height' => 480,
                    'bitrate' => '750k',
                ],
                '720p' => [
                    'width' => 1280,
                    'height' => 720,
                    'bitrate' => '2500k',
                ],
                '1080p' => [
                    'width' => 1920,
                    'height' => 1080,
                    'bitrate' => '5000k',
                ],
                '1440p' => [
                    'width' => 2560,
                    'height' => 1440,
                    'bitrate' => '9000k',
                ],
                '2160p' => [
                    'width' => 3840,
                    'height' => 2160,
                    'bitrate' => '12500k',
                ],
            ],
        ],
    ];
}

class Fraction {
    public $numerator = 0;
    public $denominator = 0;

    public function __construct( $num, $denom ) {
        $this->numerator = $num;
        $this->denominator = $denom;
    }

    public function toFloat() {
        return $this->numerator / $this->denominator;
    }

    public function toString() {
        return "$this->numerator/$this->denominator";
    }

    public static function fromString( $frac ) {
        list ( $num, $denom ) = array_map( 'intval', explode( '/', $frac, 2 ) );
        return new Fraction( $num, $denom );
    }
}

class SourceFile {
    public $filename = '';
    public $duration = 0.0;

    public $video = false;
    public $width = 0;
    public $height = 0;
    public $fps = null;

    public $audio = false;
    public $sampleRate = 0;
    public $channels = 0;

    public function __construct( $filename ) {
        $this->filename = $filename;

        $data = ffprobe( $filename );

        $this->duration = $data->format->duration;
        foreach ( $data->streams as $stream ) {
            if ( $stream->codec_type == 'video' && !$this->video ) {
                $this->video = true;
                $this->width = $stream->width;
                $this->height = $stream->height;
                $this->fps = Fraction::fromString( $stream->r_frame_rate );
            }
            if ( $stream->codec_type === 'audio' && !$this->audio ) {
                $this->audio = true;
                $this->sampleRate = $stream->sample_rate;
                $this->channels = $stream->channels;
            }
        }
    }
}

class Transcoder {
    private $source = null;
    private $fps = 0;
    private $gop = 0;

    public const SEGMENT_DURATION = 10;

    public function __construct( SourceFile $source ) {
        $this->source = $source;

        // Normalize input fps to an even standard
        $infps = $this->source->fps->toFloat();
        $this->fps = Video::RATES[0];
        foreach ( Video::RATES as $rate ) {
            if ( $rate >= $infps ) {
                $this->fps = $rate;
                break;
            }
        }

        // Each self-contained group of pictures starts with a keyframe.
        $this->gop = $this->fps * self::SEGMENT_DURATION;
    }

    private function ffmpeg( $options, $outfile, $container ) {
        if ( $mode === 'pass1' ) {
            $filename = '/dev/null';
            $playlist = '/dev/null';
        } else {
            $filename = "$outfile.%04d.$container";
            $playlist = "$outfile.$container.m3u8";
        }
        $ffmpegOptions = array_merge( [
            '-hide_banner',
            '-i',
            $this->source->filename,
            '-f', 'hls',
            '-hls_segment_type', 'fmp4',
            '-hls_time', '10',
            '-hls_playlist_type', 'vod',
            '-hls_segment_filename', $filename,
        ], $options, [
            '-y', $playlist
        ] );

        $output = run( 'ffmpeg', $ffmpegOptions );
    }

    public function video( $codec, $resolution, $mode ) {
        if ( !$this->source->video ) {
            throw new Error('no video');
        }

        $res = Video::FORMATS[$codec]['resolutions'][$resolution];

        $options = array_merge(
            [
                '-pix_fmt', 'yuv420p',
                '-r', $this->fps,
            ],
            Video::FORMATS[$codec]['options']['common'],
            Video::FORMATS[$codec]['options'][$mode],
            [
                '-vf', "scale=" . implode( ':', [ $res['width'], $res['height'] ] ),
                '-b:v', $res['bitrate'],
                '-g', $this->gop,
                '-keyint_min', $this->gop, // may not be generic enough
                '-an',
            ]
        );

        $outfile = "{$this->source->filename}.{$resolution}.{$codec}.{$mode}";
        $this->ffmpeg( $options, $outfile, "mp4" );
    }

    public function audio( $codec ) {
        if ( !$this->source->audio ) {
            throw new Error('no audio');
        }

        $format = Audio::FORMATS[$codec];
        $options = array_merge(
            $format['options'],
            [
                '-vn',
            ]
        );

        $outfile = "{$this->source->filename}.audio.{$codec}";
        $this->ffmpeg( $options, $outfile, "mp4" );
    }

}


$infiles = [
    'caminandes-llamigos.webm',
];

foreach ( $infiles as $filename ) {
    $source = new SourceFile( $filename );
    $codec = new Transcoder( $source );
    //$codec->audio('opus');
    $codec->audio('mp3');
    //$codec->audio('aac');
    //$codec->audio('alac');
    //$codec->audio('vorbis');
    /*
    foreach ( Video::FORMATS['vp9']['resolutions'] as $res => $format ) {

        if ( $format['width'] <= $source->width && $format['height'] <= $source->height ) {
            $codec->video('vp9', $res, 'fast');
            $codec->video('vp9', $res, 'pass1');
            $codec->video('vp9', $res, 'pass2');
        }
    }
    */
}