FFMpegAdapter::getCliCommand()   A
last analyzed

Complexity

Conditions 5
Paths 9

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5.025

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 19
c 2
b 1
f 0
dl 0
loc 33
ccs 18
cts 20
cp 0.9
rs 9.3222
cc 5
nc 9
nop 4
crap 5.025
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\Adapter;
13
14
use Soluble\MediaTools\Common\Exception\InvalidArgumentException;
15
use Soluble\MediaTools\Common\Exception\UnsupportedParamException;
16
use Soluble\MediaTools\Common\Exception\UnsupportedParamValueException;
17
use Soluble\MediaTools\Common\IO\UnescapedFileInterface;
18
use Soluble\MediaTools\Video\Adapter\Validator\FFMpegParamValidator;
19
use Soluble\MediaTools\Video\Config\FFMpegConfigInterface;
20
use Soluble\MediaTools\Video\Exception\ParamValidationException;
21
use Soluble\MediaTools\Video\Exception\UnexpectedValueException;
22
use Soluble\MediaTools\Video\VideoConvertParamsInterface;
23
24
final class FFMpegAdapter implements ConverterAdapterInterface
25
{
26
    /** @var FFMpegConfigInterface */
27
    private $ffmpegConfig;
28
29 45
    public function __construct(FFMpegConfigInterface $ffmpegConfig)
30
    {
31 45
        $this->ffmpegConfig = $ffmpegConfig;
32 45
    }
33
34
    /**
35
     * @return array<string, array<string, string>>
36
     */
37 42
    public function getParamsOptions(): array
38
    {
39
        return [
40 42
            VideoConvertParamsInterface::PARAM_OUTPUT_FORMAT => [
41 42
                'pattern' => '-f %s',
42
            ],
43 42
            VideoConvertParamsInterface::PARAM_VIDEO_CODEC => [
44
                'pattern' => '-c:v %s',
45
            ],
46 42
            VideoConvertParamsInterface::PARAM_VIDEO_BITRATE => [
47
                'pattern' => '-b:v %s',
48
            ],
49 42
            VideoConvertParamsInterface::PARAM_VIDEO_MIN_BITRATE => [
50
                'pattern' => '-minrate %s',
51
            ],
52 42
            VideoConvertParamsInterface::PARAM_VIDEO_MAX_BITRATE => [
53
                'pattern' => '-maxrate %s',
54
            ],
55 42
            VideoConvertParamsInterface::PARAM_AUDIO_CODEC => [
56
                'pattern' => '-c:a %s',
57
            ],
58 42
            VideoConvertParamsInterface::PARAM_AUDIO_BITRATE => [
59
                'pattern' => '-b:a %s',
60
            ],
61 42
            VideoConvertParamsInterface::PARAM_PIX_FMT => [
62
                'pattern' => '-pix_fmt %s',
63
            ],
64 42
            VideoConvertParamsInterface::PARAM_PRESET => [
65
                'pattern' => '-preset %s',
66
            ],
67 42
            VideoConvertParamsInterface::PARAM_SPEED => [
68
                'pattern' => '-speed %d',
69
            ],
70 42
            VideoConvertParamsInterface::PARAM_THREADS => [
71
                'pattern' => '-threads %d',
72
            ],
73 42
            VideoConvertParamsInterface::PARAM_KEYFRAME_SPACING => [
74
                'pattern' => '-g %d',
75
            ],
76 42
            VideoConvertParamsInterface::PARAM_QUALITY => [
77
                'pattern' => '-quality %s',
78
            ],
79 42
            VideoConvertParamsInterface::PARAM_VIDEO_QUALITY_SCALE => [
80
                'pattern' => '-qscale:v %d',
81
            ],
82 42
            VideoConvertParamsInterface::PARAM_CRF => [
83
                'pattern' => '-crf %d',
84
            ],
85 42
            VideoConvertParamsInterface::PARAM_STREAMABLE => [
86
                'pattern' => '-movflags +faststart',
87
            ],
88 42
            VideoConvertParamsInterface::PARAM_FRAME_PARALLEL => [
89
                'pattern' => '-frame-parallel %s',
90
            ],
91 42
            VideoConvertParamsInterface::PARAM_TILE_COLUMNS => [
92
                'pattern' => '-tile-columns %s',
93
            ],
94 42
            VideoConvertParamsInterface::PARAM_TUNE => [
95
                'pattern' => '-tune %s',
96
            ],
97 42
            VideoConvertParamsInterface::PARAM_VIDEO_FILTER => [
98
                'pattern' => '-filter:v %s',
99
            ],
100 42
            VideoConvertParamsInterface::PARAM_OVERWRITE => [
101
                'pattern' => '-y',
102
            ],
103 42
            VideoConvertParamsInterface::PARAM_VIDEO_FRAMES => [
104
                'pattern' => '-frames:v %d',
105
            ],
106 42
            VideoConvertParamsInterface::PARAM_NOAUDIO => [
107
                'pattern' => '-an',
108
            ],
109 42
            VideoConvertParamsInterface::PARAM_SEEK_START => [
110
                'pattern' => '-ss %s',
111
            ],
112 42
            VideoConvertParamsInterface::PARAM_SEEK_END => [
113
                'pattern' => '-to %s',
114
            ],
115 42
            VideoConvertParamsInterface::PARAM_PASSLOGFILE => [
116
                'pattern' => '-passlogfile %s',
117
            ],
118 42
            VideoConvertParamsInterface::PARAM_PASS => [
119
                'pattern' => '-pass %s',
120
            ],
121 42
            VideoConvertParamsInterface::PARAM_AUTO_ALT_REF => [
122
                'pattern' => '-auto-alt-ref %s',
123
            ],
124 42
            VideoConvertParamsInterface::PARAM_LAG_IN_FRAMES => [
125
                'pattern' => '-lag-in-frames %s',
126
            ],
127
        ];
128
    }
129
130
    /**
131
     * Return an array version of params suitable for ffmpeg cli.
132
     *
133
     * @param bool $validateParams whether to run ffmpeg validation process validation
134
     *
135
     * @return array<string, string>
136
     *
137
     * @throws UnsupportedParamException
138
     * @throws UnsupportedParamValueException
139
     * @throws ParamValidationException
140
     */
141 42
    public function getMappedConversionParams(VideoConvertParamsInterface $conversionParams, bool $validateParams = true): array
142
    {
143 42
        $args             = [];
144 42
        $supportedOptions = $this->getParamsOptions();
145
146
        // Add default overwrite option if not set
147 42
        $overwriteParam = VideoConvertParamsInterface::PARAM_OVERWRITE;
148 42
        if (!$conversionParams->hasParam($overwriteParam)) {
149 35
            $conversionParams = $conversionParams->withBuiltInParam(
150 35
                $overwriteParam,
151 35
                true
152
            );
153
        }
154
155 42
        foreach ($conversionParams->toArray() as $paramName => $value) {
156 42
            if (!array_key_exists($paramName, $supportedOptions)) {
157
                throw new UnsupportedParamException(
158
                    sprintf(
159
                        'FFMpegAdapter does not support param \'%s\'',
160
                        $paramName
161
                    )
162
                );
163
            }
164 42
            $pattern = $supportedOptions[$paramName]['pattern'];
165 42
            if (is_bool($value)) {
166 41
                $args[$paramName] = $value ? $pattern : '';
167 39
            } elseif ($value instanceof FFMpegCLIValueInterface) {
168
                // Will test also FFMpegVideoFilterInterface
169 26
                $cliValue = $value->getFFmpegCLIValue();
170 26
                if ($cliValue !== null) {
171 26
                    $args[$paramName] = sprintf($pattern, $cliValue);
172
                }
173 36
            } elseif (is_string($value) || is_int($value)) {
174 35
                $args[$paramName] = sprintf($pattern, $value);
175
            } else {
176 1
                throw new UnsupportedParamValueException(
177 1
                    sprintf(
178 1
                        'Param \'%s\' has an unsupported type: \'%s\'',
179
                        $paramName,
180 1
                        is_object($value) ? get_class($value) : gettype($value)
181
                    )
182
                );
183
            }
184
        }
185
186
        // Validation
187 41
        if ($validateParams) {
188 41
            (new FFMpegParamValidator($conversionParams))->validate();
189
        }
190
191 40
        return $args;
192
    }
193
194
    /**
195
     * @param array<string,string>               $arguments        args that will be added
196
     * @param null|string|UnescapedFileInterface $outputFile
197
     * @param array<string,string>               $prependArguments args that must be added at the beginning of the command
198
     *
199
     * @throws InvalidArgumentException
200
     *
201
     * @return array<int|string, string>
202
     */
203 35
    public function getCliCommand(array $arguments, string $inputFile, $outputFile = null, array $prependArguments = []): array
204
    {
205 35
        $outputArg = null;
206 35
        if ($outputFile instanceof UnescapedFileInterface) {
207 6
            $outputArg = $outputFile->getFile();
208 31
        } elseif (is_string($outputFile)) {
209 29
            $outputArg = $outputFile;
210
        }
211
212 35
        if ($outputArg === null) {
213 2
            throw new InvalidArgumentException(sprintf(
214 2
                'Output file must be either a non empty string, null or PlatformNullFile (type %s)',
215 2
                gettype($outputFile)
216
            ));
217
        }
218
219 33
        $ffmpegCmd = array_merge(
220
            [
221 33
                $this->ffmpegConfig->getBinary(),
222
            ],
223 33
            $this->getArgsWithExplodedValues($prependArguments),
224 33
            ['-i', $inputFile],
225 33
            $this->getArgsWithExplodedValues($arguments),
226 33
            [$outputArg]
227
        );
228
229 33
        if (count($ffmpegCmd) < 2) {
230
            throw new UnexpectedValueException(
231
                'Cannot generate ffmpeg cli command'
232
            );
233
        }
234
235 33
        return $ffmpegCmd;
236
    }
237
238
    /**
239
     * As we rely on symfony process unescaping, we
240
     * need to explode options name and values... i.e
241
     * ['-tune animation'] will become ['-tune', 'animation'].
242
     *
243
     * @param array<string, string> $args
244
     *
245
     * @return string[]
246
     */
247 33
    private function getArgsWithExplodedValues(array $args): array
248
    {
249 33
        $exploded = [];
250 33
        foreach ($args as $key => $value) {
251 33
            $elems      = explode(' ', $value);
252 33
            $exploded[] = array_shift($elems);
253 33
            if (count($elems) <= 0) {
254 33
                continue;
255
            }
256 32
            $exploded[] = implode(' ', $elems);
257
        }
258
259 33
        return $exploded;
260
    }
261
262 27
    public function getDefaultThreads(): ?int
263
    {
264 27
        return $this->ffmpegConfig->getThreads();
265
    }
266
}
267