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

VideoThumbGenerator::makeThumbnail()   B

Complexity

Conditions 9
Paths 22

Size

Total Lines 35
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 35
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\MissingTimeException;
22
use Soluble\MediaTools\Video\Exception\ProcessFailedException;
23
use Soluble\MediaTools\Video\Exception\ProcessSignaledException;
24
use Soluble\MediaTools\Video\Exception\ProcessTimedOutException;
25
use Soluble\MediaTools\Video\Exception\RuntimeException;
26
use Symfony\Component\Process\Exception as SPException;
27
use Symfony\Component\Process\Process;
28
29
class VideoThumbGenerator implements VideoThumbGeneratorInterface
30
{
31
    public const DEFAULT_QUALITY_SCALE = 2;
32
33
    use PathAssertionsTrait;
34
35
    /** @var FFMpegConfigInterface */
36
    protected $ffmpegConfig;
37
38
    /** @var int */
39
    protected $defaultQualityScale;
40
41
    /** @var LoggerInterface|NullLogger */
42
    protected $logger;
43
44 7
    public function __construct(FFMpegConfigInterface $ffmpegConfig, ?LoggerInterface $logger = null, int $defaultQualityScale = self::DEFAULT_QUALITY_SCALE)
45
    {
46 7
        $this->ffmpegConfig        = $ffmpegConfig;
47 7
        $this->defaultQualityScale = $defaultQualityScale;
48 7
        $this->logger              = $logger ?? new NullLogger();
49 7
    }
50
51
    /**
52
     * Return ready-to-run symfony process object that you can use
53
     * to `run()` or `start()` programmatically. Useful if you want
54
     * handle the process your way...
55
     *
56
     * @see https://symfony.com/doc/current/components/process.html
57
     *
58
     * @throws UnsupportedParamException
59
     * @throws UnsupportedParamValueException
60
     * @throws MissingTimeException
61
     */
62 4
    public function getSymfonyProcess(string $videoFile, string $thumbnailFile, VideoThumbParamsInterface $thumbParams, ?ProcessParamsInterface $processParams = null): Process
63
    {
64 4
        $adapter = $this->ffmpegConfig->getAdapter();
65
66 4
        if (!$thumbParams->hasParam(VideoThumbParamsInterface::PARAM_SEEK_TIME)) {
67
            throw new MissingTimeException('Missing seekTime parameter');
68
        }
69
70 4
        $conversionParams = (new VideoConvertParams());
71
72 4
        if ($adapter->getDefaultThreads() !== null) {
73
            $conversionParams->withThreads($adapter->getDefaultThreads());
74
        }
75
76
        // TIME must be the first !!!
77
78 4
        $conversionParams = $conversionParams->withSeekStart(
79 4
            $thumbParams->getParam(VideoThumbParamsInterface::PARAM_SEEK_TIME)
80
        );
81
82
        // Only one frame
83 4
        $conversionParams = $conversionParams->withVideoFrames(1);
84
85 4
        if ($thumbParams->hasParam(VideoThumbParamsInterface::PARAM_VIDEO_FILTER)) {
86 2
            $conversionParams = $conversionParams->withVideoFilter(
87 2
                $thumbParams->getParam(VideoThumbParamsInterface::PARAM_VIDEO_FILTER)
88
            );
89
        }
90
91 4
        if ($thumbParams->hasParam(VideoThumbParamsInterface::PARAM_QUALITY_SCALE)) {
92
            $conversionParams = $conversionParams->withVideoQualityScale(
93
                $thumbParams->getParam(VideoThumbParamsInterface::PARAM_QUALITY_SCALE)
94
            );
95
        } else {
96 4
            $conversionParams = $conversionParams->withVideoQualityScale(
97 4
                $this->defaultQualityScale
98
            );
99
        }
100
101 4
        $arguments = $adapter->getMappedConversionParams($conversionParams);
102 4
        $ffmpegCmd = $adapter->getCliCommand($arguments, $videoFile, $thumbnailFile);
103
104 4
        $pp = $processParams ?? $this->ffmpegConfig->getProcessParams();
105
106 4
        return (new ProcessFactory($ffmpegCmd, $pp))();
107
    }
108
109
    /**
110
     * @throws ConversionExceptionInterface        Base exception class for conversion exceptions
111
     * @throws ConversionProcessExceptionInterface Base exception class for process conversion exceptions
112
     * @throws MissingInputFileException
113
     * @throws MissingTimeException
114
     * @throws ProcessTimedOutException
115
     * @throws ProcessFailedException
116
     * @throws ProcessSignaledException
117
     * @throws RuntimeException
118
     * @throws InvalidParamException
119
     */
120 7
    public function makeThumbnail(string $videoFile, string $thumbnailFile, VideoThumbParamsInterface $thumbParams, ?callable $callback = null, ?ProcessParamsInterface $processParams = null): void
121
    {
122
        try {
123
            try {
124 7
                $this->ensureFileExists($videoFile);
125
126 4
                $process = $this->getSymfonyProcess($videoFile, $thumbnailFile, $thumbParams, $processParams);
127 4
                $process->mustRun($callback);
128 5
            } catch (FileNotFoundException $e) {
129 3
                throw new MissingInputFileException($e->getMessage());
130 2
            } catch (UnsupportedParamValueException | UnsupportedParamException $e) {
131
                throw new InvalidParamException($e->getMessage());
132 2
            } catch (SPException\ProcessTimedOutException $e) {
133 1
                throw new ProcessTimedOutException($e->getProcess(), $e);
134 1
            } catch (SPException\ProcessSignaledException $e) {
135
                throw new ProcessSignaledException($e->getProcess(), $e);
136 1
            } catch (SPException\ProcessFailedException $e) {
137 1
                throw new ProcessFailedException($e->getProcess(), $e);
138
            } catch (SPException\RuntimeException $e) {
139 2
                throw new RuntimeException($e->getMessage());
140
            }
141 5
        } catch (\Throwable $e) {
142 5
            $exceptionNs = explode('\\', get_class($e));
143 5
            $this->logger->log(
144 5
                ($e instanceof MissingInputFileException) ? LogLevel::WARNING : LogLevel::ERROR,
145 5
                sprintf(
146 5
                    'Video thumbnailing failed \'%s\' with \'%s\'. "%s(%s, %s,...)"',
147 5
                    $exceptionNs[count($exceptionNs) - 1],
148 5
                    __METHOD__,
149 5
                    $e->getMessage(),
150 5
                    $videoFile,
151 5
                    $thumbnailFile
152
                )
153
            );
154 5
            throw $e;
155
        }
156 2
    }
157
}
158