Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <?php
- /** @noinspection PhpDocMissingThrowsInspection
- * @param string $date
- *
- * @param int $fps1
- * @param int $fps2
- *
- * @return double
- * @throws Exception
- */
- function parse_raw_date_string($date, int $fps1, int $fps2): float {
- if (preg_match('_^[0-9]+(\\.[0-9]+)?$_', $date, $a)) {
- $value = (double) $a[0];
- } elseif (!preg_match('_^(.+?)(\\.[0-9]+)?$_', $date, $a)) {
- $value = 0;
- } else {
- $micro = array_key_exists(2, $a) ? (double) $a[2] : 0;
- $a[1] = preg_replace('_^([0-9]+)\\:([0-9]{1,2})$_', '0:\\1:\\2', $a[1]);
- $d = new DateTime('1.1.1970 '.$a[1].'+0:00');
- $value = $d->getTimestamp() + $micro;
- }
- $r = $value * $fps1 / $fps2;
- $frame1 = intval(floor($r));
- $frame2 = intval(ceil($r));
- $a = [
- [$frame1 * $fps2 / $fps1,],
- [$frame2 * $fps2 / $fps1,],
- ];
- $a[0][1] = abs($a[0][0] - $value);
- $a[1][1] = abs($a[1][0] - $value);
- return ($a[0][1] < $a[1][1]) ? $a[0][0] : $a[1][0];
- }
- /**
- * @param int $macroblocks_count
- * @param int $macroblocks_per_second
- * @param int $bitrate_bps
- *
- * @return string[]
- *
- * @url https://ru.wikipedia.org/wiki/H.264#%D0%A3%D1%80%D0%BE%D0%B2%D0%BD%D0%B8
- * @url https://trac.ffmpeg.org/wiki/Encode/H.264
- */
- function get_level(int $macroblocks_count, int $macroblocks_per_second, int $bitrate_bps): array {
- $bitrate_kbps = $bitrate_bps / 1024;
- $levels = [
- (object) ['level' => '3', 'macroblocks' => 1620, 'mps' => 40500, 'base' => 10000, 'high' => 12500,],
- (object) ['level' => '3.1', 'macroblocks' => 3600, 'mps' => 108000, 'base' => 14000, 'high' => 17500,],
- (object) ['level' => '3.2', 'macroblocks' => 5120, 'mps' => 216000, 'base' => 20000, 'high' => 25000,],
- (object) ['level' => '4', 'macroblocks' => 8192, 'mps' => 245760, 'base' => 20000, 'high' => 25000,],
- (object) ['level' => '4.1', 'macroblocks' => 8192, 'mps' => 245760, 'base' => 50000, 'high' => 62500,],
- (object) ['level' => '4.2', 'macroblocks' => 8704, 'mps' => 522240, 'base' => 50000, 'high' => 62500,],
- (object) ['level' => '5', 'macroblocks' => 22080, 'mps' => 589824, 'base' => 135000, 'high' => 168750,],
- (object) ['level' => '5.1', 'macroblocks' => 36864, 'mps' => 983040, 'base' => 240000, 'high' => 300000,],
- (object) ['level' => '5.2', 'macroblocks' => 36864, 'mps' => 2073600, 'base' => 240000, 'high' => 300000,],
- (object) ['level' => '6', 'macroblocks' => 139264, 'mps' => 4177920, 'base' => 240000, 'high' => 300000,],
- (object) ['level' => '6.1', 'macroblocks' => 139264, 'mps' => 8355840, 'base' => 480000, 'high' => 300000,],
- (object) ['level' => '6.2', 'macroblocks' => 139264, 'mps' => 16711680, 'base' => 800000, 'high' => 300000,],
- ];
- foreach ($levels as $level) {
- if (($macroblocks_count > $level->macroblocks) or ($macroblocks_per_second > $level->mps) or
- ($bitrate_kbps > $level->high)) {
- continue;
- }
- return [
- $level->level,
- ($bitrate_kbps <= $level->base) ? 'main' : 'high',
- ];
- }
- return ['5', 'high'];
- }
- $options = getopt('i:o:t:s:y', [
- 'an',// Нет звука
- 'no-sound',// Нет звука
- 'ass:',//
- 'sub:',//
- 'sound:',//
- 'qa:',// q:a (audio quality 1-5)
- 'filesize:',//
- 'threads:',//
- 'thread:',//
- 'temporary_folder:',//
- 'to:',//
- 'from:',
- 'input:',
- 'output:',
- 'yes',// Перезаписывать
- 'tries-count:',//
- 'tune:',// Тюнинг libx264
- 'good-audio',// AAC q:a=4
- 'scale:',// https://trac.ffmpeg.org/wiki/Scaling
- 'vf:',
- 'quite',// Не выводить ничего
- 'quite-ffmpeg',// Не выводить ffmpeg
- 'no-metadata-import',// No meta data import
- 'fast',// Сделать быстрее, пожертвовав качеством
- 'duration:',
- // Meta. https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
- 'meta-title:',
- 'meta-author:',
- 'meta-album_artist:',
- 'meta-album:',
- 'meta-composer:',
- 'meta-year:',
- 'meta-date:',
- 'meta-track:',
- 'meta-comment:',
- 'meta-genre:',
- 'meta-description:',
- 'meta-copyright:',
- 'meta-episode_id:',
- 'meta-show:',
- ]);
- if (isset($options['cv'])) {
- echo "Option 'cv' is obsolete. Only x264 is available\n";
- return 10;
- }
- foreach ([
- 'from' => 's',
- 'input' => 'i',
- 'output' => 'o',
- 'yes' => 'y',
- 'duration' => 't',
- // doubles
- 'thread' => 'threads',
- 'sub' => 'ass',
- 'meta-year' => 'meta-date',
- 'no-sound' => 'an',
- ] as $long => $short) {
- if (isset($options[$long]) and !isset($options[$short])) {
- $options[$short] = $options[$long];
- unset($options[$long]);
- }
- }
- if (isset($options['quite'])) {
- $options['quite-ffmpeg'] = '';
- }
- $null_device = defined('PHP_WINDOWS_VERSION_MAJOR') ? 'NUL' : '/dev/null';
- if (!isset($options['i'])) {
- echo "Option {$options['i']} does not exist\n";
- return 1;
- }
- if (!file_exists($options['i'])) {
- echo "File {$options['i']} does not exist\n";
- return 2;
- }
- if (isset($options['temporary_folder'])) {
- $temporary_folder = $options['temporary_folder'];
- } else {
- $temporary_folder = sys_get_temp_dir();
- }
- $temporary_folder = str_replace('\\', '/', $temporary_folder);
- $vtw_passlog = $temporary_folder.'/vtmp4-passlog'.mt_rand(0, 100000);
- $options['tries-count'] = isset($options['tries-count']) ? (int) $options['tries-count'] : null;
- $aac_quality = isset($options['qa'])
- ? intval($options['qa'])
- : (isset($options['good-audio']) ? 4 : 2);
- if (isset($options['ass']) and !file_exists($options['ass'])) {
- echo "Subtitles file {$options['ass']} does not exist\n";
- return 11;
- }
- /**
- * Устанавливаем $output_full_filesize
- */
- if (!isset($options['filesize'])) {
- $output_estimated_filesize = 20 * 1024 * 1024;
- } elseif (preg_match('_([0-9.]+)([kmg])_i', $options['filesize'], $a)) {
- $b = ['k' => 1024, 'm' => 1024 * 1024, 'g' => 1024 * 1024 * 1024];
- $output_estimated_filesize = (int) ($a[1] * $b[strtolower($a[2])]);
- unset($a, $b);
- } else {
- $output_estimated_filesize = (int) $options['filesize'];
- }
- if ($output_estimated_filesize == 0) {
- echo "Output filesize is null\n";
- return 3;
- }
- $temporary = tempnam($temporary_folder, 'vtmp4-');
- system(sprintf(
- 'ffprobe -i %s -v quiet -print_format json -show_format -show_streams > %s',
- escapeshellarg($options['i']),
- escapeshellarg($temporary)
- ));
- $buf = file_get_contents($temporary);
- unlink($temporary);
- $probe = json_decode($buf);
- $stream_video = null;
- foreach ($probe->streams as $stream) {
- if ($stream->codec_type == 'video') {
- $stream_video = $stream;
- break;
- }
- }
- $a = explode('/', $stream_video->r_frame_rate);
- $fps1 = intval($a[0]);
- $fps2 = intval($a[1]);
- unset($buf, $stream, $a);
- if (isset($options['sound'])) {
- $sound_file = $options['sound'];
- if (!file_exists($sound_file)) {
- echo "File {$sound_file} does not exist\n";
- return 4;
- }
- } else {
- $sound_file = $options['i'];
- }
- if (isset($options['o'])) {
- $output_file = $options['o'];
- } else {
- $output_file = $options['i'].'.mp4';
- }
- if (file_exists($output_file)) {
- if (!array_key_exists('y', $options)) {
- echo "Output file {$output_file} exists\n";
- return 5;
- }
- }
- /**
- * Устанавливаем from и duration
- */
- if (!isset($options['s'])) {
- $options['s'] = 0;
- } else {
- $options['s'] = parse_raw_date_string($options['s'], $fps1, $fps2);
- }
- if (isset($options['t'])) {
- $duration = parse_raw_date_string($options['t'], $fps1, $fps2);
- $duration_set_type = 0;
- } elseif (isset($options['to'])) {
- $duration = parse_raw_date_string($options['to'], $fps1, $fps2) - $options['s'];
- $duration_set_type = 1;
- } else {
- // Вычисляем сами
- $duration = (float) $probe->format->duration - $options['s'];
- $duration_set_type = 1;
- }
- if ($duration == 0) {
- echo "Duration is null\n";
- return 9;
- }
- /**
- * Устанавливаем другое
- */
- if (!isset($options['threads'])) {
- $options['threads'] = null;
- }
- $temporary_sound = tempnam($temporary_folder, 'vtmp4-');
- unlink($temporary_sound);
- $temporary_sound .= '.aac';
- $temporary_video = tempnam($temporary_folder, 'vtmp4-');
- unlink($temporary_video);
- $temporary_video .= '.mp4';
- $ffmpeg_start_time = number_format(max($options['s'] - $fps2 / $fps1 / 6, 0), 3, '.', '');
- $ffmpeg_duration = number_format($duration + $fps2 / $fps1 / 3, 3, '.', '');
- $output_width = $stream_video->width;
- $output_height = $stream_video->height;
- if (isset($options['scale'])) {
- // @todo
- }
- $macroblocks_count = (int) ceil($stream_video->width * $stream_video->height / 256);
- $macroblocks_per_second = (int) ceil($macroblocks_count * $fps1 / $fps2);
- echo sprintf(
- "temporary sound:\t%s\ntemporary video:\t%s\nduration:\t\t%s sec (%s frames)\nffmpeg settings\t\t%s + %s\n".
- "Original fps: \t%s / %s (%s fps; %s ms per frame)\n".
- "Resolution: \t%sx%s@%s\n".
- "Macroblocks: \t%s (per frame) & %s (per sec)\n",
- $temporary_sound,
- $temporary_video,
- number_format($duration + (($duration_set_type > 0) ? $fps2 / $fps1 : 0), 4, '.', ''),
- number_format($duration * $fps1 / $fps2 + (($duration_set_type > 0) ? 1 : 0), 2, '.', ''),
- $ffmpeg_start_time,
- $ffmpeg_duration,
- $fps1,
- $fps2,
- number_format($fps1 / $fps2, 3, '.', ''),
- number_format($fps2 / $fps1 * 1000, 1, '.', ''),
- $output_width,
- $output_height,
- number_format($fps1 / $fps2, 3, '.', ''),
- $macroblocks_count,
- $macroblocks_per_second
- );
- $file_estimated_overhead = $duration * 300; // 300 байт на секунду это средний overhead для mp4
- $video_bitrate = null;
- for ($try_number = 0; ($try_number < $options['tries-count']) or is_null($options['tries-count']); $try_number++) {
- echo sprintf("\niteration #%s\n", $try_number + 1);
- if (file_exists($temporary_sound)) {
- @unlink($temporary_sound);
- }
- if (file_exists($temporary_video)) {
- @unlink($temporary_video);
- }
- if (!array_key_exists('an', $options)) {
- unset($buf);
- $audio_ffmpeg_string = sprintf(
- 'ffmpeg -i %s -sn -vn -c:a aac -q:a %d -ss %s -t %s %s -y %s 2>&1',
- escapeshellarg($sound_file),
- $aac_quality,
- $ffmpeg_start_time,
- $ffmpeg_duration,
- (($options['threads'] != 0) ? ' -threads '.$options['threads'] : ''),
- escapeshellarg($temporary_sound)
- );
- exec($audio_ffmpeg_string, $buf);
- /** @var string[] $buf */
- if (!file_exists($temporary_sound)) {
- echo "Can not build sound\n";
- return 6;
- }
- $raw_audio_stream_size = null;
- for ($i = count($buf) - 1; ($i >= count($buf) - 5) and ($i >= 0); $i--) {
- if (preg_match('_ audio:\\s*([0-9.]+)([kb])?B_ui', $buf[$i], $a)) {
- switch ($a[2]) {
- case 'k':
- $multi = 1024;
- break;
- case 'm':
- $multi = 1024 * 1024;
- break;
- case '':
- $multi = 1;
- break;
- }
- $raw_audio_stream_size = floatval($a[1]) * $multi;
- break;
- }
- }
- } else {
- $raw_audio_stream_size = 0;
- }
- if ($try_number == 0) {
- $video_bitrate = ($output_estimated_filesize - $file_estimated_overhead - $raw_audio_stream_size) * 8 / $duration;
- }
- if ($video_bitrate <= 0) {
- echo "too large file anyway\n";
- return 8;
- }
- echo "Audio stream size:\t{$raw_audio_stream_size}\nVideo stream bitrate:\t{$video_bitrate}\n";
- $video_filters = [
- 'format=yuv420p',
- ];
- if (isset($options['ass'])) {
- $video_filters[] = 'ass='.escapeshellarg($options['ass']);
- }
- if (isset($options['scale'])) {
- $video_filters[] = 'scale='.escapeshellarg($options['scale']);
- }
- if (isset($options['vf'])) {
- // @todo explode(',',..)
- // @todo explode('=',..)
- $video_filters = array_merge($video_filters, explode(',', $options['vf']));
- }
- list($mp4_level, $mp4_profile) = get_level($macroblocks_count, $macroblocks_per_second, intval($video_bitrate));
- echo sprintf("H264-Level: \t%s [%s]\n", $mp4_level, $mp4_profile);
- /**
- * https://trac.ffmpeg.org/wiki/Encode/H.264
- * https://ru.wikipedia.org/wiki/H.264#%D0%A3%D1%80%D0%BE%D0%B2%D0%BD%D0%B8 Levels
- *
- * https://codec.fandom.com/ru/wiki/X264_-_%D0%BE%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%B9_%D0%BA%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F
- *
- * preset:v slow — качество
- * format=yuv420p — чтобы не было 10bit (будет, если оригинал был 10bit)
- */
- $video_ffmpeg_string = sprintf(
- 'ffmpeg -i %s -sn -an -ss %s -t %s -c:v libx264 -b:v %d -preset:v %s -profile:v %s -level %s %s %s -vf %s -f mp4 -passlogfile %s -map_chapters -1 %s',
- escapeshellarg($options['i']),
- $ffmpeg_start_time,
- $ffmpeg_duration,
- intval($video_bitrate),
- isset($options['fast']) ? 'fast' : 'slow',
- escapeshellarg($mp4_profile),
- escapeshellarg($mp4_level),
- (($options['threads'] != 0) ? ' -threads '.$options['threads'] : ''),
- isset($options['tune']) ? ' -tune '.$options['tune'] : '',
- implode(',', $video_filters),
- escapeshellarg($vtw_passlog),
- isset($options['no-metadata-import']) ? '-map_metadata -1' : ''
- );
- // Meta
- foreach ($options as $key => $value) {
- if (preg_match('|^meta\\-(.+)$|ui', $key, $a)) {
- $video_ffmpeg_string .= ' -metadata '.$a[1].'='.escapeshellarg($value);
- }
- }
- unset($key, $value, $a);
- // Запускаем двух-проходку
- echo $video_ffmpeg_string."\n";
- system(sprintf('%s -pass 1 %s %s %s',
- $video_ffmpeg_string,
- !isset($options['fast']) ? '-fastfirstpass 0' : '',
- escapeshellarg($temporary_video),
- isset($options['quite-ffmpeg']) ? ' >'.$null_device.' 2>'.$null_device : ''
- ));
- system(sprintf('%s -pass 2 -y %s %s',
- $video_ffmpeg_string,
- escapeshellarg($temporary_video),
- isset($options['quite-ffmpeg']) ? ' >'.$null_device.' 2>'.$null_device : ''
- ));
- if (!file_exists($temporary_video) or (filesize($temporary_video) == 0)) {
- echo "Can not render video\n";
- if (!array_key_exists('an', $options)) {
- unlink($temporary_sound);
- }
- if (file_exists($temporary_video)) {
- unlink($temporary_video);
- }
- return 7;
- }
- $temporary_video_filesize = filesize($temporary_video);
- if (!array_key_exists('an', $options)) {
- $both_ffmpeg_string = sprintf(
- 'ffmpeg -i %s -i %s -c copy -v quiet -y',
- escapeshellarg($temporary_video),
- escapeshellarg($temporary_sound)
- );
- foreach ($options as $key => $value) {
- if (preg_match('|^meta\\-(.+)$|ui', $key, $a)) {
- $both_ffmpeg_string .= ' -metadata '.$a[1].'='.escapeshellarg($value);
- }
- }
- unset($key, $value, $a);
- $both_ffmpeg_string .= ' '.escapeshellarg($output_file);
- if (isset($options['quite-ffmpeg'])) {
- $both_ffmpeg_string .= ' >'.$null_device.' 2>'.$null_device;
- }
- system($both_ffmpeg_string);
- unlink($temporary_sound);
- unlink($temporary_video);
- } else {
- rename($temporary_video, $output_file);
- }
- $size_left = $output_estimated_filesize - filesize($output_file);
- $too_small_file = false;
- if (($try_number == 0) and ($size_left >= 30 * 1024)) {// @todo suppress this behaviour
- // Слишком много еста осталось
- $too_small_file = true;
- } elseif ($size_left >= 0) {
- echo "Size left:\t".number_format($size_left / 1024, 2, '.', '')." KB\n";
- break;
- }
- // Вычисляем правильный overhead
- $temporary_video_raw = tempnam($temporary_folder, 'vtmp4-').'.mp4';
- $both_ffmpeg_string = sprintf(
- 'ffmpeg -i %s -c:v copy -an -sn -v quiet -f rawvideo %s',
- escapeshellarg($output_file),
- escapeshellarg($temporary_video_raw)
- );
- if (isset($options['quite-ffmpeg'])) {
- $both_ffmpeg_string .= ' >'.$null_device.' 2>'.$null_device;
- }
- system($both_ffmpeg_string);
- $raw_video_stream_size = filesize($temporary_video_raw);
- // $file_estimated_overhead = $temporary_video_filesize - $raw_video_stream_size - $raw_audio_stream_size;
- unlink($temporary_video_raw);
- unset($temporary_video_raw);
- // Вычисляем новый битрейт
- $video_bitrate = $video_bitrate * ($raw_video_stream_size + $size_left) / $raw_video_stream_size - 1024;
- echo sprintf(
- "too %s file %s KB; next bitrate %s kbps\n",
- $too_small_file ? 'small' : 'large',
- number_format(filesize($output_file) / 1024, 1, '.', ''),
- number_format($video_bitrate / 1024, 3, '.', '')
- );
- }
- ?>
Advertisement
Add Comment
Please, Sign In to add comment