1 | // Copyright (C) 2022 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | #include "qffmpegencoderoptions_p.h" |
4 | |
5 | #if QT_CONFIG(vaapi) |
6 | #include <va/va.h> |
7 | #endif |
8 | |
9 | QT_BEGIN_NAMESPACE |
10 | |
11 | // unfortunately there is no common way to specify options for the encoders. The code here tries to map our settings sensibly |
12 | // to options available in different encoders |
13 | |
14 | // For constant quality options, we're trying to map things to approx those bit rates for 1080p@30fps (in Mbps): |
15 | // VeryLow Low Normal High VeryHigh |
16 | // H264: 0.8M 1.5M 3.5M 6M 10M |
17 | // H265: 0.5M 1.0M 2.5M 4M 7M |
18 | |
19 | [[maybe_unused]] |
20 | static int bitrateForSettings(const QMediaEncoderSettings &settings, bool hdr = false) |
21 | { |
22 | // calculate an acceptable bitrate depending on video codec, resolution, framerate and requested quality |
23 | // The calculations are rather heuristic here, trying to take into account how well codecs compress using |
24 | // the tables above. |
25 | |
26 | // The table here is for 30FPS |
27 | const double bitsPerPixel[int(QMediaFormat::VideoCodec::LastVideoCodec)+1][QMediaRecorder::VeryHighQuality+1] = { |
28 | { 1.2, 2.25, 5, 9, 15 }, // MPEG1, |
29 | { 0.8, 1.5, 3.5, 6, 10 }, // MPEG2 |
30 | { 0.4, 0.75, 1.75, 3, 5 }, // MPEG4 |
31 | { 0.4, 0.75, 1.75, 3, 5 }, // H264 |
32 | { 0.3, 0.5, 0.2, 2, 3 }, // H265 |
33 | { 0.4, 0.75, 1.75, 3, 5 }, // VP8 |
34 | { 0.3, 0.5, 0.2, 2, 3 }, // VP9 |
35 | { 0.2, 0.4, 0.9, 1.5, 2.5 }, // AV1 |
36 | { 0.4, 0.75, 1.75, 3, 5 }, // Theora |
37 | { 0.8, 1.5, 3.5, 6, 10 }, // WMV |
38 | { 16, 24, 32, 40, 48 }, // MotionJPEG |
39 | }; |
40 | |
41 | QSize s = settings.videoResolution(); |
42 | double bitrate = bitsPerPixel[int(settings.videoCodec())][settings.quality()]*s.width()*s.height(); |
43 | |
44 | if (settings.videoCodec() != QMediaFormat::VideoCodec::MotionJPEG) { |
45 | // We assume that doubling the framerate requires 1.5 times the amount of data (not twice, as intraframe |
46 | // differences will be smaller). 4 times the frame rate uses thus 2.25 times the data, etc. |
47 | float rateMultiplier = log2(x: settings.videoFrameRate()/30.); |
48 | bitrate *= pow(x: 1.5, y: rateMultiplier); |
49 | } else { |
50 | // MotionJPEG doesn't optimize between frames, so we have a linear dependency on framerate |
51 | bitrate *= settings.videoFrameRate()/30.; |
52 | } |
53 | |
54 | // HDR requires 10bits per pixel instead of 8, so apply a factor of 1.25. |
55 | if (hdr) |
56 | bitrate *= 1.25; |
57 | return bitrate; |
58 | } |
59 | |
60 | static void apply_openh264(const QMediaEncoderSettings &settings, AVCodecContext *codec, |
61 | AVDictionary **opts) |
62 | { |
63 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding |
64 | || settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) { |
65 | codec->bit_rate = settings.videoBitRate(); |
66 | av_dict_set(pm: opts, key: "rc_mode" , value: "bitrate" , flags: 0); |
67 | } else { |
68 | av_dict_set(pm: opts, key: "rc_mode" , value: "quality" , flags: 0); |
69 | static const int q[] = { 51, 48, 38, 25, 5 }; |
70 | codec->qmax = codec->qmin = q[settings.quality()]; |
71 | } |
72 | } |
73 | |
74 | static void apply_x264(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **opts) |
75 | { |
76 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding || settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) { |
77 | codec->bit_rate = settings.videoBitRate(); |
78 | } else { |
79 | const char *scales[] = { |
80 | "29" , "26" , "23" , "21" , "19" |
81 | }; |
82 | av_dict_set(pm: opts, key: "crf" , value: scales[settings.quality()], flags: 0); |
83 | } |
84 | } |
85 | |
86 | static void apply_x265(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **opts) |
87 | { |
88 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding || settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) { |
89 | codec->bit_rate = settings.videoBitRate(); |
90 | } else { |
91 | const char *scales[QMediaRecorder::VeryHighQuality+1] = { |
92 | "40" , "34" , "28" , "26" , "24" , |
93 | }; |
94 | av_dict_set(pm: opts, key: "crf" , value: scales[settings.quality()], flags: 0); |
95 | } |
96 | } |
97 | |
98 | static void apply_libvpx(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **opts) |
99 | { |
100 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding || settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) { |
101 | codec->bit_rate = settings.videoBitRate(); |
102 | } else { |
103 | const char *scales[QMediaRecorder::VeryHighQuality+1] = { |
104 | "38" , "34" , "31" , "28" , "25" , |
105 | }; |
106 | av_dict_set(pm: opts, key: "crf" , value: scales[settings.quality()], flags: 0); |
107 | av_dict_set(pm: opts, key: "b" , value: 0, flags: 0); |
108 | } |
109 | av_dict_set(pm: opts, key: "row-mt" , value: "1" , flags: 0); // better multithreading |
110 | } |
111 | |
112 | #ifdef Q_OS_DARWIN |
113 | static void apply_videotoolbox(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **opts) |
114 | { |
115 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding || settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) { |
116 | codec->bit_rate = settings.videoBitRate(); |
117 | } else { |
118 | // only use quality on macOS/ARM, as FFmpeg doesn't support it on the other platforms and would throw |
119 | // an error when initializing the codec |
120 | #if defined(Q_OS_MACOS) && defined(Q_PROCESSOR_ARM_64) |
121 | // Videotoolbox describes quality as a number from 0 to 1, with low == 0.25, normal 0.5, high 0.75 and lossless = 1 |
122 | // ffmpeg uses a different scale going from 0 to 11800. |
123 | // Values here are adjusted to agree approximately with the target bit rates listed above |
124 | const int scales[] = { |
125 | 3000, 4800, 5900, 6900, 7700, |
126 | }; |
127 | codec->global_quality = scales[settings.quality()]; |
128 | codec->flags |= AV_CODEC_FLAG_QSCALE; |
129 | #else |
130 | codec->bit_rate = bitrateForSettings(settings); |
131 | #endif |
132 | } |
133 | |
134 | // Videotooldox hw acceleration fails of some hardwares, |
135 | // allow_sw makes sw encoding available if hw encoding failed. |
136 | // Under the hood, ffmpeg sets |
137 | // kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder instead of |
138 | // kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder |
139 | av_dict_set(opts, "allow_sw" , "1" , 0); |
140 | } |
141 | #endif |
142 | |
143 | #if QT_CONFIG(vaapi) |
144 | static void apply_vaapi(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **/*opts*/) |
145 | { |
146 | // See also vaapi_encode_init_rate_control() in libavcodec |
147 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding) { |
148 | codec->bit_rate = settings.videoBitRate(); |
149 | codec->rc_max_rate = settings.videoBitRate(); |
150 | } else if (settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) { |
151 | codec->bit_rate = settings.videoBitRate(); |
152 | } else { |
153 | const int *quality = nullptr; |
154 | // unfortunately, all VA codecs use different quality scales :/ |
155 | switch (settings.videoCodec()) { |
156 | case QMediaFormat::VideoCodec::MPEG2: { |
157 | static const int q[] = { 20, 15, 10, 8, 6 }; |
158 | quality = q; |
159 | break; |
160 | } |
161 | case QMediaFormat::VideoCodec::MPEG4: |
162 | case QMediaFormat::VideoCodec::H264: { |
163 | static const int q[] = { 29, 26, 23, 21, 19 }; |
164 | quality = q; |
165 | break; |
166 | } |
167 | case QMediaFormat::VideoCodec::H265: { |
168 | static const int q[] = { 40, 34, 28, 26, 24 }; |
169 | quality = q; |
170 | break; |
171 | } |
172 | case QMediaFormat::VideoCodec::VP8: { |
173 | static const int q[] = { 56, 48, 40, 34, 28 }; |
174 | quality = q; |
175 | break; |
176 | } |
177 | case QMediaFormat::VideoCodec::VP9: { |
178 | static const int q[] = { 124, 112, 100, 88, 76 }; |
179 | quality = q; |
180 | break; |
181 | } |
182 | case QMediaFormat::VideoCodec::MotionJPEG: { |
183 | static const int q[] = { 40, 60, 80, 90, 95 }; |
184 | quality = q; |
185 | break; |
186 | } |
187 | case QMediaFormat::VideoCodec::AV1: |
188 | case QMediaFormat::VideoCodec::Theora: |
189 | case QMediaFormat::VideoCodec::WMV: |
190 | default: |
191 | break; |
192 | } |
193 | |
194 | if (quality) |
195 | codec->global_quality = quality[settings.quality()]; |
196 | } |
197 | } |
198 | #endif |
199 | |
200 | static void apply_nvenc(const QMediaEncoderSettings &settings, AVCodecContext *codec, |
201 | AVDictionary **opts) |
202 | { |
203 | switch (settings.encodingMode()) { |
204 | case QMediaRecorder::EncodingMode::AverageBitRateEncoding: |
205 | av_dict_set(pm: opts, key: "vbr" , value: "1" , flags: 0); |
206 | codec->bit_rate = settings.videoBitRate(); |
207 | break; |
208 | case QMediaRecorder::EncodingMode::ConstantBitRateEncoding: |
209 | av_dict_set(pm: opts, key: "cbr" , value: "1" , flags: 0); |
210 | codec->bit_rate = settings.videoBitRate(); |
211 | codec->rc_max_rate = codec->rc_min_rate = codec->bit_rate; |
212 | break; |
213 | case QMediaRecorder::EncodingMode::ConstantQualityEncoding: { |
214 | static const char *q[] = { "51" , "48" , "35" , "15" , "1" }; |
215 | av_dict_set(pm: opts, key: "cq" , value: q[settings.quality()], flags: 0); |
216 | } break; |
217 | default: |
218 | break; |
219 | } |
220 | } |
221 | |
222 | #ifdef Q_OS_WINDOWS |
223 | static void apply_mf(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **opts) |
224 | { |
225 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding || settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) { |
226 | codec->bit_rate = settings.videoBitRate(); |
227 | av_dict_set(opts, "rate_control" , "cbr" , 0); |
228 | } else { |
229 | av_dict_set(opts, "rate_control" , "quality" , 0); |
230 | const char *scales[] = { |
231 | "25" , "50" , "75" , "90" , "100" |
232 | }; |
233 | av_dict_set(opts, "quality" , scales[settings.quality()], 0); |
234 | } |
235 | } |
236 | #endif |
237 | |
238 | #ifdef Q_OS_ANDROID |
239 | static void apply_mediacodec(const QMediaEncoderSettings &settings, AVCodecContext *codec, |
240 | AVDictionary **opts) |
241 | { |
242 | codec->bit_rate = settings.videoBitRate(); |
243 | |
244 | const int quality[] = { 25, 50, 75, 90, 100 }; |
245 | codec->global_quality = quality[settings.quality()]; |
246 | |
247 | switch (settings.encodingMode()) { |
248 | case QMediaRecorder::EncodingMode::AverageBitRateEncoding: |
249 | av_dict_set(opts, "bitrate_mode" , "vbr" , 1); |
250 | break; |
251 | case QMediaRecorder::EncodingMode::ConstantBitRateEncoding: |
252 | av_dict_set(opts, "bitrate_mode" , "cbr" , 1); |
253 | break; |
254 | case QMediaRecorder::EncodingMode::ConstantQualityEncoding: |
255 | // av_dict_set(opts, "bitrate_mode", "cq", 1); |
256 | av_dict_set(opts, "bitrate_mode" , "cbr" , 1); |
257 | break; |
258 | default: |
259 | break; |
260 | } |
261 | |
262 | switch (settings.videoCodec()) { |
263 | case QMediaFormat::VideoCodec::H264: { |
264 | const char *levels[] = { "2.2" , "3.2" , "4.2" , "5.2" , "6.2" }; |
265 | av_dict_set(opts, "level" , levels[settings.quality()], 1); |
266 | codec->profile = FF_PROFILE_H264_HIGH; |
267 | break; |
268 | } |
269 | case QMediaFormat::VideoCodec::H265: { |
270 | const char *levels[] = { "h2.1" , "h3.1" , "h4.1" , "h5.1" , "h6.1" }; |
271 | av_dict_set(opts, "level" , levels[settings.quality()], 1); |
272 | codec->profile = FF_PROFILE_HEVC_MAIN; |
273 | break; |
274 | } |
275 | default: |
276 | break; |
277 | } |
278 | } |
279 | #endif |
280 | |
281 | namespace QFFmpeg { |
282 | |
283 | using ApplyOptions = void (*)(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **opts); |
284 | |
285 | const struct { |
286 | const char *name; |
287 | ApplyOptions apply; |
288 | } videoCodecOptionTable[] = { { .name: "libx264" , .apply: apply_x264 }, |
289 | { .name: "libx265xx" , .apply: apply_x265 }, |
290 | { .name: "libvpx" , .apply: apply_libvpx }, |
291 | { .name: "libvpx_vp9" , .apply: apply_libvpx }, |
292 | { .name: "libopenh264" , .apply: apply_openh264 }, |
293 | { .name: "h264_nvenc" , .apply: apply_nvenc }, |
294 | { .name: "hevc_nvenc" , .apply: apply_nvenc }, |
295 | { .name: "av1_nvenc" , .apply: apply_nvenc }, |
296 | #ifdef Q_OS_DARWIN |
297 | { "h264_videotoolbox" , apply_videotoolbox }, |
298 | { "hevc_videotoolbox" , apply_videotoolbox }, |
299 | { "prores_videotoolbox" , apply_videotoolbox }, |
300 | { "vp9_videotoolbox" , apply_videotoolbox }, |
301 | #endif |
302 | #if QT_CONFIG(vaapi) |
303 | { .name: "mpeg2_vaapi" , .apply: apply_vaapi }, |
304 | { .name: "mjpeg_vaapi" , .apply: apply_vaapi }, |
305 | { .name: "h264_vaapi" , .apply: apply_vaapi }, |
306 | { .name: "hevc_vaapi" , .apply: apply_vaapi }, |
307 | { .name: "vp8_vaapi" , .apply: apply_vaapi }, |
308 | { .name: "vp9_vaapi" , .apply: apply_vaapi }, |
309 | #endif |
310 | #ifdef Q_OS_WINDOWS |
311 | { "hevc_mf" , apply_mf }, |
312 | { "h264_mf" , apply_mf }, |
313 | #endif |
314 | #ifdef Q_OS_ANDROID |
315 | { "hevc_mediacodec" , apply_mediacodec }, |
316 | { "h264_mediacodec" , apply_mediacodec }, |
317 | #endif |
318 | { .name: nullptr, .apply: nullptr } }; |
319 | |
320 | const struct { |
321 | const char *name; |
322 | ApplyOptions apply; |
323 | } audioCodecOptionTable[] = { |
324 | { .name: nullptr, .apply: nullptr } |
325 | }; |
326 | |
327 | void applyVideoEncoderOptions(const QMediaEncoderSettings &settings, const QByteArray &codecName, AVCodecContext *codec, AVDictionary **opts) |
328 | { |
329 | av_dict_set(pm: opts, key: "threads" , value: "auto" , flags: 0); // we always want automatic threading |
330 | |
331 | auto *table = videoCodecOptionTable; |
332 | while (table->name) { |
333 | if (codecName == table->name) { |
334 | table->apply(settings, codec, opts); |
335 | return; |
336 | } |
337 | |
338 | ++table; |
339 | } |
340 | } |
341 | |
342 | void applyAudioEncoderOptions(const QMediaEncoderSettings &settings, const QByteArray &codecName, AVCodecContext *codec, AVDictionary **opts) |
343 | { |
344 | codec->thread_count = -1; // we always want automatic threading |
345 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding || settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) |
346 | codec->bit_rate = settings.audioBitRate(); |
347 | |
348 | auto *table = audioCodecOptionTable; |
349 | while (table->name) { |
350 | if (codecName == table->name) { |
351 | table->apply(settings, codec, opts); |
352 | return; |
353 | } |
354 | |
355 | ++table; |
356 | } |
357 | |
358 | } |
359 | |
360 | } |
361 | |
362 | QT_END_NAMESPACE |
363 | |