Failed Conditions
Push — master ( 12954f...f40466 )
by Sébastien
02:10
created

VideoConverter   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 105
Duplicated Lines 0 %

Test Coverage

Coverage 93.33%

Importance

Changes 0
Metric Value
wmc 15
eloc 48
dl 0
loc 105
ccs 42
cts 45
cp 0.9333
rs 10
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getSymfonyProcess() 0 23 4
B convert() 0 34 10
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\InvalidParamReaderException;
20
use Soluble\MediaTools\Video\Exception\MissingInputFileReaderException;
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|NullLogger */
36
    protected $logger;
37
38 16
    public function __construct(FFMpegConfigInterface $ffmpegConfig, ?LoggerInterface $logger = null)
39
    {
40 16
        $this->ffmpegConfig  = $ffmpegConfig;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 2 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
41
42 16
        $this->logger = $logger ?? new NullLogger();
43 16
    }
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 14
    public function getSymfonyProcess(string $inputFile, $outputFile, VideoConvertParamsInterface $convertParams, ?ProcessParamsInterface $processParams = null): Process
59
    {
60 14
        $adapter = $this->ffmpegConfig->getAdapter();
61
62 14
        if (!$convertParams->hasParam(VideoConvertParamsInterface::PARAM_THREADS)
63 14
            && $adapter->getDefaultThreads() !== null) {
64 1
            $convertParams = $convertParams->withBuiltInParam(
65 1
                VideoConvertParamsInterface::PARAM_THREADS,
66 1
                $adapter->getDefaultThreads()
67
            );
68
        }
69
70 14
        $arguments = $adapter->getMappedConversionParams($convertParams);
71
72
        try {
73 14
            $ffmpegCmd = $adapter->getCliCommand($arguments, $inputFile, $outputFile);
74 1
        } catch (CommonException\InvalidArgumentException $e) {
75 1
            throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
76
        }
77
78 13
        $pp = $processParams ?? $this->ffmpegConfig->getProcessParams();
79
80 13
        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 MissingInputFileReaderException
93
     * @throws ProcessTimedOutException
94
     * @throws ProcessFailedException
95
     * @throws ProcessSignaledException
96
     * @throws InvalidParamReaderException
97
     * @throws RuntimeReaderException
98
     */
99 12
    public function convert(string $inputFile, $outputFile, VideoConvertParamsInterface $convertParams, ?callable $callback = null, ?ProcessParamsInterface $processParams = null): void
100
    {
101
        try {
102
            try {
103 12
                $this->ensureFileReadable($inputFile);
104 10
                $process = $this->getSymfonyProcess($inputFile, $outputFile, $convertParams, $processParams);
105 10
                $process->mustRun($callback);
106 9
            } catch (CommonException\FileNotFoundException | CommonException\FileNotReadableException $e) {
107 2
                throw new MissingInputFileReaderException($e->getMessage());
108 7
            } catch (CommonException\UnsupportedParamValueException | CommonException\UnsupportedParamException $e) {
109
                throw new InvalidParamReaderException($e->getMessage());
110 7
            } catch (SPException\ProcessTimedOutException $e) {
111 3
                throw new ProcessTimedOutException($e->getProcess(), $e);
112 4
            } catch (SPException\ProcessSignaledException $e) {
113
                throw new ProcessSignaledException($e->getProcess(), $e);
114 4
            } catch (SPException\ProcessFailedException $e) {
115 4
                throw new ProcessFailedException($e->getProcess(), $e);
116
            } catch (SPException\RuntimeException $e) {
117 3
                throw new RuntimeReaderException($e->getMessage());
118
            }
119 9
        } catch (\Throwable $e) {
120 9
            $exceptionNs = explode('\\', get_class($e));
121 9
            $this->logger->log(
122 9
                ($e instanceof MissingInputFileReaderException) ? LogLevel::WARNING : LogLevel::ERROR,
123 9
                sprintf(
124 9
                    'Video conversion failed \'%s\' with \'%s\'. "%s(%s, %s,...)"',
125 9
                    $exceptionNs[count($exceptionNs) - 1],
126 9
                    __METHOD__,
127 9
                    $e->getMessage(),
128 9
                    $inputFile,
129 9
                    $outputFile instanceof UnescapedFileInterface ? $outputFile->getFile() : $outputFile
130
                )
131
            );
132 9
            throw $e;
133
        }
134 3
    }
135
136
    /*
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...
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