Passed
Push — master ( b50829...4dee5e )
by Sébastien
06:05
created

TranscodeVideosCommand::getH264PresetParams()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
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 11
ccs 0
cts 11
cp 0
crap 2
rs 9.9666
c 0
b 0
f 0
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
128
            $rows[] = [
129
                $video->getBasename(),
130
                sprintf('%sx%s', $vStream->getWidth(), $vStream->getHeight()),
131
                $info->getDuration(),
132
                $vStream->getBitRate(),
133
                $vStream->getCodecName(),
134
                $pixFmt,
135
                $interlaceMode,
136
                filesize($videoFile)
137
            ];
138
139
            $extraParams = new VideoConvertParams();
140
            if ($pixFmt !== 'yuv420p') {
141
                $extraParams = $extraParams->withPixFmt('yuv420p');
142
            }
143
            if ($interlaceMode !== '') {
144
                $extraParams = $extraParams->withVideoFilter(
145
                    new VideoFilterChain([
146
                        new YadifVideoFilter(),
147
                        new Hqdn3DVideoFilter()
148
                    ])
149
                );
150
            } else {
151
                new VideoFilterChain([
152
                    new Hqdn3DVideoFilter()
153
                ]);
154
            }
155
156
            // VP9 conversion
157
            $vp9Output = sprintf(
158
                '%s/%s%s',
159
                $outputPath,
160
                basename($videoFile, pathinfo($videoFile, PATHINFO_EXTENSION)),
161
                'webm'
162
            );
163
164
            if ($convertVP9 && !file_exists($vp9Output)) {
165
                $this->convertVP9SinglePass(
166
                    $videoFile,
167
                    $vp9Output,
168
                    $extraParams
169
                );
170
                // to allow laptop to cool down
171
                sleep(60);
172
            }
173
174
            // H264 conversion
175
            $h264Output = sprintf(
176
                '%s/%s%s',
177
                $outputPath,
178
                basename($videoFile, pathinfo($videoFile, PATHINFO_EXTENSION)),
179
                'mp4'
180
            );
181
182
            if ($convertH264 && !file_exists($h264Output)) {
183
                $this->convertH264(
184
                    $videoFile,
185
                    $h264Output,
186
                    $extraParams
187
                );
188
                sleep(60);
189
            }
190
191
            $progressBar->advance();
192
        }
193
194
        $output->writeln('');
195
196
        $table = new Table($output);
197
        $table->setHeaders([
198
            'file', 'size', 'duration', 'bitrate', 'codec', 'fmt', 'interlace', 'filesize'
199
        ]);
200
        $table->setRows($rows ?? []);
201
        $table->render();
202
203
        $output->writeln("\nFinished");
204
205
        return 1;
206
    }
207
208
    /**
209
     * @param string $videoPath
210
     *
211
     * @return array<\SplFileInfo>
212
     */
213
    public function getVideoFiles(string $videoPath): array
214
    {
215
        $files = (new Finder())->files()
216
            ->in($videoPath)
217
            ->name(sprintf(
218
                '/\.(%s)$/',
219
                implode('|', $this->supportedVideoExtensions)
220
            ));
221
222
        $videos = [];
223
224
        /** @var \SplFileInfo $file */
225
        foreach ($files as $file) {
226
            // original files ust not be converted, an mkv have been
227
            // provided
228
            if (!preg_match('/\.original\./', $file->getPathname())) {
229
                $videos[] = $file;
230
            }
231
        }
232
233
        return $videos;
234
    }
235
236
    public function convertH264(string $input, string $output, VideoConvertParamsInterface $extraParams): void
237
    {
238
        $params = $this->getH264PresetParams(4);
239
        $params = $params->withConvertParams($extraParams);
240
241
        $tmpOutput = $output . '.tmp';
242
243
        $this->videoConverter->convert(
244
            $input,
245
            $tmpOutput,
246
            $params
247
        );
248
249
        if (!file_exists($tmpOutput)) {
250
            throw new \Exception(sprintf(
251
                'Temp file %s does not exists',
252
                $tmpOutput
253
            ));
254
        }
255
256
        rename($tmpOutput, $output);
257
    }
258
259
    public function getH264PresetParams(int $threads): VideoConvertParams
260
    {
261
        return (new VideoConvertParams())
262
            ->withVideoCodec('h264')
263
            ->withAudioCodec('aac')
264
            ->withAudioBitrate('128k')
265
            ->withPreset('medium')
266
            ->withStreamable(true)
267
            ->withCrf(24)
268
            ->withThreads($threads)
269
            ->withOutputFormat('mp4');
270
    }
271
272
    public function convertVP9SinglePass(string $input, string $output, VideoConvertParamsInterface $extraParams): void
