Revision control
Copy as Markdown
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtFFmpegMediaPluginImpl/private/qffmpegcodec_p.h>
#include <QtFFmpegMediaPluginImpl/private/qffmpeghwaccel_p.h>
#include <QtFFmpegMediaPluginImpl/private/qffmpegvideoencoderutils_p.h>
#include <QtTest/qtest.h>
#include <QtCore/qlist.h>
#include <QtCore/qobject.h>
#include <QtCore/qoperatingsystemversion.h>
extern "C" {
#include <libavutil/pixdesc.h>
}
#include <algorithm>
#include <optional>
using namespace QFFmpeg;
using namespace Qt::Literals;
namespace {
std::optional<Codec> findEncoder(AVCodecID codecId)
{
// Deliberately bypasses the codec storage, so that the test does not depend on its
// platform-specific filtering.
if (const AVCodec *codec = avcodec_find_encoder(codecId))
return Codec{ codec };
return {};
}
// Finds a video encoder that reports no pixel formats at all, i.e. one that needs probing.
// Such codecs exist only on some platforms (e.g. *_mediacodec on Android).
std::optional<Codec> findEncoderWithoutDeclaredPixelFormats()
{
for (const AVCodec *avCodec : CodecRange{}) {
if (avCodec->type != AVMEDIA_TYPE_VIDEO || !av_codec_is_encoder(avCodec))
continue;
const Codec codec{ avCodec };
if (codec.pixelFormats().empty())
return codec;
}
return {};
}
} // namespace
class tst_QFFmpegVideoEncoderUtils : public QObject
{
Q_OBJECT
private slots:
void getScaleConversionType_returnsCorrectConversionType_basedOnScaling();
void getScaleConversionType_returnsCorrectConversionType_basedOnScaling_data();
void adjustFrameRate_returnsExpectedRate_basedOnSettingsAndSource_data();
void adjustFrameRate_returnsExpectedRate_basedOnSettingsAndSource();
void adjustFrameTimeBase_returnsExpectedTimeBase_data();
void adjustFrameTimeBase_returnsExpectedTimeBase();
void scoreTargetSwFormat_matchesExpectedScore_whenSourceIsBgra();
void scoreTargetSwFormat_matchesExpectedScore_whenSourceIsBgra_data();
void scoreTargetSwFormat_matchesExpectedScore_whenSourceIsNV12();
void scoreTargetSwFormat_matchesExpectedScore_whenSourceIsNV12_data();
void scoreTargetSwFormat_matchesExpectedScore_whenSourceIsBgr0();
void scoreTargetSwFormat_matchesExpectedScore_whenSourceIsBgr0_data();
void scoreTargetSwFormat_prefersNonAlphaFormat_whenSourceIsOpaque();
void encoderPixelFormats_returnsDeclaredFormats_whenCodecDeclaresThem();
void encoderPixelFormats_returnsDeclaredFormats_whenCodecDeclaresThem_data();
void encoderPixelFormats_isMemoized_whenCodecIsProbed();
void encoderPixelFormats_cachesIndependently_whenResolutionDiffers();
void encoderPixelFormats_cachesIndependently_whenAccelPathDiffers();
void probePixelFormats_returnsSubsetOfDeclaredFormats();
void probePixelFormats_returnsSubsetOfDeclaredFormats_data();
void probePixelFormats_findsYuv420p_whenCodecIsMpeg4();
};
void tst_QFFmpegVideoEncoderUtils::getScaleConversionType_returnsCorrectConversionType_basedOnScaling_data()
{
QTest::addColumn<QSize>("sourceSize");
QTest::addColumn<QSize>("targetSize");
QTest::addColumn<SwsFlags>("expectedConversionType");
#ifdef Q_OS_ANDROID
SwsFlags expectedConversionTypeForUpscaling = SWS_BICUBIC;
#else
SwsFlags expectedConversionTypeForUpscaling = SWS_FAST_BILINEAR;
#endif
QTest::newRow("Sizes are equal")
<< QSize{ 800, 600 } << QSize{ 800, 600 } << SWS_FAST_BILINEAR;
QTest::newRow("Uniform downscaling")
<< QSize{ 800, 600 } << QSize{ 400, 300 } << SWS_FAST_BILINEAR;
QTest::newRow("Uniform upscaling")
<< QSize{ 400, 300 } << QSize{ 800, 600 } << expectedConversionTypeForUpscaling;
QTest::newRow("Anisotropic downscaling by width")
<< QSize{ 800, 600 } << QSize{ 400, 600 } << SWS_FAST_BILINEAR;
QTest::newRow("Anisotropic downscaling by height")
<< QSize{ 800, 600 } << QSize{ 800, 300 } << SWS_FAST_BILINEAR;
QTest::newRow("Anisotropic upscaling by width")
<< QSize{ 400, 300 } << QSize{ 800, 300 } << expectedConversionTypeForUpscaling;
QTest::newRow("Anisotropic upscaling by height")
<< QSize{ 400, 300 } << QSize{ 400, 600 } << expectedConversionTypeForUpscaling;
QTest::newRow("Anisotropic mixed scaling (width up, height down)")
<< QSize{ 400, 600 } << QSize{ 800, 300 } << expectedConversionTypeForUpscaling;
QTest::newRow("Anisotropic mixed scaling (width down, height up)")
<< QSize{ 800, 300 } << QSize{ 400, 600 } << expectedConversionTypeForUpscaling;
}
void tst_QFFmpegVideoEncoderUtils::getScaleConversionType_returnsCorrectConversionType_basedOnScaling()
{
// Arrange
QFETCH(QSize, sourceSize);
QFETCH(QSize, targetSize);
QFETCH(SwsFlags, expectedConversionType);
// Act
const SwsFlags actualConversionType = QFFmpeg::getScaleConversionType(sourceSize, targetSize);
// Assert
QCOMPARE(actualConversionType, expectedConversionType);
}
void tst_QFFmpegVideoEncoderUtils::
adjustFrameRate_returnsExpectedRate_basedOnSettingsAndSource_data()
{
QTest::addColumn<QList<AVRational>>("supportedRates");
QTest::addColumn<qreal>("settingsRate");
QTest::addColumn<qreal>("sourceRate");
QTest::addColumn<AVRational>("expectedRate");
const QList<AVRational> fixedOnlyRates = { { 15, 1 }, { 25, 1 }, { 30, 1 }, { 60, 1 } };
QTest::newRow("fixed-only codec, unset settings, variable source -> closest to 30")
<< fixedOnlyRates << qreal(-1.) << qreal(0.) << AVRational{ 30, 1 };
QTest::newRow("fixed-only codec, settings set, variable source -> closest to settings")
<< fixedOnlyRates << qreal(24.) << qreal(0.) << AVRational{ 25, 1 };
QTest::newRow("fixed-only codec, settings set, fixed source (different) -> settings wins")
<< fixedOnlyRates << qreal(60.) << qreal(15.) << AVRational{ 60, 1 };
QTest::newRow("fixed-only codec, unset settings, fixed source -> closest to source")
<< fixedOnlyRates << qreal(-1.) << qreal(26.) << AVRational{ 25, 1 };
QTest::newRow("variable-capable codec, unset settings -> stays variable")
<< QList<AVRational>{ } << qreal(-1.) << qreal(0.) << AVRational{ 0, 1 };
QTest::newRow("variable-capable codec, settings set -> fixed at settings rate")
<< QList<AVRational>{ } << qreal(24.) << qreal(0.) << AVRational{ 24, 1 };
QTest::newRow("variable-capable codec, unset settings, fixed source -> fixed at source rate")
<< QList<AVRational>{} << qreal(-1.) << qreal(30.) << AVRational{ 30, 1 };
}
void tst_QFFmpegVideoEncoderUtils::adjustFrameRate_returnsExpectedRate_basedOnSettingsAndSource()
{
// Arrange
QFETCH(QList<AVRational>, supportedRates);
QFETCH(qreal, settingsRate);
QFETCH(qreal, sourceRate);
QFETCH(AVRational, expectedRate);
// Act
const AVRational actualRate =
QFFmpeg::adjustFrameRate(supportedRates, settingsRate, sourceRate);
// Assert
QCOMPARE(actualRate.num, expectedRate.num);
QCOMPARE(actualRate.den, expectedRate.den);
}
void tst_QFFmpegVideoEncoderUtils::adjustFrameTimeBase_returnsExpectedTimeBase_data()
{
QTest::addColumn<QList<AVRational>>("supportedRates");
QTest::addColumn<AVRational>("frameRate");
QTest::addColumn<bool>("isFixedRate");
QTest::addColumn<AVRational>("expectedTimeBase");
const QList<AVRational> fixedOnlyRates = { { 15, 1 }, { 25, 1 }, { 30, 1 }, { 60, 1 } };
QTest::newRow("fixed-only codec -> timebase is 1/framerate")
<< fixedOnlyRates << AVRational{ 30, 1 } << true << AVRational{ 1, 30 };
QTest::newRow("variable-capable codec, placeholder default rate, not fixed "
"-> timebase scaled up for pts precision")
<< QList<AVRational>{ } << AVRational{ 30, 1 } << false << AVRational{ 1, 30000 };
QTest::newRow("variable-capable codec, genuine fixed rate -> timebase is 1/framerate, "
"no scaling")
<< QList<AVRational>{ } << AVRational{ 125, 3 } << true << AVRational{ 3, 125 };
}
void tst_QFFmpegVideoEncoderUtils::adjustFrameTimeBase_returnsExpectedTimeBase()
{
// Arrange
QFETCH(QList<AVRational>, supportedRates);
QFETCH(AVRational, frameRate);
QFETCH(bool, isFixedRate);
QFETCH(AVRational, expectedTimeBase);
// Act
const AVRational actualTimeBase =
QFFmpeg::adjustFrameTimeBase(supportedRates, frameRate, isFixedRate);
// Assert
QCOMPARE(actualTimeBase.num, expectedTimeBase.num);
QCOMPARE(actualTimeBase.den, expectedTimeBase.den);
}
void tst_QFFmpegVideoEncoderUtils::scoreTargetSwFormat_matchesExpectedScore_whenSourceIsBgra_data()
{
using namespace QFFmpeg::Literals;
QTest::addColumn<AVPixelFormat>("targetFormat");
QTest::addColumn<AVScore>("expectedScore");
QTest::newRow("yuva444p") << AV_PIX_FMT_YUVA444P << 99_avscore;
QTest::newRow("yuv444p") << AV_PIX_FMT_YUV444P << -109_avscore;
QTest::newRow("yuva420p") << AV_PIX_FMT_YUVA420P << -110_avscore;
if constexpr (QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Android)
QTest::newRow("nv12") << AV_PIX_FMT_NV12 << -117_avscore;
else
QTest::newRow("nv12") << AV_PIX_FMT_NV12 << -118_avscore;
QTest::newRow("yuv420p") << AV_PIX_FMT_YUV420P << -118_avscore;
QTest::newRow("yuv444p16le") << AV_PIX_FMT_YUV444P16LE << -201_avscore;
#if LIBAVUTIL_VERSION_MAJOR >= 60 // requires FFmpeg 8
QTest::newRow("yuv444p10msble") << AV_PIX_FMT_YUV444P10MSBLE << -203_avscore;
#endif
QTest::newRow("p010le") << AV_PIX_FMT_P010LE << -215_avscore;
QTest::newRow("p016le") << AV_PIX_FMT_P016LE << -306_avscore;
QTest::newRow("bgra") << AV_PIX_FMT_BGRA << -891_avscore;
QTest::newRow("rgba") << AV_PIX_FMT_RGBA << -901_avscore;
QTest::newRow("bgr0") << AV_PIX_FMT_BGR0 << -1109_avscore;
QTest::newRow("rgb0") << AV_PIX_FMT_RGB0 << -1109_avscore;
QTest::newRow("gray0") << AV_PIX_FMT_GRAY8 << AVScore::NotSuitableAVScore;
QTest::newRow("monowhite") << AV_PIX_FMT_MONOWHITE << AVScore::NotSuitableAVScore;
}
void tst_QFFmpegVideoEncoderUtils::scoreTargetSwFormat_matchesExpectedScore_whenSourceIsBgra()
{
// Arrange
QFETCH(AVPixelFormat, targetFormat);
QFETCH(AVScore, expectedScore);
// Act
const AVScore actualScore = scoreTargetSwFormat(AV_PIX_FMT_BGRA, targetFormat);
// Assert
QCOMPARE(actualScore, expectedScore);
}
void tst_QFFmpegVideoEncoderUtils::scoreTargetSwFormat_matchesExpectedScore_whenSourceIsNV12_data()
{
using namespace QFFmpeg::Literals;
QTest::addColumn<AVPixelFormat>("targetFormat");
QTest::addColumn<AVScore>("expectedScore");
if constexpr (QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Android)
QTest::newRow("nv12") << AV_PIX_FMT_NV12 << 113_avscore;
else
QTest::newRow("nv12") << AV_PIX_FMT_NV12 << 112_avscore;
QTest::newRow("yuv420p") << AV_PIX_FMT_YUV420P << 102_avscore;
QTest::newRow("yuva420p") << AV_PIX_FMT_YUVA420P << 52_avscore;
#if LIBAVUTIL_VERSION_MAJOR >= 60 // requires FFmpeg 8
QTest::newRow("ayuv") << AV_PIX_FMT_AYUV << -51_avscore;
#endif
QTest::newRow("p010le") << AV_PIX_FMT_P010LE << -98_avscore;
QTest::newRow("rgba") << AV_PIX_FMT_RGBA << -1051_avscore;
QTest::newRow("gray0") << AV_PIX_FMT_GRAY8 << AVScore::NotSuitableAVScore;
QTest::newRow("monowhite") << AV_PIX_FMT_MONOWHITE << AVScore::NotSuitableAVScore;
}
void tst_QFFmpegVideoEncoderUtils::scoreTargetSwFormat_matchesExpectedScore_whenSourceIsNV12()
{
// Arrange
QFETCH(AVPixelFormat, targetFormat);
QFETCH(AVScore, expectedScore);
// Act
const AVScore actualScore = scoreTargetSwFormat(AV_PIX_FMT_NV12, targetFormat);
// Assert
QCOMPARE(actualScore, expectedScore);
}
void tst_QFFmpegVideoEncoderUtils::scoreTargetSwFormat_matchesExpectedScore_whenSourceIsBgr0_data()
{
using namespace QFFmpeg::Literals;
QTest::addColumn<AVPixelFormat>("targetFormat");
QTest::addColumn<AVScore>("expectedScore");
QTest::newRow("yuv444p") << AV_PIX_FMT_YUV444P << 99_avscore;
QTest::newRow("yuva444p") << AV_PIX_FMT_YUVA444P << 49_avscore;
QTest::newRow("yuv422p") << AV_PIX_FMT_YUV422P << -108_avscore;
if constexpr (QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Android)
QTest::newRow("nv12") << AV_PIX_FMT_NV12 << -109_avscore;
else
QTest::newRow("nv12") << AV_PIX_FMT_NV12 << -110_avscore;
QTest::newRow("yuv420p") << AV_PIX_FMT_YUV420P << -110_avscore;
QTest::newRow("yuva422p") << AV_PIX_FMT_YUVA422P << -158_avscore;
QTest::newRow("yuva420p") << AV_PIX_FMT_YUVA420P << -160_avscore;
QTest::newRow("yuv444p16le") << AV_PIX_FMT_YUV444P16LE << -201_avscore;
#if LIBAVUTIL_VERSION_MAJOR >= 60 // requires FFmpeg 8
QTest::newRow("yuv444p10msble") << AV_PIX_FMT_YUV444P10MSBLE << -101_avscore;
#endif
QTest::newRow("p010le") << AV_PIX_FMT_P010LE << -207_avscore;
QTest::newRow("p016le") << AV_PIX_FMT_P016LE << -98_avscore;
QTest::newRow("bgr0") << AV_PIX_FMT_BGR0 << -891_avscore;
QTest::newRow("rgb0") << AV_PIX_FMT_RGB0 << -901_avscore;
QTest::newRow("bgra") << AV_PIX_FMT_BGRA << -951_avscore;
QTest::newRow("rgba") << AV_PIX_FMT_RGBA << -951_avscore;
QTest::newRow("gray0") << AV_PIX_FMT_GRAY8 << AVScore::NotSuitableAVScore;
QTest::newRow("monowhite") << AV_PIX_FMT_MONOWHITE << AVScore::NotSuitableAVScore;
}
void tst_QFFmpegVideoEncoderUtils::scoreTargetSwFormat_matchesExpectedScore_whenSourceIsBgr0()
{
// Arrange
QFETCH(AVPixelFormat, targetFormat);
QFETCH(AVScore, expectedScore);
// Act
const AVScore actualScore = scoreTargetSwFormat(AV_PIX_FMT_BGR0, targetFormat);
// Assert
QCOMPARE(actualScore, expectedScore);
}
void tst_QFFmpegVideoEncoderUtils::scoreTargetSwFormat_prefersNonAlphaFormat_whenSourceIsOpaque()
{
const AVScore yuv444p = scoreTargetSwFormat(AV_PIX_FMT_BGR0, AV_PIX_FMT_YUV444P);
const AVScore yuva422p = scoreTargetSwFormat(AV_PIX_FMT_BGR0, AV_PIX_FMT_YUVA422P);
QVERIFY(yuv444p > yuva422p);
}
void tst_QFFmpegVideoEncoderUtils::encoderPixelFormats_returnsDeclaredFormats_whenCodecDeclaresThem_data()
{
QTest::addColumn<AVCodecID>("codecId");
QTest::newRow("mpeg4") << AV_CODEC_ID_MPEG4;
QTest::newRow("mpeg2video") << AV_CODEC_ID_MPEG2VIDEO;
QTest::newRow("mjpeg") << AV_CODEC_ID_MJPEG;
}
void tst_QFFmpegVideoEncoderUtils::encoderPixelFormats_returnsDeclaredFormats_whenCodecDeclaresThem()
{
// Arrange
QFETCH(AVCodecID, codecId);
const std::optional<Codec> codec = findEncoder(codecId);
if (!codec)
QSKIP("Encoder not available in this FFmpeg build");
const QSpan<const AVPixelFormat> declared = codec->pixelFormats();
if (declared.empty())
QSKIP("Encoder does not declare pixel formats, nothing to compare against");
// Act
const std::vector<AVPixelFormat> actual =
QFFmpeg::encoderPixelFormats(*codec, QSize(320, 240), nullptr);
// Assert: the declared formats are passed through verbatim, without probing.
QCOMPARE(actual, std::vector<AVPixelFormat>(declared.begin(), declared.end()));
}
void tst_QFFmpegVideoEncoderUtils::encoderPixelFormats_isMemoized_whenCodecIsProbed()
{
// Arrange
const std::optional<Codec> codec = findEncoderWithoutDeclaredPixelFormats();
if (!codec)
QSKIP("No video encoder without declared pixel formats in this FFmpeg build");
// Act
const std::vector<AVPixelFormat> first =
QFFmpeg::encoderPixelFormats(*codec, QSize(320, 240), nullptr);
const std::vector<AVPixelFormat> second =
QFFmpeg::encoderPixelFormats(*codec, QSize(320, 240), nullptr);
// Assert: the second call is served from the cache rather than probed again.
QCOMPARE(second, first);
}
void tst_QFFmpegVideoEncoderUtils::encoderPixelFormats_cachesIndependently_whenResolutionDiffers()
{
// Arrange
const std::optional<Codec> codec = findEncoderWithoutDeclaredPixelFormats();
if (!codec)
QSKIP("No video encoder without declared pixel formats in this FFmpeg build");
// Act: probe at the usual test resolution and at an odd, non-macroblock-aligned one.
const std::vector<AVPixelFormat> atSmallSize =
QFFmpeg::encoderPixelFormats(*codec, QSize(320, 240), nullptr);
const std::vector<AVPixelFormat> atOddSize =
QFFmpeg::encoderPixelFormats(*codec, QSize(1918, 1078), nullptr);
const std::vector<AVPixelFormat> atSmallSizeAgain =
QFFmpeg::encoderPixelFormats(*codec, QSize(320, 240), nullptr);
// Assert: results for a given resolution are stable, and probing a second, unrelated
// resolution does not disturb the cached result for the first.
QCOMPARE(atSmallSizeAgain, atSmallSize);
Q_UNUSED(atOddSize);
}
void tst_QFFmpegVideoEncoderUtils::encoderPixelFormats_cachesIndependently_whenAccelPathDiffers()
{
// Arrange
const std::optional<Codec> codec = findEncoderWithoutDeclaredPixelFormats();
if (!codec)
QSKIP("No video encoder without declared pixel formats in this FFmpeg build");
const QSpan<const AVHWDeviceType> deviceTypes = HWAccel::encodingDeviceTypes();
if (deviceTypes.empty())
QSKIP("No hw accel device types available in this test environment");
const HWAccelUPtr accel = HWAccel::create(deviceTypes[0]);
if (!accel)
QSKIP("Could not create a hw accel for this test environment");
const QSize resolution(320, 240);
// Act: probe the plain (no hw frames context) path and the hw-frames-context-backed path
// for the same codec and resolution.
const std::vector<AVPixelFormat> plainPath =
QFFmpeg::encoderPixelFormats(*codec, resolution, nullptr);
const std::vector<AVPixelFormat> hwPath =
QFFmpeg::encoderPixelFormats(*codec, resolution, accel.get());
const std::vector<AVPixelFormat> plainPathAgain =
QFFmpeg::encoderPixelFormats(*codec, resolution, nullptr);
// Assert: probing the hw-frames-context path does not disturb the cached plain-path result,
// i.e. the two paths are tracked independently rather than overwriting each other.
QCOMPARE(plainPathAgain, plainPath);
Q_UNUSED(hwPath);
}
void tst_QFFmpegVideoEncoderUtils::probePixelFormats_returnsSubsetOfDeclaredFormats_data()
{
encoderPixelFormats_returnsDeclaredFormats_whenCodecDeclaresThem_data();
}
void tst_QFFmpegVideoEncoderUtils::probePixelFormats_returnsSubsetOfDeclaredFormats()
{
// Arrange
QFETCH(AVCodecID, codecId);
const std::optional<Codec> codec = findEncoder(codecId);
if (!codec)
QSKIP("Encoder not available in this FFmpeg build");
const QSpan<const AVPixelFormat> declared = codec->pixelFormats();
if (declared.empty())
QSKIP("Encoder does not declare pixel formats, nothing to compare against");
// Act
const std::vector<AVPixelFormat> probed =
QFFmpeg::probePixelFormats(*codec, QSize(320, 240), nullptr);
// Assert: probing never reports a format the codec does not actually accept.
for (AVPixelFormat format : probed)
QVERIFY2(std::find(declared.begin(), declared.end(), format) != declared.end(),
qPrintable(u"Probed format %1 is not declared by the codec"_s.arg(
QLatin1StringView{ av_get_pix_fmt_name(format) })));
}
void tst_QFFmpegVideoEncoderUtils::probePixelFormats_findsYuv420p_whenCodecIsMpeg4()
{
// Arrange
const std::optional<Codec> codec = findEncoder(AV_CODEC_ID_MPEG4);
if (!codec)
QSKIP("mpeg4 encoder not available in this FFmpeg build");
// Act
const std::vector<AVPixelFormat> probed =
QFFmpeg::probePixelFormats(*codec, QSize(320, 240), nullptr);
// Assert: probing is not vacuously empty, it finds the format mpeg4 is known to accept.
QVERIFY(std::find(probed.begin(), probed.end(), AV_PIX_FMT_YUV420P) != probed.end());
}
QTEST_MAIN(tst_QFFmpegVideoEncoderUtils)
#include "tst_qffmpegvideoencoderutils.moc"