Passed
Push — master ( 89fbe5...c257f0 )
by Sébastien
02:18
created

VideoConverter::convert()   B

Complexity

Conditions 9
Paths 22

Size

Total Lines 34
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 9.0995

Importance

Changes 0
Metric Value
cc 9
eloc 29
nc 22
nop 5
dl 0
loc 34
ccs 25
cts 28
cp 0.8929
crap 9.0995
rs 8.0555
c 0
b 0
f 0
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\FileNotFoundException;
12
use Soluble\MediaTools\Common\Exception\UnsupportedParamException;
13
use Soluble\MediaTools\Common\Exception\UnsupportedParamValueException;
14
use Soluble\MediaTools\Common\Process\ProcessFactory;
15
use Soluble\MediaTools\Common\Process\ProcessParamsInterface;
16
use Soluble\MediaTools\Video\Config\FFMpegConfigInterface;
17
use Soluble\MediaTools\Video\Exception\ConversionExceptionInterface;
18
use Soluble\MediaTools\Video\Exception\ConversionProcessExceptionInterface;
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\RuntimeException;
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 12
    public function __construct(FFMpegConfigInterface $ffmpegConfig, ?LoggerInterface $logger = null)
39
    {
40 12
        $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 12
        $this->logger = $logger ?? new NullLogger();
43 12
    }
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
     * @see https://symfony.com/doc/current/components/process.html
51
     *
52
     * @throws UnsupportedParamException
53
     * @throws UnsupportedParamValueException
54
     */
55 10
    public function getSymfonyProcess(string $inputFile, string $outputFile, VideoConvertParamsInterface $convertParams, ?ProcessParamsInterface $processParams = null): Process
56
    {
57 10
        $adapter = $this->ffmpegConfig->getAdapter();
58
59 10
        if (!$convertParams->hasParam(VideoConvertParamsInterface::PARAM_THREADS)
60 10
            && $adapter->getDefaultThreads() !== null) {
61 1
            $convertParams = $convertParams->withBuiltInParam(
62 1
                VideoConvertParamsInterface::PARAM_THREADS,
63 1
                $adapter->getDefaultThreads()
64
            );
65
        }
66
67 10
        $arguments = $adapter->getMappedConversionParams($convertParams);
68 10
        $ffmpegCmd = $adapter->getCliCommand($arguments, $inputFile, $outputFile);
69
70 10
        $pp = $processParams ?? $this->ffmpegConfig->getProcessParams();
71
72 10
        return (new ProcessFactory($ffmpegCmd, $pp))();
73
    }
74
75
    /**
76
     * Run a conversion, throw exception on error.
77
     *
78
     * @param callable|null $callback A PHP callback to run whenever there is some
79
     *                                tmp available on STDOUT or STDERR
80
     *
81
     * @throws ConversionExceptionInterface        Base exception class for conversion exceptions
82
     * @throws ConversionProcessExceptionInterface Base exception class for process conversion exceptions
83
     * @throws MissingInputFileException
84
     * @throws ProcessTimedOutException
85
     * @throws ProcessFailedException
86
     * @throws ProcessSignaledException
87
     * @throws InvalidParamException
88
     * @throws RuntimeException
89
     */
90 10
    public function convert(string $inputFile, string $outputFile, VideoConvertParamsInterface $convertParams, ?callable $callback = null, ?ProcessParamsInterface $processParams = null): void
