Completed
Push — master ( 252985...6cc43c )
by Sébastien
02:56 queued 14s
created

VideoThumbGenerator::makeThumbnail()   C

Complexity

Conditions 14
Paths 37

Size

Total Lines 49
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 14.4467

Importance

Changes 0
Metric Value
eloc 39
dl 0
loc 49
ccs 33
cts 38
cp 0.8684
rs 6.2666
c 0
b 0
f 0
cc 14
nc 37
nop 5
crap 14.4467

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