Completed
Push — master ( 215358...e9fb03 )
by Sébastien
10:33 queued 08:15
created

VideoTranscodeParams::withQuality()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 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 VideoTranscodeParams
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 5
    public function __construct($options = [])
103
    {
104 5
        $this->checkOptions($options);
105 5
        $this->options = $options;
106 5
    }
107
108 5
    protected function checkOptions(array $options): void
109
    {
110 5
        foreach (array_keys($options) as $optionName) {
111 4
            if (!$this->isOptionValid($optionName)) {
112
                throw new InvalidArgumentException(
113 4
                    sprintf('Unsupported option "%s" given.', $optionName)
114
                );
115
            }
116
        }
117 5
    }
118
119 4
    public function isOptionValid(string $optionName): bool
120
    {
121 4
        return array_key_exists($optionName, self::SUPPORTED_OPTIONS);
122
    }
123
124 3
    public function getOptions(): array
125
    {
126 3
        return $this->options;
127
    }
128
129
    /**
130
     * @param string                                    $option
131
     * @param bool|string|int|VideoFilterInterface|null $default if options does not exists set this one
132
     *
133
     * @return bool|string|int|VideoFilterInterface|null
134
     */
135 2
    public function getOption(string $option, $default = null)
136
    {
137 2
        return $this->options[$option] ?? $default;
138
    }
139
140
    public function hasOption(string $option): bool
141
    {
142
        return array_key_exists($option, $this->options);
143
    }
144
145 1
    public function withVideoCodec(string $videoCodec): self
146
    {
147 1
        return new self(array_merge($this->options, [
148 1
            self::OPTION_VIDEO_CODEC => $videoCodec,
149
        ]));
150
    }
151
152 1
    public function withVideoFilter(VideoFilterInterface $videoFilter): self
153
    {
154 1
        return new self(array_merge($this->options, [
155 1
            self::OPTION_VIDEO_FILTER => $videoFilter,
156
        ]));
157
    }
158
159 1
    public function withAudioCodec(string $audioCodec): self
160
    {
161 1
        return new self(array_merge($this->options, [
162 1
            self::OPTION_AUDIO_CODEC => $audioCodec,
163
        ]));
164
    }
165
166
    /**
167
     * Tiling splits the video frame into multiple columns,
168
     * which slightly reduces quality but speeds up encoding performance.
169
     * Tiles must be at least 256 pixels wide, so there is a limit to how many tiles can be used.
170
     * Depending upon the number of tiles and the resolution of the output frame, more CPU threads may be useful.
171
     *
172
     * Generally speaking, there is limited value to multiple threads when the output frame size is very small.
173
     */
174 2
    public function withTileColumns(int $tileColumns): self
175
    {
176 2
        return new self(array_merge($this->options, [
177 2
            self::OPTION_TILE_COLUMNS => $tileColumns,
178
        ]));
179
    }
180
181
    /**
182
     * VP9 ?
183
     * It is recommended to allow up to 240 frames of video between keyframes (8 seconds for 30fps content).
184
     * Keyframes are video frames which are self-sufficient; they don't rely upon any other frames to render
185
     * but they tend to be larger than other frame types.
186
     * For web and mobile playback, generous spacing between keyframes allows the encoder to choose the best
187
     * placement of keyframes to maximize quality.
188
     */
189 1
    public function withKeyframeSpacing(int $keyframeSpacing): self
190
    {
191 1
        return new self(array_merge($this->options, [
192 1
            self::OPTION_KEYFRAME_SPACING => $keyframeSpacing,
193
        ]));
194
    }
195
196 1
    public function withFrameParallel(int $frameParallel): self
197
    {
198 1
        return new self(array_merge($this->options, [
199 1
            self::OPTION_FRAME_PARALLEL => $frameParallel,
200
        ]));
201
    }
202
203 1
    public function withCrf(int $crf): self
204
    {
205 1
        return new self(array_merge($this->options, [
206 1
            self::OPTION_CRF => $crf,
207
        ]));
208
    }
209
210 1
    public function withPixFmt(string $pixFmt): self
211
    {
212 1
        return new self(array_merge($this->options, [
213 1
            self::OPTION_PIX_FMT => $pixFmt,
214
        ]));
215
    }
216
217 1
    public function withPreset(string $preset): self
218
    {
219 1
        return new self(array_merge($this->options, [
220 1
            self::OPTION_PRESET => $preset,
221
        ]));
222
    }
223
224 1
    public function withSpeed(int $speed): self
225
    {
226 1
        return new self(array_merge($this->options, [
227 1
            self::OPTION_SPEED => $speed,
228
        ]));
229
    }
