Passed
Push — master ( 4744e0...bd9fc8 )
by Sébastien
02:34
created

VideoConvertParams   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 349
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 33
dl 0
loc 349
ccs 92
cts 92
cp 1
rs 9.76
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A withCrf() 0 4 1
A __construct() 0 4 1
A withVideoMinBitrate() 0 6 1
A withVideoMaxBitrate() 0 6 1
A hasOption() 0 3 1
A withOutputFormat() 0 4 1
A ensureSupportedOptions() 0 6 3
A ensureValidBitRateUnit() 0 4 2
A withPixFmt() 0 4 1
A withQuality() 0 4 1
A getOptions() 0 3 1
A withTune() 0 4 1
A withVideoBitrate() 0 6 1
A getOption() 0 3 1
A isOptionValid() 0 3 1
A getFFMpegArguments() 0 15 4
A withStreamable() 0 4 1
A withAudioBitrate() 0 6 1
A withThreads() 0 4 1
A withAudioCodec() 0 4 1
A withKeyframeSpacing() 0 4 1
A withFrameParallel() 0 4 1
A withVideoCodec() 0 4 1
A withSpeed() 0 4 1
A withTileColumns() 0 4 1
A withPreset() 0 4 1
A withVideoFilter() 0 4 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Soluble\MediaTools;
6
7
use Soluble\MediaTools\Exception\InvalidArgumentException;
8
use Soluble\MediaTools\Filter\Video\VideoFilterInterface;
9
10
class VideoConvertParams
11
{
12
    public const OPTION_VIDEO_CODEC       = 'VIDEO_CODEC';
13
    public const OPTION_VIDEO_BITRATE     = 'VIDEO_BITRATE';
14
    public const OPTION_VIDEO_MIN_BITRATE = 'VIDEO_MIN_BITRATE';
15
    public const OPTION_VIDEO_MAX_BITRATE = 'VIDEO_MAX_BITRATE';
16
    public const OPTION_VIDEO_FILTER      = 'VIDEO_FILTER';
17
    public const OPTION_AUDIO_CODEC       = 'AUDIO_CODEC';
18
    public const OPTION_AUDIO_BITRATE     = 'AUDIO_BITRATE';
19
    public const OPTION_CRF               = 'CRF';
20
    public const OPTION_PIX_FMT           = 'PIX_FMT';
21
    public const OPTION_PRESET            = 'PRESET';
22
    public const OPTION_TUNE              = 'TUNE';
23
    public const OPTION_STREAMABLE        = 'STREAMABLE'; // h264
24
    public const OPTION_QUALITY           = 'QUALITY'; // vp9 only
25
    public const OPTION_OUTPUT_FORMAT     = 'OUTPUT_FORMAT';
26
    public const OPTION_FRAME_PARALLEL    = 'FRAME_PARALLEL';
27
    public const OPTION_TILE_COLUMNS      = 'TILE_COLUMNS';
28
    public const OPTION_SPEED             = 'SPEED'; // vp9
29
    public const OPTION_THREADS           = 'THREADS'; // vp9
30
    public const OPTION_KEYFRAME_SPACING  = 'KEYFRAME_SPACING'; // vp9
31
32
    public const SUPPORTED_OPTIONS = [
33
        self::OPTION_OUTPUT_FORMAT => [
34
            'ffmpeg_pattern' => '-f %s',
35
        ],
36
37
        self::OPTION_VIDEO_CODEC => [
38
            'ffmpeg_pattern' => '-vcodec %s',
39
        ],
40
        self::OPTION_VIDEO_BITRATE => [
41
            'ffmpeg_pattern' => '-b:v %s',
42
        ],
43
        self::OPTION_VIDEO_MIN_BITRATE => [
44
            'ffmpeg_pattern' => '-minrate %s',
45
        ],
46
        self::OPTION_VIDEO_MAX_BITRATE => [
47
            'ffmpeg_pattern' => '-maxrate %s',
48
        ],
49
50
        self::OPTION_AUDIO_CODEC => [
51
            'ffmpeg_pattern' => '-acodec %s',
52
        ],
53
        self::OPTION_AUDIO_BITRATE => [
54
            'ffmpeg_pattern' => '-b:a %s',
55
        ],
56
        self::OPTION_PIX_FMT => [
57
            'ffmpeg_pattern' => '-pix_fmt %s',
58
        ],
59
        self::OPTION_PRESET => [
60
            'ffmpeg_pattern' => '-preset %s',
61
        ],
62
        self::OPTION_SPEED => [
63
            'ffmpeg_pattern' => '-speed %s',
64
        ],
65
        self::OPTION_THREADS => [
66
            'ffmpeg_pattern' => '-threads %s',
67
        ],
68
69
        self::OPTION_KEYFRAME_SPACING => [
70
            'ffmpeg_pattern' => '-g %s',
71
        ],
72
        self::OPTION_QUALITY => [
73
            'ffmpeg_pattern' => '-quality %s',
74
        ],
75
        self::OPTION_CRF => [
76
            'ffmpeg_pattern' => '-crf %s',
77
        ],
78
        self::OPTION_STREAMABLE => [
79
            'ffmpeg_pattern' => '-movflags +faststart',
80
        ],
81
82
        self::OPTION_FRAME_PARALLEL => [
83
            'ffmpeg_pattern' => '-frame-parallel %s',
84
        ],
85
        self::OPTION_TILE_COLUMNS => [
86
            'ffmpeg_pattern' => '-tile-columns %s',
87
        ],
88
        self::OPTION_TUNE => [
89
            'ffmpeg_pattern' => '-tune %s',
90
        ],
91
        self::OPTION_VIDEO_FILTER => [
92
            'ffmpeg_pattern' => '-vf %s',
93
        ],
94
    ];
95
96
    /** @var array<string, bool|string|int|VideoFilterInterface> */
97
    protected $options = [];
98
99
    /**
100
     * @param array<string, bool|string|int|VideoFilterInterface> $options
101
     *
102
     * @throws InvalidArgumentException in case of unsupported option
103
     */
104 9
    public function __construct($options = [])
105
    {
106 9
        $this->ensureSupportedOptions($options);
107 8
        $this->options = $options;
108 8
    }
109
110 8
    public function isOptionValid(string $optionName): bool
111
    {
112 8
        return array_key_exists($optionName, self::SUPPORTED_OPTIONS);
113
    }
114
115 4
    public function getOptions(): array
116
    {
117 4
        return $this->options;
118
    }
119
120
    /**
121
     * @param string                                    $option
122
     * @param bool|string|int|VideoFilterInterface|null $default if options does not exists set this one
123
     *
124
     * @return bool|string|int|VideoFilterInterface|null
125
     */
126 2
    public function getOption(string $option, $default = null)
127
    {
128 2
        return $this->options[$option] ?? $default;
129
    }
130
131 2
    public function hasOption(string $option): bool
132
    {
133 2
        return array_key_exists($option, $this->options);
134
    }
135
136 2
    public function withVideoCodec(string $videoCodec): self
137
    {
138 2
        return new self(array_merge($this->options, [
139 2
            self::OPTION_VIDEO_CODEC => $videoCodec,
140
        ]));
141
    }
142
143 1
    public function withVideoFilter(VideoFilterInterface $videoFilter): self
144
    {
145 1
        return new self(array_merge($this->options, [
146 1
            self::OPTION_VIDEO_FILTER => $videoFilter,
147
        ]));
148
    }
149
150 1
    public function withAudioCodec(string $audioCodec): self
151
    {
152 1
        return new self(array_merge($this->options, [
153 1
            self::OPTION_AUDIO_CODEC => $audioCodec,
154
        ]));
155
    }
156
157
    /**
158
     * Tiling splits the video frame into multiple columns,
159
     * which slightly reduces quality but speeds up encoding performance.
160
     * Tiles must be at least 256 pixels wide, so there is a limit to how many tiles can be used.
161
     * Depending upon the number of tiles and the resolution of the output frame, more CPU threads may be useful.
162
     *
163
     * Generally speaking, there is limited value to multiple threads when the output frame size is very small.
164
     */
165 3
    public function withTileColumns(int $tileColumns): self
166
    {
167 3
        return new self(array_merge($this->options, [
168 3
            self::OPTION_TILE_COLUMNS => $tileColumns,
169
        ]));
170
    }
171
172
    /**
173
     * VP9 ?
174
     * It is recommended to allow up to 240 frames of video between keyframes (8 seconds for 30fps content).
175
     * Keyframes are video frames which are self-sufficient; they don't rely upon any other frames to render
176
     * but they tend to be larger than other frame types.
177
     * For web and mobile playback, generous spacing between keyframes allows the encoder to choose the best
178
     * placement of keyframes to maximize quality.
179
     */
180 1
    public function withKeyframeSpacing(int $keyframeSpacing): self
181
    {
182 1
        return new self(array_merge($this->options, [
183 1
            self::OPTION_KEYFRAME_SPACING => $keyframeSpacing,
184
        ]));
185
    }
186
187 1
    public function withFrameParallel(int $frameParallel): self
188
    {
189 1
        return new self(array_merge($this->options, [
190 1
            self::OPTION_FRAME_PARALLEL => $frameParallel,
191
        ]));
192
    }
193
194 1
    public function withCrf(int $crf): self
195
    {
196 1
        return new self(array_merge($this->options, [
197 1
            self::OPTION_CRF => $crf,
198
        ]));
199
    }
200
201 1
    public function withPixFmt(string $pixFmt): self
202
    {
203 1
        return new self(array_merge($this->options, [
204 1
            self::OPTION_PIX_FMT => $pixFmt,
205
        ]));
206
    }
207
208 1
    public function withPreset(string $preset): self
209
    {
210 1
        return new self(array_merge($this->options, [
211 1
            self::OPTION_PRESET => $preset,
212
        ]));
213
    }
214
215 1
    public function withSpeed(int $speed): self
216
    {
217 1
        return new self(array_merge($this->options, [
218 1
            self::OPTION_SPEED => $speed,
219
        ]));
220
    }
221
222 2
    public function withThreads(int $threads): self
223
    {
224 2
        return new self(array_merge($this->options, [
225 2
            self::OPTION_THREADS => $threads,
226
        ]));
227
    }
228
229 2
    public function withTune(string $tune): self
230
    {
231 2
        return new self(array_merge($this->options, [
232 2
            self::OPTION_TUNE => $tune,
233
        ]));
234
    }
235
236
    /**
237
     * If true, add streamable options for mp4 container (-movflags +faststart).
238
     */
239 1
    public function withStreamable(bool $streamable): self
240
    {
241 1
        return new self(array_merge($this->options, [
242 1
            self::OPTION_STREAMABLE => $streamable,
243
        ]));
244
    }
245
246
    /**
247
     * @param string $bitrate Bitrate with optional unit: 1000000, 1000k or 1M
248
     *
249
     * @throws InvalidArgumentException if bitrate value is invalid
250
     */
251 2
    public function withAudioBitrate(string $bitrate): self
252
    {
253 2
        $this->ensureValidBitRateUnit($bitrate);
254
255 1
        return new self(array_merge($this->options, [
256 1
            self::OPTION_AUDIO_BITRATE => $bitrate,
257
        ]));
258
    }
259
260
    /**
261
     * @param string $bitrate Bitrate or target bitrate with optional unit: 1000000, 1000k or 1M
262
     *
263
     * @throws InvalidArgumentException if bitrate value is invalid
264
     */
265 2
    public function withVideoBitrate(string $bitrate): self
266
    {
267 2
        $this->ensureValidBitRateUnit($bitrate);
268
269 1
        return new self(array_merge($this->options, [
270 1
            self::OPTION_VIDEO_BITRATE => $bitrate,
271
        ]));
272
    }
273
274
    /**
275
     * @param string $minBitrate Bitrate with optional unit: 1000000, 1000k or 1M
276
     *
277
     * @throws InvalidArgumentException if bitrate value is invalid
278
     */
279 2
    public function withVideoMinBitrate(string $minBitrate): self
280
    {
281 2
        $this->ensureValidBitRateUnit($minBitrate);
282
283 1
        return new self(array_merge($this->options, [
284 1
            self::OPTION_VIDEO_MIN_BITRATE => $minBitrate,
285
        ]));
286
    }
287
288
    /**
289
     * @param string $maxBitrate Bitrate with optional unit: 1000000, 1000k or 1M
290
     *
291
     * @throws InvalidArgumentException if bitrate value is invalid
292
     */
293 2
    public function withVideoMaxBitrate(string $maxBitrate): self
294
    {
295 2
        $this->ensureValidBitRateUnit($maxBitrate);
296
297 1
        return new self(array_merge($this->options, [
298 1
            self::OPTION_VIDEO_MAX_BITRATE => $maxBitrate,
299
        ]));
300
    }
301
302 1
    public function withQuality(string $quality): self
303
    {
304 1
        return new self(array_merge($this->options, [
305 1
            self::OPTION_QUALITY => $quality,
306
        ]));
307
    }
308
309 2
    public function withOutputFormat(string $outputFormat): self
310
    {
311 2
        return new self(array_merge($this->options, [
312 2
            self::OPTION_OUTPUT_FORMAT => $outputFormat,
313
        ]));
314
    }
315
316
    /**
317
     * @return array<string, string>
318
     */
319 3
    public function getFFMpegArguments(): array
320
    {
321 3
        $args = [];
322 3
        foreach ($this->options as $key => $value) {
323 3
            $ffmpeg_pattern = self::SUPPORTED_OPTIONS[$key]['ffmpeg_pattern'];
324 3
            if (is_bool($value)) {
325 1
                $args[$key] = $ffmpeg_pattern;
326 3
            } elseif ($value instanceof VideoFilterInterface) {
327 1
                $args[$key] = sprintf($ffmpeg_pattern, $value->getFFmpegCLIValue());
328
            } else {
329 3
                $args[$key] = sprintf($ffmpeg_pattern, $value);
330
            }
331
        }
332
333 3
        return $args;
334
    }
335
336
    /**
337
     * Ensure that a bitrate is valid (optional unit: k or M ).
338
     *
339
     * @throws InvalidArgumentException
340
     */
341 2
    protected function ensureValidBitRateUnit(string $bitrate): void
342
    {
343 2
        if (preg_match('/^\d+(k|M)?$/i', $bitrate) !== 1) {
344 1
            throw new InvalidArgumentException(sprintf('"%s"', $bitrate));
345
        }
346 1
    }
347
348
    /**
349
     * Ensure that all options are supported.
350
     *
351
     * @throws InvalidArgumentException in case of unsupported option
352
     */
353 9
    protected function ensureSupportedOptions(array $options): void
354
    {
355 9
        foreach (array_keys($options) as $optionName) {
356 8
            if (!$this->isOptionValid($optionName)) {
357 1
                throw new InvalidArgumentException(
358 8
                    sprintf('Unsupported option "%s" given.', $optionName)
359
                );
360
            }
361
        }
362 8
    }
363
}
364