Failed Conditions
Push — master ( 16370c...4fb215 )
by Sébastien
02:43
created

VideoThumbGenerator::getSymfonyProcess()   B

Complexity

Conditions 8
Paths 17

Size

Total Lines 56
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 8.1678

Importance

Changes 0
Metric Value
eloc 30
dl 0
loc 56
ccs 25
cts 29
cp 0.8621
rs 8.1954
c 0
b 0
f 0
cc 8
nc 17
nop 4
crap 8.1678

How to fix   Long Method   

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\FileNotFoundException;
12
use Soluble\MediaTools\Common\Exception\FileNotReadableException;
13
use Soluble\MediaTools\Common\Exception\UnsupportedParamException;
14
use Soluble\MediaTools\Common\Exception\UnsupportedParamValueException;
15
use Soluble\MediaTools\Common\Process\ProcessFactory;
16
use Soluble\MediaTools\Common\Process\ProcessParamsInterface;
17
use Soluble\MediaTools\Video\Config\FFMpegConfigInterface;
18
use Soluble\MediaTools\Video\Exception\ConverterExceptionInterface;
19
use Soluble\MediaTools\Video\Exception\ConverterProcessExceptionInterface;
20
use Soluble\MediaTools\Video\Exception\InvalidParamException;
21
use Soluble\MediaTools\Video\Exception\MissingInputFileException;
22
use Soluble\MediaTools\Video\Exception\MissingTimeException;
23
use Soluble\MediaTools\Video\Exception\NoOutputGeneratedException;
24
use Soluble\MediaTools\Video\Exception\ProcessFailedException;
25
use Soluble\MediaTools\Video\Exception\ProcessSignaledException;
26
use Soluble\MediaTools\Video\Exception\ProcessTimedOutException;
27
use Soluble\MediaTools\Video\Exception\RuntimeReaderException;
28
use Soluble\MediaTools\Video\Filter\SelectFilter;
29
use Soluble\MediaTools\Video\Filter\VideoFilterChain;
30
use Symfony\Component\Process\Exception as SPException;
31
use Symfony\Component\Process\Process;
32
33
class VideoThumbGenerator implements VideoThumbGeneratorInterface
34
{
35
    public const DEFAULT_QUALITY_SCALE = 2;
36
37
    use PathAssertionsTrait;
38
39
    /** @var FFMpegConfigInterface */
40
    protected $ffmpegConfig;
41
42
    /** @var int */
43
    protected $defaultQualityScale;
44
45
    /** @var LoggerInterface|NullLogger */
46
    protected $logger;
47
48 9
    public function __construct(FFMpegConfigInterface $ffmpegConfig, ?LoggerInterface $logger = null, int $defaultQualityScale = self::DEFAULT_QUALITY_SCALE)
49
    {
50 9
        $this->ffmpegConfig        = $ffmpegConfig;
51 9
        $this->defaultQualityScale = $defaultQualityScale;
52 9
        $this->logger              = $logger ?? new NullLogger();
53 9
    }
54
55
    /**
56
     * Return ready-to-run symfony process object that you can use
57
     * to `run()` or `start()` programmatically. Useful if you want
58
     * handle the process your way...
59
     *
60
     * @see https://symfony.com/doc/current/components/process.html
61
     *
62
     * @throws UnsupportedParamException
63
     * @throws UnsupportedParamValueException
64
     * @throws MissingTimeException
65
     */
66 6
    public function getSymfonyProcess(string $videoFile, string $thumbnailFile, VideoThumbParamsInterface $thumbParams, ?ProcessParamsInterface $processParams = null): Process
67
    {
68 6
        $adapter = $this->ffmpegConfig->getAdapter();
69
70 6
        $conversionParams = (new VideoConvertParams());
71
72 6
        if (!$thumbParams->hasParam(VideoThumbParamsInterface::PARAM_SEEK_TIME)
73 6
         && !$thumbParams->hasParam(VideoThumbParamsInterface::PARAM_WITH_FRAME)) {
74
            throw new MissingTimeException('Missing seekTime/time or frame selection parameter');
75
        }
76
77 6
        if ($thumbParams->hasParam(VideoThumbParamsInterface::PARAM_SEEK_TIME)) {
78
            // TIME params are separated from the rest, so we can inject them
79
            // before input file
80 5
            $timeParams = (new VideoConvertParams())->withSeekStart(
81 5
                $thumbParams->getParam(VideoThumbParamsInterface::PARAM_SEEK_TIME)
82
            );
83
        } else {
84 1
            $timeParams = null;
85
        }
86
87 6
        if ($adapter->getDefaultThreads() !== null) {
88
            $conversionParams = $conversionParams->withThreads($adapter->getDefaultThreads());
89
        }
90
91
        // Only one frame for thumbnails :)
92 6
        $conversionParams = $conversionParams->withVideoFrames(1);
93
94 6
        $videoFilters = $this->getThumbFilters($thumbParams);
95
96 6
        if (count($videoFilters->getFilters()) > 0) {
97 3
            $conversionParams = $conversionParams->withVideoFilter($videoFilters);
98
        }
99
100 6
        if ($thumbParams->hasParam(VideoThumbParamsInterface::PARAM_QUALITY_SCALE)) {
101
            $conversionParams = $conversionParams->withVideoQualityScale(
102
                $thumbParams->getParam(VideoThumbParamsInterface::PARAM_QUALITY_SCALE)
103
            );
104
        } else {
105 6
            $conversionParams = $conversionParams->withVideoQualityScale(
106 6
                $this->defaultQualityScale
107
            );
108
        }
109
110 6
        $arguments = $adapter->getMappedConversionParams($conversionParams);
111
112 6
        $ffmpegCmd = $adapter->getCliCommand(
113 6
            $arguments,
114 6
            $videoFile,
115 6
            $thumbnailFile,
116 6
            $timeParams !== null ? $adapter->getMappedConversionParams($timeParams) : []
117
        );
118
119 6
        $pp = $processParams ?? $this->ffmpegConfig->getProcessParams();
120
121 6
        return (new ProcessFactory($ffmpegCmd, $pp))();
122
    }
123
124 6
    private function getThumbFilters(VideoThumbParamsInterface $thumbParams): VideoFilterChain
125
    {
126 6
        $videoFilters = new VideoFilterChain();
127
128
        // Let's choose a frame
129 6
        if ($thumbParams->hasParam(VideoThumbParamsInterface::PARAM_WITH_FRAME)) {
130 1
            $frame      = $thumbParams->getParam(VideoThumbParamsInterface::PARAM_WITH_FRAME);
131 1
            $expression = sprintf('eq(n\,%d)', max(0, $frame - 1));
132 1
            $videoFilters->addFilter(new SelectFilter($expression));
133
        }
134
135
        // Let's add the remaning filters
136 6
        if ($thumbParams->hasParam(VideoThumbParamsInterface::PARAM_VIDEO_FILTER)) {
137 2
            $videoFilters->addFilter(
138 2
                $thumbParams->getParam(VideoThumbParamsInterface::PARAM_VIDEO_FILTER)
139
            );
140
        }
141
142 6
        return $videoFilters;
143
    }
144
145
    /**
146
     * @throws ConverterExceptionInterface        Base exception class for conversion exceptions
147
     * @throws ConverterProcessExceptionInterface Base exception class for process conversion exceptions
148
     * @throws MissingInputFileException
149
     * @throws MissingTimeException
150
     * @throws ProcessTimedOutException
151
     * @throws ProcessFailedException
152
     * @throws ProcessSignaledException
153
     * @throws RuntimeReaderException
154
     * @throws InvalidParamException
155
     * @throws NoOutputGeneratedException
156
     */
157 9
    public function makeThumbnail(string $videoFile, string $thumbnailFile, VideoThumbParamsInterface $thumbParams, ?callable $callback = null, ?ProcessParamsInterface $processParams = null): void
158
    {
159
        try {
160
            try {
161 9
                $this->ensureFileReadable($videoFile);
162
163 6
                $process = $this->getSymfonyProcess($videoFile, $thumbnailFile, $thumbParams, $processParams);
164 6
                $process->mustRun($callback);
165 5
            } catch (FileNotFoundException | FileNotReadableException $e) {
166 3
                throw new MissingInputFileException($e->getMessage());
167 2
            } catch (UnsupportedParamValueException | UnsupportedParamException $e) {
168
                throw new InvalidParamException($e->getMessage());
169 2
            } catch (SPException\ProcessTimedOutException $e) {
170 1
                throw new ProcessTimedOutException($e->getProcess(), $e);
171 1
            } catch (SPException\ProcessSignaledException $e) {
172
                throw new ProcessSignaledException($e->getProcess(), $e);
173 1
            } catch (SPException\ProcessFailedException $e) {
174 1
                throw new ProcessFailedException($e->getProcess(), $e);
175
            } catch (SPException\RuntimeException $e) {
176
                throw new RuntimeReaderException($e->getMessage());
177
            }
178
179 4
            if (!file_exists($thumbnailFile) || filesize($thumbnailFile) === 0) {
180 1
                $stdErr        = array_filter(explode("\n", trim($process->getErrorOutput())));
181 1
                $lastErrorLine = count($stdErr) > 0 ? $stdErr[count($stdErr) - 1] : 'no error message';
182 1
                throw new NoOutputGeneratedException(sprintf(
183 1
                    'Thumbnail was not generated, probably an invalid time/frame selection (ffmpeg: %s)',
184 4
                    $lastErrorLine
185
                ));
186
            }
187 6
        } catch (\Throwable $e) {
188 6
            $exceptionNs = explode('\\', get_class($e));
189 6
            $this->logger->log(
190 6
                ($e instanceof MissingInputFileException) ? LogLevel::WARNING : LogLevel::ERROR,
191 6
                sprintf(
192 6
                    'Video thumbnailing failed \'%s\' with \'%s\'. "%s(%s, %s,...)"',
193 6
                    $exceptionNs[count($exceptionNs) - 1],
194 6
                    __METHOD__,
195 6
                    $e->getMessage(),
196 6
                    $videoFile,
197 6
                    $thumbnailFile
198
                )
199
            );
200 6
            throw $e;
201
        }
202 3
    }
203
}
204