Passed
Push — master ( 33603b...a275e1 )
by Sébastien
04:10
created

VideoThumbGenerator::makeThumbnail()   C

Complexity

Conditions 12
Paths 28

Size

Total Lines 44
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 12.2341

Importance

Changes 0
Metric Value
eloc 35
dl 0
loc 44
ccs 30
cts 34
cp 0.8824
rs 6.9666
c 0
b 0
f 0
cc 12
nc 28
nop 5
crap 12.2341

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