273
    {
274
        $params = (new VideoConvertParams())
275
            ->withVideoCodec('libvpx-vp9')
276
            ->withVideoBitrate('850k')
277
            ->withVideoMinBitrate('400k')
278
            ->withVideoMaxBitrate('1200k')
279
            ->withQuality('good')
280
            ->withCrf(32)
281
            ->withThreads(8)
282
            ->withKeyframeSpacing(240)
283
            ->withTileColumns(2)
284
            ->withFrameParallel(1)
285
            ->withOutputFormat('webm')
286
            ->withConvertParams($extraParams)
287
            ->withSpeed(1)
288
            ->withAudioCodec('libopus')
289
            ->withAudioBitrate('128k');
290
291
        $tmpOutput = $output . '.tmp';
292
293
        $this->videoConverter->convert(
294
            $input,
295
            $tmpOutput,
296
            $params
297
        );
298
299
        if (!file_exists($tmpOutput)) {
300
            throw new \Exception(sprintf(
301
                'Temp file %s does not exists',
302
                $tmpOutput
303
            ));
304
        }
305
306
        rename($tmpOutput, $output);
307
    }
308
309
    public function convertVP9Multipass(string $input, string $output, VideoConvertParamsInterface $extraParams): void
310
    {
311
        /**
312
         * /opt/ffmpeg/ffmpeg -i '/web/material-for-the-spine/latest_sources/goldberg.mov' -vf yadif,hqdn3d -b:v 1024k \
313
         * -minrate 512k -maxrate 1485k -tile-columns 2 -g 240 -threads 8 \
314
         * -quality good -crf 32 -c:v libvpx-vp9 -an \
315
         * -pass 1 -passlogfile /tmp/ffmpeg-passlog-goldberg.log -speed 4 -f webm -y /dev/null && \
316
         * /opt/ffmpeg/ffmpeg -i '/web/material-for-the-spine/latest_sources/goldberg.mov' -vf yadif,hqdn3d -b:v 1024k \
317
         * -minrate 512k -maxrate 1485k -tile-columns 2 -g 240 -threads 8 \
318
         * -quality good -crf 32 -auto-alt-ref 1 -lag-in-frames 25 -c:v libvpx-vp9 -c:a libopus \
319
         * -pass 2 -passlogfile /tmp/ffmpeg-passlog-goldberg.log -speed 2 -y /tmp/goldberg.multipass.new.webm.
320
         */
321
        $logFile = tempnam(sys_get_temp_dir(), 'ffmpeg-log');
322
323
        $firstPassParams = (new VideoConvertParams())
324
            // VIDEO FILTERS MUST BE DONE BEFORE
325
            // CODEC SELECTION
326
            ->withConvertParams($extraParams)
327
            ->withVideoCodec('libvpx-vp9')
328
            ->withVideoBitrate('1024k')
329
            ->withVideoMinBitrate('512k')
330
            ->withVideoMaxBitrate('1485k')
331
            ->withQuality('good')
332
            ->withCrf(32)
333
            ->withThreads(8)
334
            ->withKeyframeSpacing(240)
335
            ->withTileColumns(2)
336
            ->withFrameParallel(1)
337
            ->withOutputFormat('webm')
338
            ->withSpeed(4)
339
            ->withPass(1)
340
            ->withPassLogFile($logFile ?: '/tmp/ffmpeg-log');
341
342
        try {
343
            $pass1Process = $this->videoConverter->getSymfonyProcess(
344
                $input,
345
                new PlatformNullFile(),
346
                // We don't need audio (speedup)
347
                $firstPassParams->withNoAudio()
348
            );
349
            //var_dump($pass1Process->getCommandLine());
350
            //die();
351
            $pass1Process->mustRun();
352
        } catch (\Throwable $e) {
353
            if ($logFile && file_exists($logFile)) {
354
                unlink($logFile);
355
            }
356
            throw $e;
357
        }
358
359
        $secondPassParams = $firstPassParams
360
                                ->withConvertParams($extraParams)
361
                                ->withSpeed(2)
362
                                ->withPass(2)
363
                                ->withAudioCodec('libopus')
364
                                ->withAudioBitrate('128k')
365
                                ->withAutoAltRef(1)
366
                                ->withLagInFrames(25);
367
368
        $tmpOutput = $output . '.tmp';
369
370
        $pass2Process = $this->videoConverter->getSymfonyProcess(
371
            $input,
372
            $tmpOutput,
373
            $secondPassParams
374
        );
375
376
        //var_dump($pass2Process->getCommandLine());
377
        $pass2Process->mustRun();
378
379
        if (!file_exists($tmpOutput)) {
380
            throw new \Exception(sprintf(
381
                'Temp file %s does not exists',
382
                $tmpOutput
383
            ));
384
        }
385
386
        rename($tmpOutput, $output);
387
    }
388
}
389