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