Passed
Push — master ( d9d94f...477c6b )
by Sébastien
04:36
created

VideoTranscode::transcode()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 35
ccs 0
cts 28
cp 0
rs 9.36
c 0
b 0
f 0
cc 4
nc 4
nop 4
crap 20
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Soluble\MediaTools;
6
7
use Soluble\MediaTools\Config\FFMpegConfig;
8
use Soluble\MediaTools\Exception\FileNotFoundException;
9
use Soluble\MediaTools\Filter\Video\EmptyVideoFilter;
10
use Soluble\MediaTools\Filter\Video\VideoFilterChain;
11
use Soluble\MediaTools\Filter\Video\VideoFilterInterface;
12
use Soluble\MediaTools\Filter\Video\VideoFilterTypeDenoiseInterface;
13
use Soluble\MediaTools\Filter\Video\YadifVideoFilter;
14
use Symfony\Component\Process\Process;
15
16
class VideoTranscode
17
{
18
    /** @var FFMpegConfig */
19
    protected $ffmpegConfig;
20
21
    /** @var VideoProbe */
22
    protected $videoProbe;
23
24
    public function __construct(FFMpegConfig $ffmpegConfig, VideoProbe $videoProbe)
25
    {
26
        $this->videoProbe   = $videoProbe;
27
        $this->ffmpegConfig = $ffmpegConfig;
28
        $this->ffmpegConfig->getProcess()->ensureBinaryExists();
29
    }
30
31
    /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
47% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
32
    public function transcodeMultiPass(string $videoFile, string $outputFile, VideoTranscodeParams $transcodeParams, VideoFilterInterface $videoFilter=null): void {
33
34
        $this->ensureFileExists($videoFile);
35
        if ($videoFilter === null) {
36
            $videoFilter = new EmptyVideoFilter();
37
        }
38
39
40
        $threads = $transcodeParams->getOption(VideoTranscodeParams::OPTION_THREADS, $this->ffmpegConfig->getThreads());
41
42
        $ffmpegBin = $this->ffmpegConfig->getBinary();
43
44
        $commonArgs = array_merge([
45
                $ffmpegBin,
46
                sprintf('-i %s', escapeshellarg($videoFile)), // input filename
47
                $videoFilter->getFFMpegCliArgument(), // add -vf yadif,nlmeans
48
                ($threads === null) ? '' : sprintf('-threads %s', $threads),
49
        ], $transcodeParams->getFFMpegArguments());
50
51
        $pass1Cmd = implode(' ', array_merge(
52
            $commonArgs,
53
            [
54
                '-pass 1',
55
                // tells VP9 to encode really fast, sacrificing quality. Useful to speed up the first pass.
56
                '-speed 4',
57
                '-y /dev/null',
58
            ]
59
        ));
60
61
        $pass2Cmd = implode( ' ', array_merge(
62
            $commonArgs,
63
            [
64
                '-pass 2',
65
                // speed 1 is a good speed vs. quality compromise.
66
                // Produces output quality typically very close to speed 0, but usually encodes much faster.
67
                '-speed 1',
68
                '-y',
69
                sprintf("%s", escapeshellarg($outputFile))
70
            ]
71
        ));
72
73
74
        $process = new Process($pass1Cmd);
75
        $process->setTimeout(null);
76
        $process->setIdleTimeout(60); // 60 seconds without output will stop the process
77
        $process->start();
78
        foreach ($process as $type => $data) {
79
            if ($process::OUT === $type) {
80
                echo "\nRead from stdout: ".$data;
81
            } else { // $process::ERR === $type
82
                echo "\nRead from stderr: ".$data;
83
            }
84
        }
85
86
        $process = new Process($pass2Cmd);
87
        $process->setTimeout(null);
88
        $process->setIdleTimeout(60); // 60 seconds without output will stop the process
89
        $process->start();
90
        foreach ($process as $type => $data) {
91
            if ($process::OUT === $type) {
92
                echo "\nRead from stdout: ".$data;
93
            } else { // $process::ERR === $type
94
                echo "\nRead from stderr: ".$data;
95
            }
96
        }
97
98
    }
99
    */
100
101
    public function transcode(string $videoFile, string $outputFile, VideoTranscodeParams $transcodeParams, ?VideoFilterInterface $videoFilter = null): Process
102
    {
103
        $this->ensureFileExists($videoFile);
104
105
        if ($videoFilter === null) {
106
            $videoFilter = new EmptyVideoFilter();
107
        }
108
109
        $process = $this->ffmpegConfig->getProcess();
110
111
        if (!$transcodeParams->hasOption(VideoTranscodeParams::OPTION_THREADS) && $this->ffmpegConfig->getThreads() !== null) {
112
            $transcodeParams = $transcodeParams->withThreads($this->ffmpegConfig->getThreads());
113
        }
114
115
        $ffmpegCmd = $process->buildCommand(
116
            array_merge(
117
                [
118
                    sprintf('-i %s', escapeshellarg($videoFile)), // input filename
119
                    $videoFilter->getFFMpegCLIArgument(), // add -vf yadif,nlmeans
120
                ],
121
                $transcodeParams->getFFMpegArguments(),
122
                [
123
                    '-y', // tell to overwrite
124
                    sprintf('%s', escapeshellarg($outputFile)),
125
                ]
126
            )
127
        );
128
129
        $process = new Process($ffmpegCmd);
130
        $process->setTimeout(null);
131
        // 60 seconds without output will stop the process
132
        $process->setIdleTimeout(60);
133
        $process->start();
134
135
        return $process;
136
    }
137
138
    /**
139
     * Try to guess if the original video is interlaced (bff, tff) and
140
     * return ffmpeg yadif filter argument and add denoise filter if any.
141
     *
142
     * @see https://ffmpeg.org/ffmpeg-filters.html (section yadif)
143
     * @see https://askubuntu.com/a/867203
144
     *
145
     * @return VideoFilterInterface|VideoFilterChain|EmptyVideoFilter|YadifVideoFilter
146
     */
147
    public function getDeintFilter(string $videoFile, ?VideoFilterTypeDenoiseInterface $denoiseFilter = null): VideoFilterInterface
148
    {
149
        $guess       = $this->videoProbe->guessInterlacing($videoFile);
150
        $deintFilter = $guess->getDeinterlaceVideoFilter();
151
        // skip all filters if video is not interlaces
152
        if ($deintFilter instanceof EmptyVideoFilter) {
153
            return $deintFilter;
154
        }
155
        if ($denoiseFilter !== null) {
156
            $videoFilterChain = new VideoFilterChain();
157
            $videoFilterChain->addFilter($deintFilter);
158
            $videoFilterChain->addFilter($denoiseFilter);
159
160
            return $videoFilterChain;
161
        }
162
163
        return $deintFilter;
164
    }
165
166
    protected function ensureFileExists(string $file): void
167
    {
168
        if (!file_exists($file)) {
169
            throw new FileNotFoundException(sprintf('File "%s" does not exists or is not readable', $file));
170
        }
171
    }
172
}
173