Revision control
++ // 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"