Revision control
Copy as Markdown
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qffmpegvideoencoderutils_p.h"
#include <QtFFmpegMediaPluginImpl/private/qffmpeg_p.h>
#include <QtMultimedia/private/qmultimediautils_p.h>
#include <QtCore/qglobalstatic.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qoperatingsystemversion.h>
#include <QtCore/qreadwritelock.h>
#include <QtCore/private/qminimalflatset_p.h>
#include <QtFFmpegMediaPluginImpl/private/qffmpegrecordingengineutils_p.h>
#include <map>
#include <mutex>
#include <shared_mutex>
extern "C" {
#include <libavutil/hwcontext.h>
#include <libavutil/pixdesc.h>
}
QT_BEGIN_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(qLcVideoEncoderUtils, "qt.multimedia.ffmpeg.videoencoderutils");
namespace QFFmpeg {
using namespace Qt::Literals;
namespace {
bool is16BitFormat(const AVPixFmtDescriptor *desc)
{
return desc->comp[0].depth == 16;
}
bool is10BitFormat(const AVPixFmtDescriptor *desc)
{
return desc->comp[0].depth == 10;
}
bool is8BitFormat(const AVPixFmtDescriptor *desc)
{
return desc->comp[0].depth == 8;
}
bool is444Format(const AVPixFmtDescriptor *desc)
{
return desc->log2_chroma_h == 0 && desc->log2_chroma_w == 0;
}
bool is422Format(const AVPixFmtDescriptor *desc)
{
return desc->log2_chroma_h == 1 && desc->log2_chroma_w == 0;
}
bool is420Format(const AVPixFmtDescriptor *desc)
{
return desc->log2_chroma_h == 1 && desc->log2_chroma_w == 1;
}
bool isGreyFormat(const AVPixFmtDescriptor *desc)
{
return desc->nb_components == 1;
}
AVScore scoreTargetSwFormat(const AVPixFmtDescriptor *sourceSwFormatDesc, AVPixelFormat fmt)
{
constexpr int AlphaMismatchPenalty = 50;
// determine the format used by the encoder.
// We prefer YUV420 based formats such as NV12 or P010. Selection trues to find the best
// matching format for the encoder depending on the bit depth of the source format
const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(fmt);
if (!desc)
return NotSuitableAVScore;
if (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)
// we really don't want HW accelerated formats here
return NotSuitableAVScore;
AVScore score = DefaultAVScore;
if (desc == sourceSwFormatDesc)
// prefer exact matches
score += 10;
const int sourceBpp = av_get_bits_per_pixel(sourceSwFormatDesc);
int bpp = av_get_bits_per_pixel(desc);
const bool sourceHasAlpha = sourceSwFormatDesc->flags & AV_PIX_FMT_FLAG_ALPHA;
if ((desc->flags & AV_PIX_FMT_FLAG_ALPHA) && !sourceHasAlpha) {
// The source has no real alpha to preserve -- don't credit the target's alpha bits
// toward a bpp match, and penalize picking an alpha target over an equally-good
// non-alpha one.
bpp -= desc->comp[desc->nb_components - 1].depth; // alpha is always the last component
score -= AlphaMismatchPenalty;
}
// we want formats with the same bpp
if (bpp == sourceBpp)
score += 100;
else if (bpp < sourceBpp)
score -= 100 + (sourceBpp - bpp);
// pessimize 10 and 16 bit formats if the source format is 8 bit
if (is8BitFormat(sourceSwFormatDesc)) {
if (is10BitFormat(desc))
score -= 100;
else if (is16BitFormat(desc))
score -= 200;
}
// Add a slight preference for 4:2:0 formats.
if (is420Format(desc))
score += 2;
else if (is422Format(desc))
score += 1;
else if (is444Format(desc))
score -= 1;
if constexpr (QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Android) {
// Add a slight preference for NV12 on Android
// as it's supported better than other 4:2:0 formats
if (fmt == AV_PIX_FMT_NV12)
score += 1;
}
if (isGreyFormat(desc) && !isGreyFormat(sourceSwFormatDesc)) // we don't want greyscale formats
return AVScore::NotSuitableAVScore;
if (desc->flags & AV_PIX_FMT_FLAG_BE) // we don't want big endian formats
score -= 10;
if (desc->flags & AV_PIX_FMT_FLAG_PAL) // we don't want paletted formats
score -= 10000;
if (desc->flags & AV_PIX_FMT_FLAG_RGB) // we don't want RGB formats
score -= 1000;
return score;
}
auto targetSwFormatScoreCalculator(AVPixelFormat sourceFormat)
{
const auto sourceSwFormatDesc = av_pix_fmt_desc_get(sourceFormat);
return [=](AVPixelFormat fmt) {
return scoreTargetSwFormat(sourceSwFormatDesc, fmt);
};
}
bool isHwFormatAcceptedByCodec(AVPixelFormat pixFormat)
{
switch (pixFormat) {
case AV_PIX_FMT_MEDIACODEC:
// Mediacodec doesn't accept AV_PIX_FMT_MEDIACODEC (QTBUG-116836)
return false;
default:
return true;
}
}
// The sw formats commonly accepted by mpeg2, mpeg4, h264, h265, vp8, vp9 and av1 encoders.
// Probing is only a fallback for codecs that tell us nothing, so the list is deliberately short;
// scoreTargetSwFormat() takes care of ranking whatever we find.
static constexpr AVPixelFormat probeCandidates[] = {
AV_PIX_FMT_YUV420P, AV_PIX_FMT_NV12, AV_PIX_FMT_NV21, AV_PIX_FMT_YUVJ420P,
AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUVA420P, AV_PIX_FMT_P010LE,
AV_PIX_FMT_YUV420P10LE, AV_PIX_FMT_YUV422P10LE, AV_PIX_FMT_YUV444P10LE, AV_PIX_FMT_GRAY8,
};
class ProbedPixelFormatCache
{
public:
std::vector<AVPixelFormat> pixelFormats(const Codec &codec, QSize resolution,
const HWAccel *accel);
private:
std::optional<std::vector<AVPixelFormat>> lookup(const AVCodec *codec, QSize resolution,
AVPixelFormat hwFormat);
struct ProbeRecord
{
QSize resolution;
AVPixelFormat hwFormat = AV_PIX_FMT_NONE;
AVPixelFormat format = AV_PIX_FMT_NONE;
bool supported = false;
};
QReadWriteLock m_lock;
std::multimap<const AVCodec *, ProbeRecord> m_cache;
};
std::vector<AVPixelFormat>
ProbedPixelFormatCache::pixelFormats(const Codec &codec, QSize resolution, const HWAccel *accel)
{
const AVPixelFormat hwFormat = accel ? accel->hwFormat() : AV_PIX_FMT_NONE;
{
std::shared_lock locker(m_lock);
if (std::optional cached = lookup(codec.get(), resolution, hwFormat))
return *cached;
}
std::unique_lock locker(m_lock);
if (std::optional cached = lookup(codec.get(), resolution, hwFormat))
return *cached;
const std::vector<AVPixelFormat> supported = probePixelFormats(codec, resolution, accel);
qCDebug(qLcVideoEncoderUtils) << "Probed pixel formats for" << codec.name() << "at"
<< resolution << "hwFormat" << hwFormat << ":" << supported;
using namespace QtMultimediaPrivate;
for (AVPixelFormat format : probeCandidates)
m_cache.emplace(codec.get(),
ProbeRecord{
resolution,
hwFormat,
format,
ranges::contains(supported, format),
});
return supported;
}
std::optional<std::vector<AVPixelFormat>>
ProbedPixelFormatCache::lookup(const AVCodec *codec, QSize resolution, AVPixelFormat hwFormat)
{
using namespace QtMultimediaPrivate;
namespace ranges = QtMultimediaPrivate::ranges;
auto range_pair = m_cache.equal_range(codec);
ranges::subrange<std::multimap<const AVCodec *, ProbeRecord>::iterator> range{
range_pair.first,
range_pair.second,
};
auto matches = [&](const auto &entry) {
return entry.second.resolution == resolution && entry.second.hwFormat == hwFormat;
};
if (!ranges::any_of(range, matches))
return std::nullopt;
return views::filter(range, [&](const auto &entry) {
return matches(entry) && entry.second.supported;
}) | views::transform([](const auto &entry) {
return entry.second.format;
}) | ranges::to<std::vector>();
}
Q_GLOBAL_STATIC(ProbedPixelFormatCache, probedPixelFormatCache)
bool isProbingDisabled()
{
static const bool disabled =
qEnvironmentVariableIntValue("QT_FFMPEG_DISABLE_ENCODER_PROBE") != 0;
return disabled;
}
AVBufferUPtr createProbeFramesContext(const HWAccel &accel, AVPixelFormat swFormat,
QSize resolution)
{
AVBufferUPtr framesContext{ av_hwframe_ctx_alloc(accel.hwDeviceContextAsBuffer()) };
if (!framesContext)
return nullptr;
auto *context = reinterpret_cast<AVHWFramesContext *>(framesContext->data);
context->format = accel.hwFormat();
context->sw_format = swFormat;
context->width = resolution.width();
context->height = resolution.height();
if (av_hwframe_ctx_init(framesContext.get()) < 0)
return nullptr;
return framesContext;
}
// Some hw encoders (e.g. nvenc, qsv) advertise the sw formats they can consume directly in
// codec.pixelFormats(), on top of their own hw pixel format. Others (e.g. vaapi) only ever
// declare their own hw pixel format there and rely entirely on the hw accel's frame
// constraints (valid_sw_formats) to describe the sw formats they accept.
bool codecDeclaresSwFormats(const Codec &codec, QSize resolution, const HWAccel *accel)
{
const std::vector<AVPixelFormat> formats = encoderPixelFormats(codec, resolution, accel);
return QtMultimediaPrivate::ranges::any_of(formats, [](AVPixelFormat fmt) {
return !isHwPixelFormat(fmt);
});
}
inline constexpr auto filterSuitablePixelFormats =
QtMultimediaPrivate::views::filter([](const ScoredPixelFormat &scoredFmt) {
return scoredFmt.score > AVScore::NotSuitableAVScore;
});
} // namespace
std::vector<AVPixelFormat> probePixelFormats(const Codec &codec, QSize resolution,
const HWAccel *accel)
{
std::vector<AVPixelFormat> result;
for (AVPixelFormat format : probeCandidates) {
AVCodecContextUPtr context{ avcodec_alloc_context3(codec.get()) };
if (!context) {
qCWarning(qLcVideoEncoderUtils)
<< "Could not allocate codec context to probe" << codec.name();
break;
}
QSpan<const AVRational> supportedFrameRates = codec.frameRates();
const AVRational frameRate =
supportedFrameRates.empty() ? AVRational{ 25, 1 } : supportedFrameRates.front();
context->width = resolution.width();
context->height = resolution.height();
context->time_base = { frameRate.den, frameRate.num };
context->framerate = frameRate;
AVBufferUPtr framesContext;
if (accel) {
framesContext = createProbeFramesContext(*accel, format, resolution);
if (!framesContext)
continue;
context->hw_frames_ctx = av_buffer_ref(framesContext.get());
context->pix_fmt = accel->hwFormat();
} else {
context->pix_fmt = format;
}
AVDictionaryHolder opts;
applyExperimentalCodecOptions(codec, opts);
if (avcodec_open2(context.get(), codec.get(), opts) >= 0)
result.push_back(format);
}
return result;
}
std::vector<AVPixelFormat> encoderPixelFormats(const Codec &codec, QSize resolution,
const HWAccel *accel)
{
// An empty result from FFmpeg means 'unknown', not 'nothing is supported', so fall back to
// asking the encoder itself rather than guessing.
const QSpan<const AVPixelFormat> declared = codec.pixelFormats();
if (!declared.empty() || isProbingDisabled())
return std::vector<AVPixelFormat>(declared.begin(), declared.end());
return probedPixelFormatCache->pixelFormats(codec, resolution, accel);
}
AVScore scoreTargetSwFormat(AVPixelFormat source, AVPixelFormat target)
{
const auto *desc = av_pix_fmt_desc_get(source);
return desc ? scoreTargetSwFormat(desc, target) : AVScore::NotSuitableAVScore;
}
std::optional<AVPixelFormat> findTargetSWFormat(AVPixelFormat sourceSWFormat, const Codec &codec,
const HWAccel &accel, QSize resolution)
{
using namespace QtMultimediaPrivate;
auto scoreTargetSwFormat = targetSwFormatScoreCalculator(sourceSWFormat);
const auto constraints = accel.constraints();
if (constraints && constraints->valid_sw_formats) {
const auto validSWFormatsForHWAccel =
makeSpan(constraints->valid_sw_formats) | ranges::to<QMinimalFlatSet>();
auto bestPixelFormat = [&]() -> std::optional<AVPixelFormat> {
if (codecDeclaresSwFormats(codec, resolution, &accel)) {
// If the codec declares sw formats, we can find the best one among the intersection
// of the codec's sw formats and the valid sw formats for the hw accel.
const std::vector<AVPixelFormat> codecPixelFormats =
encoderPixelFormats(codec, resolution, &accel);
auto formats = views::filter(codecPixelFormats, [&](AVPixelFormat fmt) {
return validSWFormatsForHWAccel.contains(fmt);
});
return findBestAVValue(formats, scoreTargetSwFormat);
} else {
return findBestAVValue(validSWFormatsForHWAccel, scoreTargetSwFormat);
}
}();
if (bestPixelFormat)
return bestPixelFormat;
}
// Some codecs, e.g. mediacodec, don't expose constraints, let's find the format in
// codec->pix_fmts (avcodec_get_supported_config with AV_CODEC_CONFIG_PIX_FORMAT since n7.1),
// or, if the codec declares nothing at all, in the probed formats.
const std::vector<AVPixelFormat> codecPixelFormats =
encoderPixelFormats(codec, resolution, &accel);
return findBestAVValue(codecPixelFormats, scoreTargetSwFormat);
}
std::vector<ScoredPixelFormat> findAndScoreTargetSWFormats(AVPixelFormat sourceSWFormat,
const Codec &codec,
const HWAccel *accel, QSize resolution)
{
if constexpr (false) {
qDebug() << "findAndScoreTargetSWFormats" << codec.name() << codec.id();
}
using namespace QtMultimediaPrivate;
auto scoreTargetSwFormat = targetSwFormatScoreCalculator(sourceSWFormat);
auto score = views::transform([&](AVPixelFormat arg) {
return ScoredPixelFormat{ arg, scoreTargetSwFormat(arg) };
});
std::vector<ScoredPixelFormat> scoredPixelFormats = [&] {
const AVHWFramesConstraints *constraints = accel ? accel->constraints() : nullptr;
if (constraints && constraints->valid_sw_formats) {
const auto validSWFormatsForHWAccel =
makeSpan(constraints->valid_sw_formats) | ranges::to<QMinimalFlatSet>();
if (codecDeclaresSwFormats(codec, resolution, accel)) {
// If the codec declares sw formats, we can find the best one among the intersection
// of the codec's sw formats and the valid sw formats for the hw accel.
const std::vector<AVPixelFormat> codecPixelFormats =
encoderPixelFormats(codec, resolution, accel);
auto validCodecPixelFormats =
views::filter(codecPixelFormats, [&](AVPixelFormat fmt) {
return validSWFormatsForHWAccel.contains(fmt);
});
return validCodecPixelFormats | score | filterSuitablePixelFormats
| ranges::to<std::vector>();
} else {
// The codec only declares its own hw pixel format (e.g. vaapi); the sw formats it
// actually accepts come solely from the hw accel's frame constraints.
return validSWFormatsForHWAccel | score | filterSuitablePixelFormats
| ranges::to<std::vector>();
}
} else {
// Some codecs, e.g. mediacodec, don't expose constraints, and pure software
// codecs (accel == nullptr) never have constraints at all; let's find the
// format in codec->pix_fmts (avcodec_get_supported_config with
// AV_CODEC_CONFIG_PIX_FORMAT since n7.1), or, if the codec declares nothing at
// all, in the probed formats.
const std::vector<AVPixelFormat> codecPixelFormats =
encoderPixelFormats(codec, resolution, accel);
return codecPixelFormats | score | filterSuitablePixelFormats
| ranges::to<std::vector>();
}
}();
ranges::sort(scoredPixelFormats, [](const ScoredPixelFormat &a, const ScoredPixelFormat &b) {
return a.score > b.score;
});
return scoredPixelFormats;
}
std::optional<AVPixelFormat> findTargetFormat(AVPixelFormat sourceSWFormat, const Codec &codec,
const HWAccel *accel, QSize resolution)
{
using namespace QtMultimediaPrivate;
if (accel) {
const auto hwFormat = accel->hwFormat();
// TODO: handle codec->capabilities & AV_CODEC_CAP_HARDWARE here
if (!isHwFormatAcceptedByCodec(hwFormat))
return findTargetSWFormat(sourceSWFormat, codec, *accel, resolution);
const auto constraints = accel->constraints();
if (constraints && ranges::contains(makeSpan(constraints->valid_hw_formats), hwFormat))
return hwFormat;
// Some codecs, don't expose constraints,
// let's find the format in codec->pix_fmts (avcodec_get_supported_config with
// AV_CODEC_CONFIG_PIX_FORMAT since n7.1) and hw_config
if (isAVFormatSupported(codec, hwFormat))
return hwFormat;
}
const auto pixelFormats = encoderPixelFormats(codec, resolution, nullptr);
if (pixelFormats.empty()) {
qCWarning(qLcVideoEncoderUtils)
<< "Codec pix formats are undefined and probing" << codec.name()
<< "found none either, it's likely to behave incorrectly";
return sourceSWFormat;
}
auto swScoreCalculator = targetSwFormatScoreCalculator(sourceSWFormat);
return findBestAVValue(pixelFormats, swScoreCalculator);
}
AVScore findSWFormatScores(const Codec &codec, AVPixelFormat sourceSWFormat, QSize resolution)
{
const auto pixelFormats = encoderPixelFormats(codec, resolution, nullptr);
if (pixelFormats.empty())
// codecs that neither declare pixel formats nor accept any of the probed ones
// are suspicious
return MinAVScore;
auto formatScoreCalculator = targetSwFormatScoreCalculator(sourceSWFormat);
std::optional bestFormatWithScore =
findBestAVValueWithScore(pixelFormats, formatScoreCalculator);
if (bestFormatWithScore)
return bestFormatWithScore->score;
else
return MinAVScore;
}
AVRational adjustFrameRate(QSpan<const AVRational> supportedRates, qreal settingsRate,
qreal sourceRate)
{
qreal preferredRate = 0.;
if (settingsRate > 0)
preferredRate = settingsRate;
else if (sourceRate > 0)
preferredRate = sourceRate;
else if (supportedRates.empty())
preferredRate = 0.;
else
preferredRate = qreal(DefaultVideoFrameRate);
auto calcScore = [preferredRate](const AVRational &rate) {
// relative comparison
return qMin(preferredRate * rate.den, qreal(rate.num))
/ qMax(preferredRate * rate.den, qreal(rate.num));
};
const auto result = findBestAVValue(supportedRates, calcScore);
if (result && result->num && result->den)
return *result;
const auto [num, den] = qRealToFraction(preferredRate);
return { num, den };
}
AVRational adjustFrameTimeBase(QSpan<const AVRational> supportedRates, AVRational frameRate,
bool isFixedRate)
{
// TODO: user-specified frame rate might be required.
if (!supportedRates.empty()) {
auto hasFrameRate = [&]() {
for (AVRational rate : supportedRates)
if (rate.den == frameRate.den && rate.num == frameRate.num)
return true;
return false;
};
Q_ASSERT(hasFrameRate());
return { frameRate.den, frameRate.num };
}
if (isFixedRate)
return { frameRate.den, frameRate.num };
constexpr int TimeScaleFactor = 1000; // Allows not to follow fixed rate
return { frameRate.den, frameRate.num * TimeScaleFactor };
}
QSize adjustVideoResolution(const Codec &codec, QSize requestedResolution)
{
if constexpr (QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Windows) {
// TODO: investigate, there might be more encoders not supporting odd resolution
if (codec.name() == "h264_mf"_L1) {
auto makeEven = [](int size) { return size & ~1; };
return QSize(makeEven(requestedResolution.width()), makeEven(requestedResolution.height()));
}
}
return requestedResolution;
}
SwsFlags getScaleConversionType(const QSize &sourceSize, const QSize &targetSize)
{
SwsFlags conversionType = SWS_FAST_BILINEAR;
if constexpr (QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Android) {
// On Android, use SWS_BICUBIC for upscaling if least one dimension is upscaled
// to avoid a crash caused by ff_hcscale_fast_c with SWS_FAST_BILINEAR.
if (targetSize.width() > sourceSize.width() || targetSize.height() > sourceSize.height())
conversionType = SWS_BICUBIC;
}
return conversionType;
}
} // namespace QFFmpeg
QT_END_NAMESPACE