contredanse /
mfts-server
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | namespace App\Command; |
||
| 6 | |||
| 7 | use Soluble\MediaTools\Common\IO\PlatformNullFile; |
||
| 8 | use Soluble\MediaTools\Video\Filter\Hqdn3DVideoFilter; |
||
| 9 | use Soluble\MediaTools\Video\Filter\VideoFilterChain; |
||
| 10 | use Soluble\MediaTools\Video\Filter\YadifVideoFilter; |
||
| 11 | use Soluble\MediaTools\Video\VideoAnalyzerInterface; |
||
| 12 | use Soluble\MediaTools\Video\VideoConverterInterface; |
||
| 13 | use Soluble\MediaTools\Video\VideoConvertParams; |
||
| 14 | use Soluble\MediaTools\Video\VideoConvertParamsInterface; |
||
| 15 | use Soluble\MediaTools\Video\VideoInfoReaderInterface; |
||
| 16 | use Symfony\Component\Console\Command\Command; |
||
| 17 | use Symfony\Component\Console\Helper\ProgressBar; |
||
| 18 | use Symfony\Component\Console\Helper\Table; |
||
| 19 | use Symfony\Component\Console\Input\InputDefinition; |
||
| 20 | use Symfony\Component\Console\Input\InputInterface; |
||
| 21 | use Symfony\Component\Console\Input\InputOption; |
||
| 22 | use Symfony\Component\Console\Output\OutputInterface; |
||
| 23 | use Symfony\Component\Finder\Finder; |
||
| 24 | |||
| 25 | class TranscodeVideosCommand extends Command |
||
| 26 | { |
||
| 27 | /** |
||
| 28 | * @var VideoInfoReaderInterface |
||
| 29 | */ |
||
| 30 | protected $videoInfoReader; |
||
| 31 | |||
| 32 | /** |
||
| 33 | * @var VideoAnalyzerInterface |
||
| 34 | */ |
||
| 35 | protected $videoAnalyzer; |
||
| 36 | |||
| 37 | /** |
||
| 38 | * @var VideoConverterInterface |
||
| 39 | */ |
||
| 40 | protected $videoConverter; |
||
| 41 | |||
| 42 | /** |
||
| 43 | * @var string[] |
||
| 44 | */ |
||
| 45 | protected $supportedVideoExtensions = [ |
||
| 46 | 'mov', 'mp4', 'mkv', 'flv', 'webm' |
||
| 47 | ]; |
||
| 48 | |||
| 49 | public function __construct(VideoInfoReaderInterface $videoInfoReader, VideoAnalyzerInterface $videoAnalyzer, VideoConverterInterface $videoConverter) |
||
| 50 | { |
||
| 51 | $this->videoInfoReader = $videoInfoReader; |
||
| 52 | $this->videoAnalyzer = $videoAnalyzer; |
||
| 53 | $this->videoConverter = $videoConverter; |
||
| 54 | parent::__construct(); |
||
| 55 | } |
||
| 56 | |||
| 57 | /** |
||
| 58 | * Configures the command. |
||
| 59 | */ |
||
| 60 | protected function configure(): void |
||
| 61 | { |
||
| 62 | $this |
||
| 63 | ->setName('transcode:videos') |
||
| 64 | ->setDescription('Generate mp4/vp9 videos from directory') |
||
| 65 | ->setDefinition( |
||
| 66 | new InputDefinition([ |
||
| 67 | new InputOption('dir', 'd', InputOption::VALUE_REQUIRED), |
||
| 68 | ]) |
||
| 69 | ); |
||
| 70 | } |
||
| 71 | |||
| 72 | /** |
||
| 73 | * {@inheritdoc} |
||
| 74 | */ |
||
| 75 | protected function execute(InputInterface $input, OutputInterface $output) |
||
| 76 | { |
||
| 77 | if (!$input->hasOption('dir')) { |
||
| 78 | throw new \Exception('Missing dir argument, use <command> <dir>'); |
||
| 79 | } |
||
| 80 | $videoPath = $input->hasOption('dir') ? $input->getOption('dir') : ''; |
||
| 81 | if (!is_string($videoPath) || !is_dir($videoPath)) { |
||
| 82 | throw new \Exception(sprintf( |
||
| 83 | 'Video dir %s does not exists', |
||
| 84 | is_string($videoPath) ? $videoPath : '' |
||
| 85 | )); |
||
| 86 | } |
||
| 87 | |||
| 88 | $convertVP9 = true; |
||
| 89 | $convertH264 = true; |
||
| 90 | |||
| 91 | $output->writeln('Getting information'); |
||
| 92 | |||
| 93 | // Get the videos in path |
||
| 94 | |||
| 95 | $videos = $this->getVideoFiles($videoPath); |
||
| 96 | |||
| 97 | $progressBar = new ProgressBar($output, count($videos)); |
||
| 98 | $progressBar->start(); |
||
| 99 | |||
| 100 | $outputPath = $videoPath . '/../latest_conversion'; |
||
| 101 | if (!is_dir($outputPath)) { |
||
| 102 | throw new \Exception('Output path does not exists'); |
||
| 103 | } |
||
| 104 | |||
| 105 | $rows = []; |
||
| 106 | |||
| 107 | /** @var \SplFileInfo $video */ |
||
| 108 | foreach ($videos as $video) { |
||
| 109 | $videoFile = $video->getPathname(); |
||
| 110 | |||
| 111 | $info = $this->videoInfoReader->getInfo($videoFile); |
||
| 112 | |||
| 113 | $interlaceGuess = $this->videoAnalyzer->detectInterlacement( |
||
| 114 | $videoFile, |
||
| 115 | // Max frames to analyze must be big !!! |
||
| 116 | // There's a lot of videos satrting with black |
||
| 117 | 2000 |
||
| 118 | ); |
||
| 119 | |||
| 120 | $interlaceMode = $interlaceGuess->isInterlacedBff(0.4) ? 'BFF' : |
||
| 121 | ($interlaceGuess->isInterlacedTff(0.4) ? 'TFF' : ''); |
||
| 122 | |||
| 123 | $vStream = $info->getVideoStreams()->getFirst(); |
||
| 124 | |||
| 125 | $pixFmt = $vStream->getPixFmt(); |
||
| 126 | |||
| 127 | $rows[] = [ |
||
| 128 | $video->getBasename(), |
||
| 129 | sprintf('%sx%s', $vStream->getWidth(), $vStream->getHeight()), |
||
| 130 | $info->getDuration(), |
||
| 131 | $vStream->getBitRate(), |
||
| 132 | $vStream->getCodecName(), |
||
| 133 | $pixFmt, |
||
| 134 | $interlaceMode, |
||
| 135 | filesize($videoFile) |
||
| 136 | ]; |
||
| 137 | |||
| 138 | $extraParams = new VideoConvertParams(); |
||
| 139 | if ($pixFmt !== 'yuv420p') { |
||
| 140 | $extraParams = $extraParams->withPixFmt('yuv420p'); |
||
| 141 | } |
||
| 142 | if ($interlaceMode !== '') { |
||
| 143 | $extraParams = $extraParams->withVideoFilter( |
||
| 144 | new VideoFilterChain([ |
||
| 145 | new YadifVideoFilter(), |
||
| 146 | new Hqdn3DVideoFilter() |
||
| 147 | ]) |
||
| 148 | ); |
||
| 149 | } else { |
||
| 150 | new VideoFilterChain([ |
||
| 151 | new Hqdn3DVideoFilter() |
||
| 152 | ]); |
||
| 153 | } |
||
| 154 | |||
| 155 | // VP9 conversion |
||
| 156 | $vp9Output = sprintf( |
||
| 157 | '%s/%s%s', |
||
| 158 | $outputPath, |
||
| 159 | basename($videoFile, pathinfo($videoFile, PATHINFO_EXTENSION)), |
||
| 160 | 'webm' |
||
| 161 | ); |
||
| 162 | |||
| 163 | if ($convertVP9 && !file_exists($vp9Output)) { |
||
| 164 | $this->convertVP9SinglePass( |
||
| 165 | $videoFile, |
||
| 166 | $vp9Output, |
||
| 167 | $extraParams |
||
| 168 | ); |
||
| 169 | // to allow laptop to cool down |
||
| 170 | sleep(60); |
||
| 171 | } |
||
| 172 | |||
| 173 | // H264 conversion |
||
| 174 | $h264Output = sprintf( |
||
| 175 | '%s/%s%s', |
||
| 176 | $outputPath, |
||
| 177 | basename($videoFile, pathinfo($videoFile, PATHINFO_EXTENSION)), |
||
| 178 | 'mp4' |
||
| 179 | ); |
||
| 180 | |||
| 181 | if ($convertH264 && !file_exists($h264Output)) { |
||
| 182 | $this->convertH264( |
||
| 183 | $videoFile, |
||
| 184 | $h264Output, |
||
| 185 | $extraParams |
||
| 186 | ); |
||
| 187 | sleep(60); |
||
| 188 | } |
||
| 189 | |||
| 190 | $progressBar->advance(); |
||
| 191 | } |
||
| 192 | |||
| 193 | $output->writeln(''); |
||
| 194 | |||
| 195 | $table = new Table($output); |
||
| 196 | $table->setHeaders([ |
||
| 197 | 'file', 'size', 'duration', 'bitrate', 'codec', 'fmt', 'interlace', 'filesize' |
||
| 198 | ]); |
||
| 199 | $table->setRows($rows); |
||
| 200 | $table->render(); |
||
| 201 | |||
| 202 | $output->writeln("\nFinished"); |
||
| 203 | |||
| 204 | return 1; |
||
| 205 | } |
||
| 206 | |||
| 207 | /** |
||
| 208 | * @param string $videoPath |
||
| 209 | * |
||
| 210 | * @return array<\SplFileInfo> |
||
| 211 | */ |
||
| 212 | public function getVideoFiles(string $videoPath): array |
||
| 213 | { |
||
| 214 | $files = (new Finder())->files() |
||
| 215 | ->in($videoPath) |
||
| 216 | ->name(sprintf( |
||
| 217 | '/\.(%s)$/', |
||
| 218 | implode('|', $this->supportedVideoExtensions) |
||
| 219 | )); |
||
| 220 | |||
| 221 | $videos = []; |
||
| 222 | |||
| 223 | /** @var \SplFileInfo $file */ |
||
| 224 | foreach ($files as $file) { |
||
| 225 | // original files ust not be converted, an mkv have been |
||
| 226 | // provided |
||
| 227 | if (preg_match('/\.original\./', $file->getPathname()) !== 0) { |
||
| 228 | $videos[] = $file; |
||
| 229 | } |
||
| 230 | } |
||
| 231 | |||
| 232 | return $videos; |
||
| 233 | } |
||
| 234 | |||
| 235 | public function convertH264(string $input, string $output, VideoConvertParamsInterface $extraParams): void |
||
| 236 | { |
||
| 237 | $params = $this->getH264PresetParams(4); |
||
| 238 | $params = $params->withConvertParams($extraParams); |
||
| 239 | |||
| 240 | $tmpOutput = $output . '.tmp'; |
||
| 241 | |||
| 242 | $this->videoConverter->convert( |
||
| 243 | $input, |
||
| 244 | $tmpOutput, |
||
| 245 | $params |
||
| 246 | ); |
||
| 247 | |||
| 248 | if (!file_exists($tmpOutput)) { |
||
| 249 | throw new \Exception(sprintf( |
||
| 250 | 'Temp file %s does not exists', |
||
| 251 | $tmpOutput |
||
| 252 | )); |
||
| 253 | } |
||
| 254 | |||
| 255 | rename($tmpOutput, $output); |
||
| 256 | } |
||
| 257 | |||
| 258 | public function getH264PresetParams(int $threads): VideoConvertParams |
||
| 259 | { |
||
| 260 | return (new VideoConvertParams()) |
||
| 261 | ->withVideoCodec('h264') |
||
| 262 | ->withAudioCodec('aac') |
||
| 263 | ->withAudioBitrate('128k') |
||
| 264 | ->withPreset('medium') |
||
| 265 | ->withStreamable(true) |
||
| 266 | ->withCrf(24) |
||
| 267 | ->withThreads($threads) |
||
| 268 | ->withOutputFormat('mp4'); |
||
| 269 | } |
||
| 270 | |||
| 271 | public function convertVP9SinglePass(string $input, string $output, VideoConvertParamsInterface $extraParams): void |
||
| 272 | { |
||
| 273 | $params = (new VideoConvertParams()) |
||
| 274 | ->withVideoCodec('libvpx-vp9') |
||
| 275 | ->withVideoBitrate('850k') |
||
| 276 | ->withVideoMinBitrate('400k') |
||
| 277 | ->withVideoMaxBitrate('1200k') |
||
| 278 | ->withQuality('good') |
||
| 279 | ->withCrf(32) |
||
| 280 | ->withThreads(8) |
||
| 281 | ->withKeyframeSpacing(240) |
||
| 282 | ->withTileColumns(2) |
||
| 283 | ->withFrameParallel(1) |
||
| 284 | ->withOutputFormat('webm') |
||
| 285 | ->withConvertParams($extraParams) |
||
| 286 | ->withSpeed(1) |
||
| 287 | ->withAudioCodec('libopus') |
||
| 288 | ->withAudioBitrate('128k'); |
||
| 289 | |||
| 290 | $tmpOutput = $output . '.tmp'; |
||
| 291 | |||
| 292 | $this->videoConverter->convert( |
||
| 293 | $input, |
||
| 294 | $tmpOutput, |
||
| 295 | $params |
||
| 296 | ); |
||
| 297 | |||
| 298 | if (!file_exists($tmpOutput)) { |
||
| 299 | throw new \Exception(sprintf( |
||
| 300 | 'Temp file %s does not exists', |
||
| 301 | $tmpOutput |
||
| 302 | )); |
||
| 303 | } |
||
| 304 | |||
| 305 | rename($tmpOutput, $output); |
||
| 306 | } |
||
| 307 | |||
| 308 | public function convertVP9Multipass(string $input, string $output, VideoConvertParamsInterface $extraParams): void |
||
| 309 | { |
||
| 310 | /** |
||
| 311 | * /opt/ffmpeg/ffmpeg -i '/web/material-for-the-spine/latest_sources/goldberg.mov' -vf yadif,hqdn3d -b:v 1024k \ |
||
| 312 | * -minrate 512k -maxrate 1485k -tile-columns 2 -g 240 -threads 8 \ |
||
| 313 | * -quality good -crf 32 -c:v libvpx-vp9 -an \ |
||
| 314 | * -pass 1 -passlogfile /tmp/ffmpeg-passlog-goldberg.log -speed 4 -f webm -y /dev/null && \ |
||
| 315 | * /opt/ffmpeg/ffmpeg -i '/web/material-for-the-spine/latest_sources/goldberg.mov' -vf yadif,hqdn3d -b:v 1024k \ |
||
| 316 | * -minrate 512k -maxrate 1485k -tile-columns 2 -g 240 -threads 8 \ |
||
| 317 | * -quality good -crf 32 -auto-alt-ref 1 -lag-in-frames 25 -c:v libvpx-vp9 -c:a libopus \ |
||
| 318 | * -pass 2 -passlogfile /tmp/ffmpeg-passlog-goldberg.log -speed 2 -y /tmp/goldberg.multipass.new.webm. |
||
| 319 | */ |
||
| 320 | $logFile = tempnam(sys_get_temp_dir(), 'ffmpeg-log'); |
||
| 321 | |||
| 322 | $firstPassParams = (new VideoConvertParams()) |
||
| 323 | // VIDEO FILTERS MUST BE DONE BEFORE |
||
| 324 | // CODEC SELECTION |
||
| 325 | ->withConvertParams($extraParams) |
||
| 326 | ->withVideoCodec('libvpx-vp9') |
||
| 327 | ->withVideoBitrate('1024k') |
||
| 328 | ->withVideoMinBitrate('512k') |
||
| 329 | ->withVideoMaxBitrate('1485k') |
||
| 330 | ->withQuality('good') |
||
| 331 | ->withCrf(32) |
||
| 332 | ->withThreads(8) |
||
| 333 | ->withKeyframeSpacing(240) |
||
| 334 | ->withTileColumns(2) |
||
| 335 | ->withFrameParallel(1) |
||
| 336 | ->withOutputFormat('webm') |
||
| 337 | ->withSpeed(4) |
||
| 338 | ->withPass(1) |
||
| 339 | ->withPassLogFile(is_string($logFile) ? $logFile : '/tmp/ffmpeg-log'); |
||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 340 | |||
| 341 | try { |
||
| 342 | $pass1Process = $this->videoConverter->getSymfonyProcess( |
||
| 343 | $input, |
||
| 344 | new PlatformNullFile(), |
||
| 345 | // We don't need audio (speedup) |
||
| 346 | $firstPassParams->withNoAudio() |
||
| 347 | ); |
||
| 348 | //var_dump($pass1Process->getCommandLine()); |
||
| 349 | //die(); |
||
| 350 | $pass1Process->mustRun(); |
||
| 351 | } catch (\Throwable $e) { |
||
| 352 | if (is_string($logFile) && file_exists($logFile)) { |
||
| 353 | unlink($logFile); |
||
| 354 | } |
||
| 355 | throw $e; |
||
| 356 | } |
||
| 357 | |||
| 358 | $secondPassParams = $firstPassParams |
||
| 359 | ->withConvertParams($extraParams) |
||
| 360 | ->withSpeed(2) |
||
| 361 | ->withPass(2) |
||
| 362 | ->withAudioCodec('libopus') |
||
| 363 | ->withAudioBitrate('128k') |
||
| 364 | ->withAutoAltRef(1) |
||
| 365 | ->withLagInFrames(25); |
||
| 366 | |||
| 367 | $tmpOutput = $output . '.tmp'; |
||
| 368 | |||
| 369 | $pass2Process = $this->videoConverter->getSymfonyProcess( |
||
| 370 | $input, |
||
| 371 | $tmpOutput, |
||
| 372 | $secondPassParams |
||
| 373 | ); |
||
| 374 | |||
| 375 | //var_dump($pass2Process->getCommandLine()); |
||
| 376 | $pass2Process->mustRun(); |
||
| 377 | |||
| 378 | if (!file_exists($tmpOutput)) { |
||
| 379 | throw new \Exception(sprintf( |
||
| 380 | 'Temp file %s does not exists', |
||
| 381 | $tmpOutput |
||
| 382 | )); |
||
| 383 | } |
||
| 384 | |||
| 385 | rename($tmpOutput, $output); |
||
| 386 | } |
||
| 387 | } |
||
| 388 |