Guest User

Mp4-Нарезчик

a guest
Sep 13th, 2020
96
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 19.44 KB | None | 0 0
  1. <?php
  2.     /** @noinspection PhpDocMissingThrowsInspection
  3.      * @param string $date
  4.      *
  5.      * @param int    $fps1
  6.      * @param int    $fps2
  7.      *
  8.      * @return double
  9.      * @throws Exception
  10.      */
  11.     function parse_raw_date_string($date, int $fps1, int $fps2): float {
  12.         if (preg_match('_^[0-9]+(\\.[0-9]+)?$_', $date, $a)) {
  13.             $value = (double) $a[0];
  14.         } elseif (!preg_match('_^(.+?)(\\.[0-9]+)?$_', $date, $a)) {
  15.             $value = 0;
  16.         } else {
  17.             $micro = array_key_exists(2, $a) ? (double) $a[2] : 0;
  18.             $a[1] = preg_replace('_^([0-9]+)\\:([0-9]{1,2})$_', '0:\\1:\\2', $a[1]);
  19.             $d = new DateTime('1.1.1970 '.$a[1].'+0:00');
  20.  
  21.             $value = $d->getTimestamp() + $micro;
  22.         }
  23.  
  24.         $r = $value * $fps1 / $fps2;
  25.         $frame1 = intval(floor($r));
  26.         $frame2 = intval(ceil($r));
  27.  
  28.         $a = [
  29.             [$frame1 * $fps2 / $fps1,],
  30.             [$frame2 * $fps2 / $fps1,],
  31.         ];
  32.         $a[0][1] = abs($a[0][0] - $value);
  33.         $a[1][1] = abs($a[1][0] - $value);
  34.  
  35.         return ($a[0][1] < $a[1][1]) ? $a[0][0] : $a[1][0];
  36.     }
  37.  
  38.     /**
  39.      * @param int $macroblocks_count
  40.      * @param int $macroblocks_per_second
  41.      * @param int $bitrate_bps
  42.      *
  43.      * @return string[]
  44.      *
  45.      * @url https://ru.wikipedia.org/wiki/H.264#%D0%A3%D1%80%D0%BE%D0%B2%D0%BD%D0%B8
  46.      * @url https://trac.ffmpeg.org/wiki/Encode/H.264
  47.      */
  48.     function get_level(int $macroblocks_count, int $macroblocks_per_second, int $bitrate_bps): array {
  49.         $bitrate_kbps = $bitrate_bps / 1024;
  50.         $levels = [
  51.             (object) ['level' => '3', 'macroblocks' => 1620, 'mps' => 40500, 'base' => 10000, 'high' => 12500,],
  52.             (object) ['level' => '3.1', 'macroblocks' => 3600, 'mps' => 108000, 'base' => 14000, 'high' => 17500,],
  53.             (object) ['level' => '3.2', 'macroblocks' => 5120, 'mps' => 216000, 'base' => 20000, 'high' => 25000,],
  54.             (object) ['level' => '4', 'macroblocks' => 8192, 'mps' => 245760, 'base' => 20000, 'high' => 25000,],
  55.             (object) ['level' => '4.1', 'macroblocks' => 8192, 'mps' => 245760, 'base' => 50000, 'high' => 62500,],
  56.             (object) ['level' => '4.2', 'macroblocks' => 8704, 'mps' => 522240, 'base' => 50000, 'high' => 62500,],
  57.             (object) ['level' => '5', 'macroblocks' => 22080, 'mps' => 589824, 'base' => 135000, 'high' => 168750,],
  58.             (object) ['level' => '5.1', 'macroblocks' => 36864, 'mps' => 983040, 'base' => 240000, 'high' => 300000,],
  59.             (object) ['level' => '5.2', 'macroblocks' => 36864, 'mps' => 2073600, 'base' => 240000, 'high' => 300000,],
  60.             (object) ['level' => '6', 'macroblocks' => 139264, 'mps' => 4177920, 'base' => 240000, 'high' => 300000,],
  61.             (object) ['level' => '6.1', 'macroblocks' => 139264, 'mps' => 8355840, 'base' => 480000, 'high' => 300000,],
  62.             (object) ['level' => '6.2', 'macroblocks' => 139264, 'mps' => 16711680, 'base' => 800000, 'high' => 300000,],
  63.         ];
  64.  
  65.         foreach ($levels as $level) {
  66.             if (($macroblocks_count > $level->macroblocks) or ($macroblocks_per_second > $level->mps) or
  67.                 ($bitrate_kbps > $level->high)) {
  68.                 continue;
  69.             }
  70.  
  71.             return [
  72.                 $level->level,
  73.                 ($bitrate_kbps <= $level->base) ? 'main' : 'high',
  74.             ];
  75.         }
  76.  
  77.         return ['5', 'high'];
  78.     }
  79.  
  80.     $options = getopt('i:o:t:s:y', [
  81.         'an',// Нет звука
  82.         'no-sound',// Нет звука
  83.         'ass:',//
  84.         'sub:',//
  85.         'sound:',//
  86.         'qa:',// q:a (audio quality 1-5)
  87.         'filesize:',//
  88.         'threads:',//
  89.         'thread:',//
  90.         'temporary_folder:',//
  91.         'to:',//
  92.         'from:',
  93.         'input:',
  94.         'output:',
  95.         'yes',// Перезаписывать
  96.         'tries-count:',//
  97.         'tune:',// Тюнинг libx264
  98.         'good-audio',// AAC q:a=4
  99.         'scale:',// https://trac.ffmpeg.org/wiki/Scaling
  100.         'vf:',
  101.         'quite',// Не выводить ничего
  102.         'quite-ffmpeg',// Не выводить ffmpeg
  103.         'no-metadata-import',// No meta data import
  104.         'fast',// Сделать быстрее, пожертвовав качеством
  105.         'duration:',
  106.  
  107.         // Meta. https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
  108.         'meta-title:',
  109.         'meta-author:',
  110.         'meta-album_artist:',
  111.         'meta-album:',
  112.         'meta-composer:',
  113.         'meta-year:',
  114.         'meta-date:',
  115.         'meta-track:',
  116.         'meta-comment:',
  117.         'meta-genre:',
  118.         'meta-description:',
  119.         'meta-copyright:',
  120.         'meta-episode_id:',
  121.         'meta-show:',
  122.     ]);
  123.     if (isset($options['cv'])) {
  124.         echo "Option 'cv' is obsolete. Only x264 is available\n";
  125.  
  126.         return 10;
  127.     }
  128.     foreach ([
  129.                  'from' => 's',
  130.                  'input' => 'i',
  131.                  'output' => 'o',
  132.                  'yes' => 'y',
  133.                  'duration' => 't',
  134.  
  135.                  // doubles
  136.                  'thread' => 'threads',
  137.                  'sub' => 'ass',
  138.                  'meta-year' => 'meta-date',
  139.                  'no-sound' => 'an',
  140.              ] as $long => $short) {
  141.         if (isset($options[$long]) and !isset($options[$short])) {
  142.             $options[$short] = $options[$long];
  143.             unset($options[$long]);
  144.         }
  145.     }
  146.     if (isset($options['quite'])) {
  147.         $options['quite-ffmpeg'] = '';
  148.     }
  149.     $null_device = defined('PHP_WINDOWS_VERSION_MAJOR') ? 'NUL' : '/dev/null';
  150.  
  151.     if (!isset($options['i'])) {
  152.         echo "Option {$options['i']} does not exist\n";
  153.  
  154.         return 1;
  155.     }
  156.     if (!file_exists($options['i'])) {
  157.         echo "File {$options['i']} does not exist\n";
  158.  
  159.         return 2;
  160.     }
  161.     if (isset($options['temporary_folder'])) {
  162.         $temporary_folder = $options['temporary_folder'];
  163.     } else {
  164.         $temporary_folder = sys_get_temp_dir();
  165.     }
  166.     $temporary_folder = str_replace('\\', '/', $temporary_folder);
  167.     $vtw_passlog = $temporary_folder.'/vtmp4-passlog'.mt_rand(0, 100000);
  168.     $options['tries-count'] = isset($options['tries-count']) ? (int) $options['tries-count'] : null;
  169.     $aac_quality = isset($options['qa'])
  170.         ? intval($options['qa'])
  171.         : (isset($options['good-audio']) ? 4 : 2);
  172.  
  173.     if (isset($options['ass']) and !file_exists($options['ass'])) {
  174.         echo "Subtitles file {$options['ass']} does not exist\n";
  175.  
  176.         return 11;
  177.     }
  178.  
  179.     /**
  180.      * Устанавливаем $output_full_filesize
  181.      */
  182.     if (!isset($options['filesize'])) {
  183.         $output_estimated_filesize = 20 * 1024 * 1024;
  184.     } elseif (preg_match('_([0-9.]+)([kmg])_i', $options['filesize'], $a)) {
  185.         $b = ['k' => 1024, 'm' => 1024 * 1024, 'g' => 1024 * 1024 * 1024];
  186.         $output_estimated_filesize = (int) ($a[1] * $b[strtolower($a[2])]);
  187.         unset($a, $b);
  188.     } else {
  189.         $output_estimated_filesize = (int) $options['filesize'];
  190.     }
  191.     if ($output_estimated_filesize == 0) {
  192.         echo "Output filesize is null\n";
  193.  
  194.         return 3;
  195.     }
  196.     $temporary = tempnam($temporary_folder, 'vtmp4-');
  197.     system(sprintf(
  198.         'ffprobe -i %s -v quiet -print_format json -show_format -show_streams > %s',
  199.         escapeshellarg($options['i']),
  200.         escapeshellarg($temporary)
  201.     ));
  202.     $buf = file_get_contents($temporary);
  203.     unlink($temporary);
  204.     $probe = json_decode($buf);
  205.     $stream_video = null;
  206.     foreach ($probe->streams as $stream) {
  207.         if ($stream->codec_type == 'video') {
  208.             $stream_video = $stream;
  209.             break;
  210.         }
  211.     }
  212.     $a = explode('/', $stream_video->r_frame_rate);
  213.     $fps1 = intval($a[0]);
  214.     $fps2 = intval($a[1]);
  215.     unset($buf, $stream, $a);
  216.  
  217.     if (isset($options['sound'])) {
  218.         $sound_file = $options['sound'];
  219.         if (!file_exists($sound_file)) {
  220.             echo "File {$sound_file} does not exist\n";
  221.  
  222.             return 4;
  223.         }
  224.     } else {
  225.         $sound_file = $options['i'];
  226.     }
  227.  
  228.     if (isset($options['o'])) {
  229.         $output_file = $options['o'];
  230.     } else {
  231.         $output_file = $options['i'].'.mp4';
  232.     }
  233.     if (file_exists($output_file)) {
  234.         if (!array_key_exists('y', $options)) {
  235.             echo "Output file {$output_file} exists\n";
  236.  
  237.             return 5;
  238.         }
  239.     }
  240.     /**
  241.      * Устанавливаем from и duration
  242.      */
  243.     if (!isset($options['s'])) {
  244.         $options['s'] = 0;
  245.     } else {
  246.         $options['s'] = parse_raw_date_string($options['s'], $fps1, $fps2);
  247.     }
  248.     if (isset($options['t'])) {
  249.         $duration = parse_raw_date_string($options['t'], $fps1, $fps2);
  250.         $duration_set_type = 0;
  251.     } elseif (isset($options['to'])) {
  252.         $duration = parse_raw_date_string($options['to'], $fps1, $fps2) - $options['s'];
  253.         $duration_set_type = 1;
  254.     } else {
  255.         // Вычисляем сами
  256.         $duration = (float) $probe->format->duration - $options['s'];
  257.         $duration_set_type = 1;
  258.     }
  259.     if ($duration == 0) {
  260.         echo "Duration is null\n";
  261.  
  262.         return 9;
  263.     }
  264.  
  265.     /**
  266.      * Устанавливаем другое
  267.      */
  268.     if (!isset($options['threads'])) {
  269.         $options['threads'] = null;
  270.     }
  271.  
  272.     $temporary_sound = tempnam($temporary_folder, 'vtmp4-');
  273.     unlink($temporary_sound);
  274.     $temporary_sound .= '.aac';
  275.     $temporary_video = tempnam($temporary_folder, 'vtmp4-');
  276.     unlink($temporary_video);
  277.     $temporary_video .= '.mp4';
  278.  
  279.     $ffmpeg_start_time = number_format(max($options['s'] - $fps2 / $fps1 / 6, 0), 3, '.', '');
  280.     $ffmpeg_duration = number_format($duration + $fps2 / $fps1 / 3, 3, '.', '');
  281.  
  282.     $output_width = $stream_video->width;
  283.     $output_height = $stream_video->height;
  284.     if (isset($options['scale'])) {
  285.         // @todo
  286.     }
  287.     $macroblocks_count = (int) ceil($stream_video->width * $stream_video->height / 256);
  288.     $macroblocks_per_second = (int) ceil($macroblocks_count * $fps1 / $fps2);
  289.  
  290.     echo sprintf(
  291.         "temporary sound:\t%s\ntemporary video:\t%s\nduration:\t\t%s sec (%s frames)\nffmpeg settings\t\t%s + %s\n".
  292.         "Original fps:   \t%s / %s (%s fps; %s ms per frame)\n".
  293.         "Resolution:     \t%sx%s@%s\n".
  294.         "Macroblocks:    \t%s (per frame) & %s (per sec)\n",
  295.         $temporary_sound,
  296.         $temporary_video,
  297.         number_format($duration + (($duration_set_type > 0) ? $fps2 / $fps1 : 0), 4, '.', ''),
  298.         number_format($duration * $fps1 / $fps2 + (($duration_set_type > 0) ? 1 : 0), 2, '.', ''),
  299.         $ffmpeg_start_time,
  300.         $ffmpeg_duration,
  301.         $fps1,
  302.         $fps2,
  303.         number_format($fps1 / $fps2, 3, '.', ''),
  304.         number_format($fps2 / $fps1 * 1000, 1, '.', ''),
  305.         $output_width,
  306.         $output_height,
  307.         number_format($fps1 / $fps2, 3, '.', ''),
  308.         $macroblocks_count,
  309.         $macroblocks_per_second
  310.     );
  311.  
  312.     $file_estimated_overhead = $duration * 300; // 300 байт на секунду это средний overhead для mp4
  313.     $video_bitrate = null;
  314.     for ($try_number = 0; ($try_number < $options['tries-count']) or is_null($options['tries-count']); $try_number++) {
  315.         echo sprintf("\niteration #%s\n", $try_number + 1);
  316.         if (file_exists($temporary_sound)) {
  317.             @unlink($temporary_sound);
  318.         }
  319.         if (file_exists($temporary_video)) {
  320.             @unlink($temporary_video);
  321.         }
  322.         if (!array_key_exists('an', $options)) {
  323.             unset($buf);
  324.             $audio_ffmpeg_string = sprintf(
  325.                 'ffmpeg -i %s -sn -vn -c:a aac -q:a %d -ss %s -t %s %s -y %s 2>&1',
  326.  
  327.                 escapeshellarg($sound_file),
  328.                 $aac_quality,
  329.                 $ffmpeg_start_time,
  330.                 $ffmpeg_duration,
  331.                 (($options['threads'] != 0) ? ' -threads '.$options['threads'] : ''),
  332.                 escapeshellarg($temporary_sound)
  333.             );
  334.             exec($audio_ffmpeg_string, $buf);
  335.             /** @var string[] $buf */
  336.             if (!file_exists($temporary_sound)) {
  337.                 echo "Can not build sound\n";
  338.  
  339.                 return 6;
  340.             }
  341.  
  342.             $raw_audio_stream_size = null;
  343.             for ($i = count($buf) - 1; ($i >= count($buf) - 5) and ($i >= 0); $i--) {
  344.                 if (preg_match('_ audio:\\s*([0-9.]+)([kb])?B_ui', $buf[$i], $a)) {
  345.                     switch ($a[2]) {
  346.                         case 'k':
  347.                             $multi = 1024;
  348.                             break;
  349.                         case 'm':
  350.                             $multi = 1024 * 1024;
  351.                             break;
  352.                         case '':
  353.                             $multi = 1;
  354.                             break;
  355.                     }
  356.                     $raw_audio_stream_size = floatval($a[1]) * $multi;
  357.                     break;
  358.                 }
  359.             }
  360.         } else {
  361.             $raw_audio_stream_size = 0;
  362.         }
  363.         if ($try_number == 0) {
  364.             $video_bitrate = ($output_estimated_filesize - $file_estimated_overhead - $raw_audio_stream_size) * 8 / $duration;
  365.         }
  366.  
  367.         if ($video_bitrate <= 0) {
  368.             echo "too large file anyway\n";
  369.  
  370.             return 8;
  371.         }
  372.         echo "Audio stream size:\t{$raw_audio_stream_size}\nVideo stream bitrate:\t{$video_bitrate}\n";
  373.  
  374.         $video_filters = [
  375.             'format=yuv420p',
  376.         ];
  377.         if (isset($options['ass'])) {
  378.             $video_filters[] = 'ass='.escapeshellarg($options['ass']);
  379.         }
  380.         if (isset($options['scale'])) {
  381.             $video_filters[] = 'scale='.escapeshellarg($options['scale']);
  382.         }
  383.         if (isset($options['vf'])) {
  384.             // @todo explode(',',..)
  385.             // @todo explode('=',..)
  386.             $video_filters = array_merge($video_filters, explode(',', $options['vf']));
  387.         }
  388.  
  389.         list($mp4_level, $mp4_profile) = get_level($macroblocks_count, $macroblocks_per_second, intval($video_bitrate));
  390.         echo sprintf("H264-Level:     \t%s [%s]\n", $mp4_level, $mp4_profile);
  391.  
  392.         /**
  393.          * https://trac.ffmpeg.org/wiki/Encode/H.264
  394.          * https://ru.wikipedia.org/wiki/H.264#%D0%A3%D1%80%D0%BE%D0%B2%D0%BD%D0%B8 Levels
  395.          *
  396.          * 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
  397.          *
  398.          * preset:v slow — качество
  399.          * format=yuv420p — чтобы не было 10bit (будет, если оригинал был 10bit)
  400.          */
  401.         $video_ffmpeg_string = sprintf(
  402.             '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',
  403.  
  404.             escapeshellarg($options['i']),
  405.             $ffmpeg_start_time,
  406.             $ffmpeg_duration,
  407.             intval($video_bitrate),
  408.             isset($options['fast']) ? 'fast' : 'slow',
  409.             escapeshellarg($mp4_profile),
  410.             escapeshellarg($mp4_level),
  411.             (($options['threads'] != 0) ? ' -threads '.$options['threads'] : ''),
  412.             isset($options['tune']) ? ' -tune '.$options['tune'] : '',
  413.             implode(',', $video_filters),
  414.             escapeshellarg($vtw_passlog),
  415.             isset($options['no-metadata-import']) ? '-map_metadata -1' : ''
  416.         );
  417.         // Meta
  418.         foreach ($options as $key => $value) {
  419.             if (preg_match('|^meta\\-(.+)$|ui', $key, $a)) {
  420.                 $video_ffmpeg_string .= ' -metadata '.$a[1].'='.escapeshellarg($value);
  421.             }
  422.         }
  423.         unset($key, $value, $a);
  424.  
  425.         // Запускаем двух-проходку
  426.         echo $video_ffmpeg_string."\n";
  427.         system(sprintf('%s -pass 1 %s %s %s',
  428.             $video_ffmpeg_string,
  429.             !isset($options['fast']) ? '-fastfirstpass 0' : '',
  430.             escapeshellarg($temporary_video),
  431.             isset($options['quite-ffmpeg']) ? ' >'.$null_device.' 2>'.$null_device : ''
  432.         ));
  433.         system(sprintf('%s -pass 2 -y %s %s',
  434.             $video_ffmpeg_string,
  435.             escapeshellarg($temporary_video),
  436.             isset($options['quite-ffmpeg']) ? ' >'.$null_device.' 2>'.$null_device : ''
  437.         ));
  438.         if (!file_exists($temporary_video) or (filesize($temporary_video) == 0)) {
  439.             echo "Can not render video\n";
  440.             if (!array_key_exists('an', $options)) {
  441.                 unlink($temporary_sound);
  442.             }
  443.             if (file_exists($temporary_video)) {
  444.                 unlink($temporary_video);
  445.             }
  446.  
  447.             return 7;
  448.         }
  449.         $temporary_video_filesize = filesize($temporary_video);
  450.  
  451.         if (!array_key_exists('an', $options)) {
  452.             $both_ffmpeg_string = sprintf(
  453.                 'ffmpeg -i %s -i %s -c copy -v quiet -y',
  454.  
  455.                 escapeshellarg($temporary_video),
  456.                 escapeshellarg($temporary_sound)
  457.             );
  458.             foreach ($options as $key => $value) {
  459.                 if (preg_match('|^meta\\-(.+)$|ui', $key, $a)) {
  460.                     $both_ffmpeg_string .= ' -metadata '.$a[1].'='.escapeshellarg($value);
  461.                 }
  462.             }
  463.             unset($key, $value, $a);
  464.             $both_ffmpeg_string .= ' '.escapeshellarg($output_file);
  465.             if (isset($options['quite-ffmpeg'])) {
  466.                 $both_ffmpeg_string .= ' >'.$null_device.' 2>'.$null_device;
  467.             }
  468.  
  469.             system($both_ffmpeg_string);
  470.             unlink($temporary_sound);
  471.             unlink($temporary_video);
  472.         } else {
  473.             rename($temporary_video, $output_file);
  474.         }
  475.  
  476.         $size_left = $output_estimated_filesize - filesize($output_file);
  477.         $too_small_file = false;
  478.         if (($try_number == 0) and ($size_left >= 30 * 1024)) {// @todo suppress this behaviour
  479.             // Слишком много еста осталось
  480.             $too_small_file = true;
  481.         } elseif ($size_left >= 0) {
  482.             echo "Size left:\t".number_format($size_left / 1024, 2, '.', '')." KB\n";
  483.  
  484.             break;
  485.         }
  486.  
  487.         // Вычисляем правильный overhead
  488.         $temporary_video_raw = tempnam($temporary_folder, 'vtmp4-').'.mp4';
  489.  
  490.         $both_ffmpeg_string = sprintf(
  491.             'ffmpeg -i %s -c:v copy -an -sn -v quiet -f rawvideo %s',
  492.             escapeshellarg($output_file),
  493.             escapeshellarg($temporary_video_raw)
  494.         );
  495.         if (isset($options['quite-ffmpeg'])) {
  496.             $both_ffmpeg_string .= ' >'.$null_device.' 2>'.$null_device;
  497.         }
  498.         system($both_ffmpeg_string);
  499.         $raw_video_stream_size = filesize($temporary_video_raw);
  500.         // $file_estimated_overhead = $temporary_video_filesize - $raw_video_stream_size - $raw_audio_stream_size;
  501.         unlink($temporary_video_raw);
  502.         unset($temporary_video_raw);
  503.  
  504.         // Вычисляем новый битрейт
  505.         $video_bitrate = $video_bitrate * ($raw_video_stream_size + $size_left) / $raw_video_stream_size - 1024;
  506.         echo sprintf(
  507.             "too %s file %s KB; next bitrate %s kbps\n",
  508.             $too_small_file ? 'small' : 'large',
  509.             number_format(filesize($output_file) / 1024, 1, '.', ''),
  510.             number_format($video_bitrate / 1024, 3, '.', '')
  511.         );
  512.     }
  513. ?>
Advertisement
Add Comment
Please, Sign In to add comment