| 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 | #ifdef Q_OS_DARWIN |
| 125 | static void apply_videotoolbox(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **opts) |
| 126 | { |
| 127 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding || settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) { |
| 128 | codec->bit_rate = settings.videoBitRate(); |
| 129 | } else { |
| 130 | // only use quality on macOS/ARM, as FFmpeg doesn't support it on the other platforms and would throw |
| 131 | // an error when initializing the codec |
| 132 | #if defined(Q_OS_MACOS) && defined(Q_PROCESSOR_ARM_64) |
| 133 | // Videotoolbox describes quality as a number from 0 to 1, with low == 0.25, normal 0.5, high 0.75 and lossless = 1 |
| 134 | // ffmpeg uses a different scale going from 0 to 11800. |
| 135 | // Values here are adjusted to agree approximately with the target bit rates listed above |
| 136 | const int scales[] = { |
| 137 | 3000, 4800, 5900, 6900, 7700, |
| 138 | }; |
| 139 | codec->global_quality = scales[settings.quality()]; |
| 140 | codec->flags |= AV_CODEC_FLAG_QSCALE; |
| 141 | #else |
| 142 | codec->bit_rate = bitrateForSettings(settings); |
| 143 | #endif |
| 144 | } |
| 145 | |
| 146 | // Videotooldox hw acceleration fails of some hardwares, |
| 147 | // allow_sw makes sw encoding available if hw encoding failed. |
| 148 | // Under the hood, ffmpeg sets |
| 149 | // kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder instead of |
| 150 | // kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder |
| 151 | av_dict_set(opts, "allow_sw" , "1" , 0); |
| 152 | } |
| 153 | #endif |
| 154 | |
| 155 | #if QT_CONFIG(vaapi) |
| 156 | static void apply_vaapi(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **/*opts*/) |
| 157 | { |
| 158 | // See also vaapi_encode_init_rate_control() in libavcodec |
| 159 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding) { |
| 160 | codec->bit_rate = settings.videoBitRate(); |
| 161 | codec->rc_max_rate = settings.videoBitRate(); |
| 162 | } else if (settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) { |
| 163 | codec->bit_rate = settings.videoBitRate(); |
| 164 | } else { |
| 165 | const int *quality = nullptr; |
| 166 | // unfortunately, all VA codecs use different quality scales :/ |
| 167 | switch (settings.videoCodec()) { |
| 168 | case QMediaFormat::VideoCodec::MPEG2: { |
| 169 | static const int q[] = { 20, 15, 10, 8, 6 }; |
| 170 | quality = q; |
| 171 | break; |
| 172 | } |
| 173 | case QMediaFormat::VideoCodec::MPEG4: |
| 174 | case QMediaFormat::VideoCodec::H264: { |
| 175 | static const int q[] = { 29, 26, 23, 21, 19 }; |
| 176 | quality = q; |
| 177 | break; |
| 178 | } |
| 179 | case QMediaFormat::VideoCodec::H265: { |
| 180 | static const int q[] = { 40, 34, 28, 26, 24 }; |
| 181 | quality = q; |
| 182 | break; |
| 183 | } |
| 184 | case QMediaFormat::VideoCodec::VP8: { |
| 185 | static const int q[] = { 56, 48, 40, 34, 28 }; |
| 186 | quality = q; |
| 187 | break; |
| 188 | } |
| 189 | case QMediaFormat::VideoCodec::VP9: { |
| 190 | static const int q[] = { 124, 112, 100, 88, 76 }; |
| 191 | quality = q; |
| 192 | break; |
| 193 | } |
| 194 | case QMediaFormat::VideoCodec::MotionJPEG: { |
| 195 | static const int q[] = { 40, 60, 80, 90, 95 }; |
| 196 | quality = q; |
| 197 | break; |
| 198 | } |
| 199 | case QMediaFormat::VideoCodec::AV1: |
| 200 | case QMediaFormat::VideoCodec::Theora: |
| 201 | case QMediaFormat::VideoCodec::WMV: |
| 202 | default: |
| 203 | break; |
| 204 | } |
| 205 | |
| 206 | if (quality) |
| 207 | codec->global_quality = quality[settings.quality()]; |
| 208 | } |
| 209 | } |
| 210 | #endif |
| 211 | |
| 212 | static void apply_nvenc(const QMediaEncoderSettings &settings, AVCodecContext *codec, |
| 213 | AVDictionary **opts) |
| 214 | { |
| 215 | switch (settings.encodingMode()) { |
| 216 | case QMediaRecorder::EncodingMode::AverageBitRateEncoding: |
| 217 | av_dict_set(pm: opts, key: "vbr" , value: "1" , flags: 0); |
| 218 | codec->bit_rate = settings.videoBitRate(); |
| 219 | break; |
| 220 | case QMediaRecorder::EncodingMode::ConstantBitRateEncoding: |
| 221 | av_dict_set(pm: opts, key: "cbr" , value: "1" , flags: 0); |
| 222 | codec->bit_rate = settings.videoBitRate(); |
| 223 | codec->rc_max_rate = codec->rc_min_rate = codec->bit_rate; |
| 224 | break; |
| 225 | case QMediaRecorder::EncodingMode::ConstantQualityEncoding: { |
| 226 | static const char *q[] = { "51" , "48" , "35" , "15" , "1" }; |
| 227 | av_dict_set(pm: opts, key: "cq" , value: q[settings.quality()], flags: 0); |
| 228 | } break; |
| 229 | default: |
| 230 | break; |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | #ifdef Q_OS_WINDOWS |
| 235 | static void apply_mf(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **opts) |
| 236 | { |
| 237 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding || settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) { |
| 238 | codec->bit_rate = settings.videoBitRate(); |
| 239 | av_dict_set(opts, "rate_control" , "cbr" , 0); |
| 240 | } else { |
| 241 | av_dict_set(opts, "rate_control" , "quality" , 0); |
| 242 | const char *scales[] = { |
| 243 | "25" , "50" , "75" , "90" , "100" |
| 244 | }; |
| 245 | av_dict_set(opts, "quality" , scales[settings.quality()], 0); |
| 246 | } |
| 247 | } |
| 248 | #endif |
| 249 | |
| 250 | #ifdef Q_OS_ANDROID |
| 251 | static void apply_mediacodec(const QMediaEncoderSettings &settings, AVCodecContext *codec, |
| 252 | AVDictionary **opts) |
| 253 | { |
| 254 | if (settings.videoBitRate() != -1) |
| 255 | codec->bit_rate = settings.videoBitRate(); |
| 256 | else |
| 257 | codec->bit_rate = bitrateForSettings(settings); |
| 258 | |
| 259 | const int quality[] = { 25, 50, 75, 90, 100 }; |
| 260 | codec->global_quality = quality[settings.quality()]; |
| 261 | |
| 262 | switch (settings.encodingMode()) { |
| 263 | case QMediaRecorder::EncodingMode::AverageBitRateEncoding: |
| 264 | av_dict_set(opts, "bitrate_mode" , "vbr" , 1); |
| 265 | break; |
| 266 | case QMediaRecorder::EncodingMode::ConstantBitRateEncoding: |
| 267 | av_dict_set(opts, "bitrate_mode" , "cbr" , 1); |
| 268 | break; |
| 269 | case QMediaRecorder::EncodingMode::ConstantQualityEncoding: |
| 270 | // av_dict_set(opts, "bitrate_mode", "cq", 1); |
| 271 | av_dict_set(opts, "bitrate_mode" , "cbr" , 1); |
| 272 | break; |
| 273 | default: |
| 274 | break; |
| 275 | } |
| 276 | |
| 277 | switch (settings.videoCodec()) { |
| 278 | case QMediaFormat::VideoCodec::H264: { |
| 279 | const char *levels[] = { "2.2" , "3.2" , "4.2" , "5.2" , "6.2" }; |
| 280 | av_dict_set(opts, "level" , levels[settings.quality()], 1); |
| 281 | codec->profile = FF_PROFILE_H264_HIGH; |
| 282 | break; |
| 283 | } |
| 284 | case QMediaFormat::VideoCodec::H265: { |
| 285 | // Set the level only for FFmpeg versions that correctly recognize level values. |
| 286 | // Affected revisions: from n7.1 https://github.com/FFmpeg/FFmpeg/commit/7753a9d62725d5bd8313e2d249acbe1c8af79ab1 |
| 287 | // up to https://github.com/FFmpeg/FFmpeg/commit/020d9f2b4886aa620252da4db7a4936378d6eb3a |
| 288 | if (avcodec_version() < 4000612 || avcodec_version() > 4002660) { |
| 289 | const char *levels[] = { "h2.1" , "h3.1" , "h4.1" , "h5.1" , "h6.1" }; |
| 290 | av_dict_set(opts, "level" , levels[settings.quality()], 1); |
| 291 | } |
| 292 | |
| 293 | codec->profile = FF_PROFILE_HEVC_MAIN; |
| 294 | break; |
| 295 | } |
| 296 | default: |
| 297 | break; |
| 298 | } |
| 299 | } |
| 300 | #endif |
| 301 | |
| 302 | namespace QFFmpeg { |
| 303 | |
| 304 | using ApplyOptions = void (*)(const QMediaEncoderSettings &settings, AVCodecContext *codec, AVDictionary **opts); |
| 305 | |
| 306 | const struct { |
| 307 | const char *name; |
| 308 | ApplyOptions apply; |
| 309 | } videoCodecOptionTable[] = { { .name: "libx264" , .apply: apply_x264 }, |
| 310 | { .name: "libx265xx" , .apply: apply_x265 }, |
| 311 | { .name: "libvpx" , .apply: apply_libvpx }, |
| 312 | { .name: "libvpx_vp9" , .apply: apply_libvpx }, |
| 313 | { .name: "libopenh264" , .apply: apply_openh264 }, |
| 314 | { .name: "h264_nvenc" , .apply: apply_nvenc }, |
| 315 | { .name: "hevc_nvenc" , .apply: apply_nvenc }, |
| 316 | { .name: "av1_nvenc" , .apply: apply_nvenc }, |
| 317 | #ifdef Q_OS_DARWIN |
| 318 | { "h264_videotoolbox" , apply_videotoolbox }, |
| 319 | { "hevc_videotoolbox" , apply_videotoolbox }, |
| 320 | { "prores_videotoolbox" , apply_videotoolbox }, |
| 321 | { "vp9_videotoolbox" , apply_videotoolbox }, |
| 322 | #endif |
| 323 | #if QT_CONFIG(vaapi) |
| 324 | { .name: "mpeg2_vaapi" , .apply: apply_vaapi }, |
| 325 | { .name: "mjpeg_vaapi" , .apply: apply_vaapi }, |
| 326 | { .name: "h264_vaapi" , .apply: apply_vaapi }, |
| 327 | { .name: "hevc_vaapi" , .apply: apply_vaapi }, |
| 328 | { .name: "vp8_vaapi" , .apply: apply_vaapi }, |
| 329 | { .name: "vp9_vaapi" , .apply: apply_vaapi }, |
| 330 | #endif |
| 331 | #ifdef Q_OS_WINDOWS |
| 332 | { "hevc_mf" , apply_mf }, |
| 333 | { "h264_mf" , apply_mf }, |
| 334 | #endif |
| 335 | #ifdef Q_OS_ANDROID |
| 336 | { "hevc_mediacodec" , apply_mediacodec }, |
| 337 | { "h264_mediacodec" , apply_mediacodec }, |
| 338 | #endif |
| 339 | { .name: nullptr, .apply: nullptr } }; |
| 340 | |
| 341 | const struct { |
| 342 | const char *name; |
| 343 | ApplyOptions apply; |
| 344 | } audioCodecOptionTable[] = { |
| 345 | { .name: nullptr, .apply: nullptr } |
| 346 | }; |
| 347 | |
| 348 | void applyVideoEncoderOptions(const QMediaEncoderSettings &settings, const QByteArray &codecName, AVCodecContext *codec, AVDictionary **opts) |
| 349 | { |
| 350 | av_dict_set(pm: opts, key: "threads" , value: "auto" , flags: 0); // we always want automatic threading |
| 351 | |
| 352 | auto *table = videoCodecOptionTable; |
| 353 | while (table->name) { |
| 354 | if (codecName == table->name) { |
| 355 | table->apply(settings, codec, opts); |
| 356 | return; |
| 357 | } |
| 358 | |
| 359 | ++table; |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | void applyAudioEncoderOptions(const QMediaEncoderSettings &settings, const QByteArray &codecName, AVCodecContext *codec, AVDictionary **opts) |
| 364 | { |
| 365 | codec->thread_count = -1; // we always want automatic threading |
| 366 | if (settings.encodingMode() == QMediaRecorder::ConstantBitRateEncoding || settings.encodingMode() == QMediaRecorder::AverageBitRateEncoding) |
| 367 | codec->bit_rate = settings.audioBitRate(); |
| 368 | |
| 369 | if (settings.audioSampleRate() != -1) |
| 370 | codec->sample_rate = settings.audioSampleRate(); |
| 371 | |
| 372 | if (settings.audioChannelCount() != -1) { |
| 373 | auto mask = QFFmpegMediaFormatInfo::avChannelLayout( |
| 374 | channelConfig: QAudioFormat::defaultChannelConfigForChannelCount(channelCount: settings.audioChannelCount())); |
| 375 | |
| 376 | #if QT_FFMPEG_HAS_AV_CHANNEL_LAYOUT |
| 377 | av_channel_layout_from_mask(&codec->ch_layout, mask); |
| 378 | #else |
| 379 | codec->channel_layout = mask; |
| 380 | codec->channels = qPopulationCount(v: codec->channel_layout); |
| 381 | #endif |
| 382 | } |
| 383 | |
| 384 | auto *table = audioCodecOptionTable; |
| 385 | while (table->name) { |
| 386 | if (codecName == table->name) { |
| 387 | table->apply(settings, codec, opts); |
| 388 | return; |
| 389 | } |
| 390 | |
| 391 | ++table; |
| 392 | } |
| 393 | |
| 394 | } |
| 395 | |
| 396 | } // namespace QFFmpeg |
| 397 | |
| 398 | QT_END_NAMESPACE |
| 399 | |