230
231 2
    public function withThreads(int $threads): self
232
    {
233 2
        return new self(array_merge($this->options, [
234 2
            self::OPTION_THREADS => $threads,
235
        ]));
236
    }
237
238 1
    public function withTune(string $tune): self
239
    {
240 1
        return new self(array_merge($this->options, [
241 1
            self::OPTION_TUNE => $tune,
242
        ]));
243
    }
244
245
    /**
246
     * If true, add streamable options for mp4 container (-movflags +faststart).
247
     */
248 1
    public function withStreamable(bool $streamable): self
249
    {
250 1
        return new self(array_merge($this->options, [
251 1
            self::OPTION_STREAMABLE => $streamable,
252
        ]));
253
    }
254
255
    /**
256
     * @param string $bitrate Bitrate with optional unit: 1000000, 1000k or 1M
257
     *
258
     * @throws InvalidArgumentException if bitrate value is invalid
259
     */
260 2
    public function withAudioBitrate(string $bitrate): self
261
    {
262 2
        $this->ensureValidBitRateUnit($bitrate);
263
264 1
        return new self(array_merge($this->options, [
265 1
            self::OPTION_AUDIO_BITRATE => $bitrate,
266
        ]));
267
    }
268
269
    /**
270
     * @param string $bitrate Bitrate or target bitrate with optional unit: 1000000, 1000k or 1M
271
     *
272
     * @throws InvalidArgumentException if bitrate value is invalid
273
     */
274 2
    public function withVideoBitrate(string $bitrate): self
275
    {
276 2
        $this->ensureValidBitRateUnit($bitrate);
277
278 1
        return new self(array_merge($this->options, [
279 1
            self::OPTION_VIDEO_BITRATE => $bitrate,
280
        ]));
281
    }
282
283
    /**
284
     * @param string $minBitrate Bitrate with optional unit: 1000000, 1000k or 1M
285
     *
286
     * @throws InvalidArgumentException if bitrate value is invalid
287
     */
288 2
    public function withVideoMinBitrate(string $minBitrate): self
289
    {
290 2
        $this->ensureValidBitRateUnit($minBitrate);
291
292 1
        return new self(array_merge($this->options, [
293 1
            self::OPTION_VIDEO_MIN_BITRATE => $minBitrate,
294
        ]));
295
    }
296
297
    /**
298
     * @param string $maxBitrate Bitrate with optional unit: 1000000, 1000k or 1M
299
     *
300
     * @throws InvalidArgumentException if bitrate value is invalid
301
     */
302 2
    public function withVideoMaxBitrate(string $maxBitrate): self
303
    {
304 2
        $this->ensureValidBitRateUnit($maxBitrate);
305
306 1
        return new self(array_merge($this->options, [
307 1
            self::OPTION_VIDEO_MAX_BITRATE => $maxBitrate,
308
        ]));
309
    }
310
311 1
    public function withQuality(string $quality): self
312
    {
313 1
        return new self(array_merge($this->options, [
314 1
            self::OPTION_QUALITY => $quality,
315
        ]));
316
    }
317
318 1
    public function withOutputFormat(string $outputFormat): self
319
    {
320 1
        return new self(array_merge($this->options, [
321 1
            self::OPTION_OUTPUT_FORMAT => $outputFormat,
322
        ]));
323
    }
324
325
    /**
326
     * @return array<string, string>
327
     */
328 2
    public function getFFMpegArguments(): array
329
    {
330 2
        $args = [];
331 2
        foreach ($this->options as $key => $value) {
332 2
            $ffmpeg_pattern = self::SUPPORTED_OPTIONS[$key]['ffmpeg_pattern'];
333 2
            if (is_bool($value)) {
334 1
                $args[$key] = $ffmpeg_pattern;
335 2
            } elseif ($value instanceof VideoFilterInterface) {
336 1
                $args[$key] = sprintf($ffmpeg_pattern, $value->getFFmpegCLIValue());
337
            } else {
338 2
                $args[$key] = sprintf($ffmpeg_pattern, $value);
339
            }
340
        }
341
342 2
        return $args;
343
    }
344
345
    /**
346
     * Ensure that a bitrate is valid (optionnal unit: k or M ).
347
     *
348
     * @throws InvalidArgumentException
349
     */
350 2
    protected function ensureValidBitRateUnit(string $bitrate): void
351
    {
352 2
        if (preg_match('/^\d+(k|M)?$/i', $bitrate) !== 1) {
353 1
            throw new InvalidArgumentException(sprintf('"%s"', $bitrate));
354
        }
355 1
    }
356
}
357