VideoConverter   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 110
Duplicated Lines 0 %

Test Coverage

Coverage 93.62%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 17
eloc 51
c 3
b 1
f 0
dl 0
loc 110
ccs 44
cts 47
cp 0.9362
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
A getSymfonyProcess() 0 23 4
A __construct() 0 5 1
C convert() 0 38 12
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-2020 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 as CommonException;
19
use Soluble\MediaTools\Common\IO\UnescapedFileInterface;
20
use Soluble\MediaTools\Common\Process\ProcessFactory;
21
use Soluble\MediaTools\Common\Process\ProcessParamsInterface;
22
use Soluble\MediaTools\Video\Config\FFMpegConfigInterface;
23
use Soluble\MediaTools\Video\Exception\ConverterExceptionInterface;
24
use Soluble\MediaTools\Video\Exception\ConverterProcessExceptionInterface;
25
use Soluble\MediaTools\Video\Exception\InvalidArgumentException;
26
use Soluble\MediaTools\Video\Exception\InvalidParamException;
27
use Soluble\MediaTools\Video\Exception\MissingFFMpegBinaryException;
28
use Soluble\MediaTools\Video\Exception\MissingInputFileException;
29
use Soluble\MediaTools\Video\Exception\ProcessFailedException;
30
use Soluble\MediaTools\Video\Exception\ProcessSignaledException;
31
use Soluble\MediaTools\Video\Exception\ProcessTimedOutException;
32
use Soluble\MediaTools\Video\Exception\RuntimeReaderException;
33
use Symfony\Component\Process\Exception as SPException;
34
use Symfony\Component\Process\Process;
35
36
final class VideoConverter implements VideoConverterInterface
37
{
38
    use PathAssertionsTrait;
39
40
    /** @var FFMpegConfigInterface */
41
    private $ffmpegConfig;
42
43
    /** @var LoggerInterface */
44
    private $logger;
45
46 22
    public function __construct(FFMpegConfigInterface $ffmpegConfig, ?LoggerInterface $logger = null)
47
    {
48 22
        $this->ffmpegConfig = $ffmpegConfig;
49
50 22
        $this->logger = $logger ?? new NullLogger();
51 22
    }
52
53
    /**
54
     * Return ready-to-run symfony process object that you can use
55
     * to `run()` or `start()` programmatically. Useful if you want to make
56
     * things async...
57
     *
58
     * @param null|string|UnescapedFileInterface $outputFile
59
     *
60
     * @see https://symfony.com/doc/current/components/process.html
61
     *
62
     * @throws CommonException\UnsupportedParamException
63
     * @throws CommonException\UnsupportedParamValueException
64
     * @throws InvalidArgumentException
65
     */
66 19
    public function getSymfonyProcess(string $inputFile, $outputFile, VideoConvertParamsInterface $convertParams, ?ProcessParamsInterface $processParams = null): Process
67
    {
68 19
        $adapter = $this->ffmpegConfig->getAdapter();
69
70 19
        if (!$convertParams->hasParam(VideoConvertParamsInterface::PARAM_THREADS)
71 19
            && $adapter->getDefaultThreads() !== null) {
72 1
            $convertParams = $convertParams->withBuiltInParam(
73 1
                VideoConvertParamsInterface::PARAM_THREADS,
74 1
                $adapter->getDefaultThreads()
75
            );
76
        }
77
78 19
        $arguments = $adapter->getMappedConversionParams($convertParams);
79
80
        try {
81 19
            $ffmpegCmd = $adapter->getCliCommand($arguments, $inputFile, $outputFile);
82 1
        } catch (CommonException\InvalidArgumentException $e) {
83 1
            throw new InvalidArgumentException($e->getMessage(), (int) $e->getCode(), $e);
84
        }
85
86 18
        $pp = $processParams ?? $this->ffmpegConfig->getProcessParams();
87
88 18
        return (new ProcessFactory($ffmpegCmd, $pp))();
89
    }
90
91
    /**
92
     * Run a conversion, throw exception on error.
93
     *
94
     * @param null|string|UnescapedFileInterface $outputFile
95
     * @param callable|null                      $callback   A PHP callback to run whenever there is some
96
     *                                                       tmp available on STDOUT or STDERR
97
     *
98
     * @throws ConverterExceptionInterface        Base exception class for conversion exceptions
99
     * @throws ConverterProcessExceptionInterface Base exception class for process conversion exceptions
100
     * @throws MissingInputFileException
101
     * @throws MissingFFMpegBinaryException
102
     * @throws ProcessTimedOutException
103
     * @throws ProcessFailedException
104
     * @throws ProcessSignaledException
105
     * @throws InvalidParamException
106
     * @throws RuntimeReaderException
107
     */
108 17
    public function convert(string $inputFile, $outputFile, VideoConvertParamsInterface $convertParams, ?callable $callback = null, ?ProcessParamsInterface $processParams = null): void
109
    {
110
        try {
111
            try {
112 17
                $this->ensureFileReadable($inputFile, true);
113 15
                $process = $this->getSymfonyProcess($inputFile, $outputFile, $convertParams, $processParams);
114 15
                $process->mustRun($callback);
115 11
            } catch (CommonException\FileNotFoundException | CommonException\FileNotReadableException | CommonException\FileEmptyException $e) {
116 2
                throw new MissingInputFileException($e->getMessage());
117 9
            } catch (CommonException\UnsupportedParamValueException | CommonException\UnsupportedParamException $e) {
118
                throw new InvalidParamException($e->getMessage());
119 9
            } catch (SPException\ProcessSignaledException $e) {
120
                throw new ProcessSignaledException($e->getProcess(), $e);
121 9
            } catch (SPException\ProcessTimedOutException $e) {
122 3
                throw new ProcessTimedOutException($e->getProcess(), $e);
123 6
            } catch (SPException\ProcessFailedException $e) {
124 6
                $process = $e->getProcess();
125 6
                if ($process->getExitCode() === 127 ||
126 6
                    mb_strpos(mb_strtolower($process->getExitCodeText()), 'command not found') !== false) {
127 1
                    throw new MissingFFMpegBinaryException($process, $e);
128
                }
129 5
                throw new ProcessFailedException($process, $e);
130
            } catch (SPException\RuntimeException $e) {
131 7
                throw new RuntimeReaderException($e->getMessage());
132
            }
133 11
        } catch (\Throwable $e) {
134 11
            $exceptionNs = explode('\\', get_class($e));
135 11
            $this->logger->log(
136 11
                ($e instanceof MissingInputFileException) ? LogLevel::WARNING : LogLevel::ERROR,
137 11
                sprintf(
138 11
                    'VideoConverter %s: \'%s\' to \'%s\'. (%s)',
139 11
                    $exceptionNs[count($exceptionNs) - 1],
140
                    $inputFile,
141 11
                    $outputFile instanceof UnescapedFileInterface ? $outputFile->getFile() : $outputFile,
142 11
                    $e->getMessage()
143
                )
144
            );
145 11
            throw $e;
146
        }
147 7
    }
148
}
149