TranscodeVideosCommand::getVideoFiles()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 3
eloc 10
c 2
b 1
f 0
nc 3
nop 1
dl 0
loc 21
ccs 0
cts 15
cp 0
crap 12
rs 9.9332
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
The condition is_string($logFile) is always true.
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