Failed Conditions
Push — master ( 30ba3e...71771c )
by Sébastien
02:18
created

TranscodeVideosCommand::getH264PresetParams()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 1
dl 0
loc 12
ccs 0
cts 11
cp 0
crap 2
rs 9.9666
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace App\Command;
4
5
use Soluble\MediaTools\Common\IO\PlatformNullFile;
6
use Soluble\MediaTools\Video\Filter\Hqdn3DVideoFilter;
7
use Soluble\MediaTools\Video\Filter\VideoFilterChain;
8
use Soluble\MediaTools\Video\Filter\YadifVideoFilter;
9
use Soluble\MediaTools\Video\VideoAnalyzerInterface;
10
use Soluble\MediaTools\Video\VideoConverterInterface;
11
use Soluble\MediaTools\Video\VideoConvertParams;
12
use Soluble\MediaTools\Video\VideoConvertParamsInterface;
13
use Soluble\MediaTools\Video\VideoInfoReaderInterface;
14
use Symfony\Component\Console\Command\Command;
15
use Symfony\Component\Console\Helper\ProgressBar;
16
use Symfony\Component\Console\Helper\Table;
17
use Symfony\Component\Console\Input\InputArgument;
18
use Symfony\Component\Console\Input\InputDefinition;
19
use Symfony\Component\Console\Input\InputInterface;
20
use Symfony\Component\Console\Input\InputOption;
21
use Symfony\Component\Console\Output\OutputInterface;
22
use Symfony\Component\Finder\Finder;
23
24
class TranscodeVideosCommand extends Command
25
{
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
    /**
59
     * Configures the command
60
     */
61
    protected function configure(): void
62
    {
63
        $this
64
            ->setName('transcode:videos')
65
            ->setDescription('Generate mp4/vp9 videos from directory')
66
            ->setDefinition(
67
                new InputDefinition([
68
                    new InputOption('dir', 'd', InputOption::VALUE_REQUIRED),
69
                ])
70
            );
71
    }
72
73
    /**
74
     * Executes the current command
75
     */
76
    protected function execute(InputInterface $input, OutputInterface $output): void
77
    {
78
        if (!$input->hasOption('dir')) {
79
            throw new \Exception('Missing dir argument, use <command> <dir>');
80
        }
81
        $videoPath = $input->hasOption('dir') ? $input->getOption('dir') : '';
82
        if (!is_string($videoPath) || !is_dir($videoPath)) {
83
            throw new \Exception(sprintf("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
            $pixFmt = $info->getVideoStreamInfo()['pix_fmt'];
124
125
            $rows[] = [
126
                $video->getBasename(),
127
                sprintf("%sx%s", $info->getWidth(), $info->getHeight()),
128
                $info->getDuration(),
129
                $info->getBitrate(),
130
                $info->getVideoStreamInfo()['codec_name'],
131
                $pixFmt,
132
                $interlaceMode,
133
                filesize($videoFile)
134
            ];
135
136
            $extraParams = new VideoConvertParams();
137
            if ($pixFmt !== 'yuv420p') {
138
                $extraParams = $extraParams->withPixFmt('yuv420p');
139
            }
140
            if ($interlaceMode !== '') {
141
                $extraParams = $extraParams->withVideoFilter(
142
                    new VideoFilterChain([
143
                        new YadifVideoFilter(),
144
                        new Hqdn3DVideoFilter()
145
                    ])
146
                );
147
            } else {
148
                new VideoFilterChain([
149
                    new Hqdn3DVideoFilter()
150
                ]);
151
            }
152
153
            // VP9 conversion
154
            $vp9Output = sprintf(
155
                '%s/%s%s',
156
                $outputPath,
157
                basename($videoFile, pathinfo($videoFile, PATHINFO_EXTENSION)),
158
                'webm'
159
            );
160
161
            if ($convertVP9 && !file_exists($vp9Output)) {
162
                $this->convertVP9SinglePass(
163
                    $videoFile,
164
                    $vp9Output,
165
                    $extraParams
166
                );
167
168
169
                /*
170
                $this->convertVP9Multipass(
171
                    $videoFile,
172
                    $vp9Output,
173
                    $extraParams
174
                );*/
175
                sleep(60);
176
            }
177
178
179
180
            // H264 conversion
181
            $h264Output =  sprintf(
182
                '%s/%s%s',
183
                $outputPath,
184
                basename($videoFile, pathinfo($videoFile, PATHINFO_EXTENSION)),
185
                'mp4'
186
            );
187
188
            if ($convertH264 && !file_exists($h264Output)) {
189
                $this->convertH264(
190
                    $videoFile,
191
                    $h264Output,
192
                    $extraParams
193
                );
194
                sleep(60);
195
            }
196
197
198
199
            $progressBar->advance();
200
        }
201
202
        $output->writeln('');
203
204
        $table = new Table($output);
205
        $table->setHeaders([
206
            'file', 'size', 'duration', 'bitrate', 'codec', 'fmt', 'interlace', 'filesize'
207
        ]);
208
        $table->setRows($rows ?? []);
209
        $table->render();
210
211
        /*
212
        foreach($commands['mp4'] as $cmd) {
213
            $output->writeln($cmd . ";");
214
        }*/
215
216
        /*
217
        $table = new Table($output);
218
        $table->setHeaders([
219
            'vp9 commands'
220
        ]);
221
        $table->setRows($commands['vp9']);
222
        $table->render();
223
224
225
        $table = new Table($output);
226
        $table->setHeaders([
227
            'mp4 commands'
228
        ]);
229
        $table->setRows($commands['mp4']);
230
        $table->render();
231
        */
232
233
        $output->writeln("\nFinished");
234
    }
235
236
    /**
237
     * @param string $videoPath
238
     * @return array<\SplFileInfo>
239
     */
240
    public function getVideoFiles(string $videoPath): array
241
    {
242
243
        $files = (new Finder())->files()
244
            ->in($videoPath)
245
            ->name(sprintf(
246
                '/\.(%s)$/',
247
                implode('|', $this->supportedVideoExtensions)
248
            ));
249
250
        $videos = [];
251
252
        /** @var \SplFileInfo $file */
253
        foreach ($files as $file) {
254
            // original files ust not be converted, an mkv have been
255
            // provided
256
            if (!preg_match('/\.original\./', $file->getPathname())) {
257
                $videos[] = $file;
258
            }
259
        }
260
261
        return $videos;
262
    }
263
264
    public function convertH264(string $input, string $output, VideoConvertParamsInterface $extraParams): void
265
    {
266
267
        $params = $this->getH264PresetParams(4);
268
        $params = $params->withConvertParams($extraParams);
269
270
        $tmpOutput = $output . '.tmp';
271
272
        $this->videoConverter->convert(
273
            $input,
274
            $tmpOutput,
275
            $params
276
        );
277
278
        if (!file_exists($tmpOutput)) {
279
            throw new \Exception(sprintf(
280
                "Temp file %s does not exists",
281
                $tmpOutput
282
            ));
283
        }
284
285
        rename($tmpOutput, $output);
286
    }
287
288
    public function getH264PresetParams(int $threads): VideoConvertParams
289
    {
290
291
        return (new VideoConvertParams())
292
            ->withVideoCodec('h264')
293
            ->withAudioCodec('aac')
294
            ->withAudioBitrate('128k')
295
            ->withPreset('medium')
296
            ->withStreamable(true)
297
            ->withCrf(24)
298
            ->withThreads($threads)
299
            ->withOutputFormat('mp4');
300
    }
301
302
303
    public function convertVP9SinglePass(string $input, string $output, VideoConvertParamsInterface $extraParams): void
304
    {
305
306
        $params = (new VideoConvertParams())
307
            ->withVideoCodec('libvpx-vp9')
308
            ->withVideoBitrate('850k')
309
            ->withVideoMinBitrate('400k')
310
            ->withVideoMaxBitrate('1200k')
311
            ->withQuality('good')
312
            ->withCrf(32)
313
            ->withThreads(8)
314
            ->withKeyframeSpacing(240)
315
            ->withTileColumns(2)
316
            ->withFrameParallel(1)
317
            ->withOutputFormat('webm')
318
            ->withConvertParams($extraParams)
319
            ->withSpeed(1)
320
            ->withAudioCodec('libopus')
321
            ->withAudioBitrate('128k');
322
323
        $tmpOutput = $output . '.tmp';
324
325
        $this->videoConverter->convert(
326
            $input,
327
            $tmpOutput,
328
            $params
329
        );
330
331
        if (!file_exists($tmpOutput)) {
332
            throw new \Exception(sprintf(
333
                "Temp file %s does not exists",
334
                $tmpOutput
335
            ));
336
        }
337
338
        rename($tmpOutput, $output);
339
    }
340
341
    public function convertVP9Multipass(string $input, string $output, VideoConvertParamsInterface $extraParams): void
342
    {
343
344
        /**
345
        /opt/ffmpeg/ffmpeg -i '/web/material-for-the-spine/latest_sources/goldberg.mov' -vf yadif,hqdn3d -b:v 1024k \
346
        -minrate 512k -maxrate 1485k -tile-columns 2 -g 240 -threads 8 \
347
        -quality good -crf 32 -c:v libvpx-vp9 -an \
348
        -pass 1 -passlogfile /tmp/ffmpeg-passlog-goldberg.log -speed 4 -f webm -y /dev/null && \
349
        /opt/ffmpeg/ffmpeg -i '/web/material-for-the-spine/latest_sources/goldberg.mov' -vf yadif,hqdn3d -b:v 1024k \
350
        -minrate 512k -maxrate 1485k -tile-columns 2 -g 240 -threads 8 \
351
        -quality good -crf 32 -auto-alt-ref 1 -lag-in-frames 25 -c:v libvpx-vp9 -c:a libopus \
352
        -pass 2 -passlogfile /tmp/ffmpeg-passlog-goldberg.log -speed 2 -y /tmp/goldberg.multipass.new.webm
353
         */
354
355
356
        $logFile = tempnam(sys_get_temp_dir(), 'ffmpeg-log');
357
358
        $firstPassParams = (new VideoConvertParams())
359
            // VIDEO FILTERS MUST BE DONE BEFORE
360
            // CODEC SELECTION
361
            ->withConvertParams($extraParams)
362
            ->withVideoCodec('libvpx-vp9')
363
            ->withVideoBitrate('1024k')
364
            ->withVideoMinBitrate('512k')
365
            ->withVideoMaxBitrate('1485k')
366
            ->withQuality('good')
367
            ->withCrf(32)
368
            ->withThreads(8)
369
            ->withKeyframeSpacing(240)
370
            ->withTileColumns(2)
371
            ->withFrameParallel(1)
372
            ->withOutputFormat('webm')
373
            ->withSpeed(4)
374
            ->withPass(1)
375
            ->withPassLogFile($logFile ?: '/tmp/ffmpeg-log');
376
377
        try {
378
            $pass1Process = $this->videoConverter->getSymfonyProcess(
379
                $input,
380
                new PlatformNullFile(),
381
                // We don't need audio (speedup)
382
                $firstPassParams->withNoAudio()
383
            );
384
            //var_dump($pass1Process->getCommandLine());
385
            //die();
386
            $pass1Process->mustRun();
387
        } catch (\Throwable $e) {
388
            if ($logFile && file_exists($logFile)) {
389
                unlink($logFile);
390
            }
391
            throw $e;
392
        }
393
394
        $secondPassParams = $firstPassParams
395
                                ->withConvertParams($extraParams)
396
                                ->withSpeed(2)
397
                                ->withPass(2)
398
                                ->withAudioCodec('libopus')
399
                                ->withAudioBitrate('128k')
400
                                ->withAutoAltRef(1)
401
                                ->withLagInFrames(25);
402
403
        $tmpOutput = $output . '.tmp';
404
405
        $pass2Process = $this->videoConverter->getSymfonyProcess(
406
            $input,
407
            $tmpOutput,
408
            $secondPassParams
409
        );
410
411
        //var_dump($pass2Process->getCommandLine());
412
        $pass2Process->mustRun();
413
414
        if (!file_exists($tmpOutput)) {
415
            throw new \Exception(sprintf(
416
                "Temp file %s does not exists",
417
                $tmpOutput
418
            ));
419
        }
420
421
        rename($tmpOutput, $output);
422
    }
423
}
424