Passed
Push — master ( d0bcc1...2a69c8 )
by Sébastien
02:28
created

VideoConverter::convert()   B

Complexity

Conditions 10
Paths 22

Size

Total Lines 34
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 10.1371

Importance

Changes 0
Metric Value
eloc 29
dl 0
loc 34
ccs 24
cts 27
cp 0.8889
rs 7.6666
c 0
b 0
f 0
cc 10
nc 22
nop 5
crap 10.1371

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Soluble\MediaTools\Video;
6
7
use Psr\Log\LoggerInterface;
8
use Psr\Log\LogLevel;
9
use Psr\Log\NullLogger;
10
use Soluble\MediaTools\Common\Assert\PathAssertionsTrait;
11
use Soluble\MediaTools\Common\Exception as CommonException;
12
use Soluble\MediaTools\Common\IO\UnescapedFileInterface;
13
use Soluble\MediaTools\Common\Process\ProcessFactory;
14
use Soluble\MediaTools\Common\Process\ProcessParamsInterface;
15
use Soluble\MediaTools\Video\Config\FFMpegConfigInterface;
16
use Soluble\MediaTools\Video\Exception\ConverterExceptionInterface;
17
use Soluble\MediaTools\Video\Exception\ConverterProcessExceptionInterface;
18
use Soluble\MediaTools\Video\Exception\InvalidArgumentException;
19
use Soluble\MediaTools\Video\Exception\InvalidParamException;
20
use Soluble\MediaTools\Video\Exception\MissingInputFileException;
21
use Soluble\MediaTools\Video\Exception\ProcessFailedException;
22
use Soluble\MediaTools\Video\Exception\ProcessSignaledException;
23
use Soluble\MediaTools\Video\Exception\ProcessTimedOutException;
24
use Soluble\MediaTools\Video\Exception\RuntimeReaderException;
25
use Symfony\Component\Process\Exception as SPException;
26
use Symfony\Component\Process\Process;
27
28
class VideoConverter implements VideoConverterInterface
29
{
30
    use PathAssertionsTrait;
31
32
    /** @var FFMpegConfigInterface */
33
    protected $ffmpegConfig;
34
35
    /** @var LoggerInterface */
36
    protected $logger;
37
38 19
    public function __construct(FFMpegConfigInterface $ffmpegConfig, ?LoggerInterface $logger = null)
39
    {
40 19
        $this->ffmpegConfig = $ffmpegConfig;
41
42 19
        $this->logger = $logger ?? new NullLogger();
43 19
    }
44
45
    /**
46
     * Return ready-to-run symfony process object that you can use
47
     * to `run()` or `start()` programmatically. Useful if you want to make
48
     * things async...
49
     *
50
     * @param null|string|UnescapedFileInterface $outputFile
51
     *
52
     * @see https://symfony.com/doc/current/components/process.html
53
     *
54
     * @throws CommonException\UnsupportedParamException
55
     * @throws CommonException\UnsupportedParamValueException
56
     * @throws InvalidArgumentException
57
     */
58 17
    public function getSymfonyProcess(string $inputFile, $outputFile, VideoConvertParamsInterface $convertParams, ?ProcessParamsInterface $processParams = null): Process
59
    {
60 17
        $adapter = $this->ffmpegConfig->getAdapter();
61
62 17
        if (!$convertParams->hasParam(VideoConvertParamsInterface::PARAM_THREADS)
63 17
            && $adapter->getDefaultThreads() !== null) {
64 1
            $convertParams = $convertParams->withBuiltInParam(
65 1
                VideoConvertParamsInterface::PARAM_THREADS,
66 1
                $adapter->getDefaultThreads()
67
            );
68
        }
69
70 17
        $arguments = $adapter->getMappedConversionParams($convertParams);
71
72
        try {
73 17
            $ffmpegCmd = $adapter->getCliCommand($arguments, $inputFile, $outputFile);
74 1
        } catch (CommonException\InvalidArgumentException $e) {
75 1
            throw new InvalidArgumentException($e->getMessage(), (int) $e->getCode(), $e);
76
        }
77
78 16
        $pp = $processParams ?? $this->ffmpegConfig->getProcessParams();
79
80 16
        return (new ProcessFactory($ffmpegCmd, $pp))();
81
    }
82
83
    /**
84
     * Run a conversion, throw exception on error.
85
     *
86
     * @param null|string|UnescapedFileInterface $outputFile
87
     * @param callable|null                      $callback   A PHP callback to run whenever there is some
88
     *                                                       tmp available on STDOUT or STDERR
89
     *
90
     * @throws ConverterExceptionInterface        Base exception class for conversion exceptions
91
     * @throws ConverterProcessExceptionInterface Base exception class for process conversion exceptions
92
     * @throws MissingInputFileException
93
     * @throws ProcessTimedOutException
94
     * @throws ProcessFailedException
95
     * @throws ProcessSignaledException
96
     * @throws InvalidParamException
97
     * @throws RuntimeReaderException
98
     */
99 15
    public function convert(string $inputFile, $outputFile, VideoConvertParamsInterface $convertParams, ?callable $callback = null, ?ProcessParamsInterface $processParams = null): void
100
    {
101
        try {
102
            try {
103 15
                $this->ensureFileReadable($inputFile);
104 13
                $process = $this->getSymfonyProcess($inputFile, $outputFile, $convertParams, $processParams);
105 13
                $process->mustRun($callback);
106 10
            } catch (CommonException\FileNotFoundException | CommonException\FileNotReadableException $e) {
107 2
                throw new MissingInputFileException($e->getMessage());
108 8
            } catch (CommonException\UnsupportedParamValueException | CommonException\UnsupportedParamException $e) {
109
                throw new InvalidParamException($e->getMessage());
110 8
            } catch (SPException\ProcessTimedOutException $e) {
111 3
                throw new ProcessTimedOutException($e->getProcess(), $e);
112 5
            } catch (SPException\ProcessSignaledException $e) {
113
                throw new ProcessSignaledException($e->getProcess(), $e);
114 5
            } catch (SPException\ProcessFailedException $e) {
115 5
                throw new ProcessFailedException($e->getProcess(), $e);
116
            } catch (SPException\RuntimeException $e) {
117 6
                throw new RuntimeReaderException($e->getMessage());
118
            }
119 10
        } catch (\Throwable $e) {
120 10
            $exceptionNs = explode('\\', get_class($e));
121 10
            $this->logger->log(
122 10
                ($e instanceof MissingInputFileException) ? LogLevel::WARNING : LogLevel::ERROR,
123 10
                sprintf(
124 10
                    'Video conversion failed \'%s\' with \'%s\'. "%s(%s, %s,...)"',
125 10
                    $exceptionNs[count($exceptionNs) - 1],
126 10
                    __METHOD__,
127 10
                    $e->getMessage(),
128
                    $inputFile,
129 10
                    $outputFile instanceof UnescapedFileInterface ? $outputFile->getFile() : $outputFile
130
                )
131
            );
132 10
            throw $e;
133
        }
134 6
    }
135
136
    /*
137
     * FOR LATER REFERENCE !!!
138
    public function convertMultiPass(string $videoFile, string $outputFile, VideoConvertParams $convertParams, VideoFilterInterface $videoFilter=null): void {
139
140
        $this->ensureFileExists($videoFile);
141
        if ($videoFilter === null) {
142
            $videoFilter = new EmptyVideoFilter();
143
        }
144
145
146
        $threads = $convertParams->getOption(VideoConversionParams::OPTION_THREADS, $this->ffmpegConfig->getThreads());
147
148
        $ffmpegBin = $this->ffmpegConfig->getBinary();
149
150
        $commonArgs = array_merge([
151
                $ffmpegBin,
152
                sprintf('-i %s', escapeshellarg($videoFile)), // input filename
153
                $videoFilter->getFFMpegCliArgument(), // add -vf yadif,nlmeans
154
                ($threads === null) ? '' : sprintf('-threads %s', $threads),
155
        ], $convertParams->getFFMpegArguments());
156
157
        $pass1Cmd = implode(' ', array_merge(
158
            $commonArgs,
159
            [
160
                '-pass 1',
161
                // tells VP9 to encode really fast, sacrificing quality. Useful to speed up the first pass.
162
                '-speed 4',
163
                '-y /dev/null',
164
            ]
165
        ));
166
167
        $pass2Cmd = implode( ' ', array_merge(
168
            $commonArgs,
169
            [
170
                '-pass 2',
171
                // speed 1 is a good speed vs. quality compromise.
172
                // Produces tmp quality typically very close to speed 0, but usually encodes much faster.
173
                '-speed 1',
174
                '-y',
175
                sprintf("%s", escapeshellarg($outputFile))
176
            ]
177
        ));
178
179
180
        $process = new Process($pass1Cmd);
181
        $process->setTimeout(null);
182
        $process->setIdleTimeout(60); // 60 seconds without tmp will stop the process
183
        $process->start();
184
        foreach ($process as $type => $data) {
185
            if ($process::OUT === $type) {
186
                echo "\nRead from stdout: ".$data;
187
            } else { // $process::ERR === $type
188
                echo "\nRead from stderr: ".$data;
189
            }
190
        }
191
192
        $process = new Process($pass2Cmd);
193
        $process->setTimeout(null);
194
        $process->setIdleTimeout(60); // 60 seconds without tmp will stop the process
195
        $process->start();
196
        foreach ($process as $type => $data) {
197
            if ($process::OUT === $type) {
198
                echo "\nRead from stdout: ".$data;
199
            } else { // $process::ERR === $type
200
                echo "\nRead from stderr: ".$data;
201
            }
202
        }
203
204
    }
205
    */
206
}
207