91
    {
92
        try {
93
            try {
94 10
                $this->ensureFileExists($inputFile);
95 8
                $process = $this->getSymfonyProcess($inputFile, $outputFile, $convertParams, $processParams);
96 8
                $process->mustRun($callback);
97 8
            } catch (FileNotFoundException $e) {
98 2
                throw new MissingInputFileException($e->getMessage());
99 6
            } catch (UnsupportedParamValueException | UnsupportedParamException $e) {
100
                throw new InvalidParamException($e->getMessage());
101 6
            } catch (SPException\ProcessTimedOutException $e) {
102 3
                throw new ProcessTimedOutException($e->getProcess(), $e);
103 3
            } catch (SPException\ProcessSignaledException $e) {
104
                throw new ProcessSignaledException($e->getProcess(), $e);
105 3
            } catch (SPException\ProcessFailedException $e) {
106 3
                throw new ProcessFailedException($e->getProcess(), $e);
107
            } catch (SPException\RuntimeException $e) {
108 2
                throw new RuntimeException($e->getMessage());
109
            }
110 8
        } catch (\Throwable $e) {
111 8
            $exceptionNs = explode('\\', get_class($e));
112 8
            $this->logger->log(
113 8
                ($e instanceof MissingInputFileException) ? LogLevel::WARNING : LogLevel::ERROR,
114 8
                sprintf(
115 8
                    'Video conversion failed \'%s\' with \'%s\'. "%s(%s, %s,...)"',
116 8
                    $exceptionNs[count($exceptionNs) - 1],
117 8
                    __METHOD__,
118 8
                    $e->getMessage(),
119 8
                    $inputFile,
120 8
                    $outputFile
121
                )
122
            );
123 8
            throw $e;
124
        }
125 2
    }
126
127
    /*
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...
128
     * FOR LATER REFERENCE !!!
129
    public function convertMultiPass(string $videoFile, string $outputFile, VideoConvertParams $convertParams, VideoFilterInterface $videoFilter=null): void {
130
131
        $this->ensureFileExists($videoFile);
132
        if ($videoFilter === null) {
133
            $videoFilter = new EmptyVideoFilter();
134
        }
135
136
137
        $threads = $convertParams->getOption(VideoConversionParams::OPTION_THREADS, $this->ffmpegConfig->getThreads());
138
139
        $ffmpegBin = $this->ffmpegConfig->getBinary();
140
141
        $commonArgs = array_merge([
142
                $ffmpegBin,
143
                sprintf('-i %s', escapeshellarg($videoFile)), // input filename
144
                $videoFilter->getFFMpegCliArgument(), // add -vf yadif,nlmeans
145
                ($threads === null) ? '' : sprintf('-threads %s', $threads),
146
        ], $convertParams->getFFMpegArguments());
147
148
        $pass1Cmd = implode(' ', array_merge(
149
            $commonArgs,
150
            [
151
                '-pass 1',
152
                // tells VP9 to encode really fast, sacrificing quality. Useful to speed up the first pass.
153
                '-speed 4',
154
                '-y /dev/null',
155
            ]
156
        ));
157
158
        $pass2Cmd = implode( ' ', array_merge(
159
            $commonArgs,
160
            [
161
                '-pass 2',
162
                // speed 1 is a good speed vs. quality compromise.
163
                // Produces tmp quality typically very close to speed 0, but usually encodes much faster.
164
                '-speed 1',
165
                '-y',
166
                sprintf("%s", escapeshellarg($outputFile))
167
            ]
168
        ));
169
170
171
        $process = new Process($pass1Cmd);
172
        $process->setTimeout(null);
173
        $process->setIdleTimeout(60); // 60 seconds without tmp will stop the process
174
        $process->start();
175
        foreach ($process as $type => $data) {
176
            if ($process::OUT === $type) {
177
                echo "\nRead from stdout: ".$data;
178
            } else { // $process::ERR === $type
179
                echo "\nRead from stderr: ".$data;
180
            }
181
        }
182
183
        $process = new Process($pass2Cmd);
184
        $process->setTimeout(null);
185
        $process->setIdleTimeout(60); // 60 seconds without tmp will stop the process
186
        $process->start();
187
        foreach ($process as $type => $data) {
188
            if ($process::OUT === $type) {
189
                echo "\nRead from stdout: ".$data;
190
            } else { // $process::ERR === $type
191
                echo "\nRead from stderr: ".$data;
192
            }
193
        }
194
195
    }
196
    */
197
}
198