Revision control

++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtTest/qtest.h>
++ #include <QtMultimedia/qvideoframe.h>
++ #include <QtMultimedia/qvideoframeformat.h>
++ #include <QtMultimedia/private/qmultimediautils_p.h>
++ #include <QtCore/qdebug.h>
++
++ #ifdef Q_OS_APPLE
++ # include <QtMultimedia/private/qapple_utils_p.h>
++ # include <CoreAudioTypes/CoreAudioBaseTypes.h>
++ #endif
++
++ class tst_QMultimediaUtils : public QObject
++ {
++ Q_OBJECT
++
++ private slots:
++ void fraction_of_0();
++ void fraction_of_negative_1_5();
++ void fraction_of_1_5();
++ void fraction_of_30();
++ void fraction_of_29_97();
++ void fraction_of_lower_boundary();
++ void fraction_of_upper_boundary();
++
++ void qRotatedFrameSize_returnsSizeAccordinglyToRotation();
++
++ void qClosestSupportedResolution_returnsBestMatch_data();
++ void qClosestSupportedResolution_returnsBestMatch();
++
++ void qMediaFromUserInput_addsFilePrefix_whenCalledWithLocalFile();
++
++ void qGetRequiredSwapChainFormat_returnsSdr_whenMaxLuminanceIsBelowSdrThreshold_data();
++ void qGetRequiredSwapChainFormat_returnsSdr_whenMaxLuminanceIsBelowSdrThreshold();
++ void qGetRequiredSwapChainFormat_returnsHdr_whenMaxLuminanceIsBelowHdrThreshold_data();
++ void qGetRequiredSwapChainFormat_returnsHdr_whenMaxLuminanceIsBelowHdrThreshold();
++
++ void qShouldUpdateSwapChainFormat_returnsFalse_whenSwapChainIsNullPointer();
++
++ void qNormalizedFrameTransformation_normilizesInputTransformation_data();
++ void qNormalizedFrameTransformation_normilizesInputTransformation();
++
++ void qVideoRotationFromDegrees_basicValues_data();
++ void qVideoRotationFromDegrees_basicValues();
++
++ void qVideoTransformationFromMatrix_returnsNormalizedTransformation_whenInputIsValid_data();
++ void qVideoTransformationFromMatrix_returnsNormalizedTransformation_whenInputIsValid();
++
++ void qVideoTransformationFromMatrix_returnsNormalizedTransformation_whenInputIsInvalid_data();
++ void qVideoTransformationFromMatrix_returnsNormalizedTransformation_whenInputIsInvalid();
++
++ #ifdef Q_OS_APPLE
++ void QOSStatus_qDebug();
++ #endif
++ };
++
++ void tst_QMultimediaUtils::fraction_of_0()
++ {
++ auto [n, d] = qRealToFraction(0.);
++ QCOMPARE(n, 0);
++ QCOMPARE(d, 1);
++ }
++
++ void tst_QMultimediaUtils::fraction_of_negative_1_5()
++ {
++ auto [n, d] = qRealToFraction(-1.5);
++ QCOMPARE(double(n) / double(d), -1.5);
++ QCOMPARE(n, -3);
++ QCOMPARE(d, 2);
++ }
++
++ void tst_QMultimediaUtils::fraction_of_1_5()
++ {
++ auto [n, d] = qRealToFraction(1.5);
++ QCOMPARE(double(n) / double(d), 1.5);
++ QCOMPARE(n, 3);
++ QCOMPARE(d, 2);
++ }
++
++ void tst_QMultimediaUtils::fraction_of_30()
++ {
++ auto [n, d] = qRealToFraction(30.);
++ QCOMPARE(double(n) / double(d), 30.);
++ QCOMPARE(d, 1);
++ }
++
++ void tst_QMultimediaUtils::fraction_of_29_97()
++ {
++ auto [n, d] = qRealToFraction(29.97);
++ QCOMPARE(double(n) / double(d), 29.97);
++ }
++
++ void tst_QMultimediaUtils::fraction_of_lower_boundary()
++ {
++ double f = 0.000001;
++ auto [n, d] = qRealToFraction(f);
++ QVERIFY(double(n) / double(d) < f);
++ QVERIFY(double(n) / double(d) >= 0.);
++ }
++
++ void tst_QMultimediaUtils::fraction_of_upper_boundary()
++ {
++ double f = 0.999999;
++ auto [n, d] = qRealToFraction(f);
++ QVERIFY(double(n) / double(d) <= 1.);
++ QVERIFY(double(n) / double(d) > f);
++ }
++
++ void tst_QMultimediaUtils::qRotatedFrameSize_returnsSizeAccordinglyToRotation()
++ {
++ QCOMPARE(qRotatedFrameSize({ 10, 22 }, 0), QSize(10, 22));
++ QCOMPARE(qRotatedFrameSize({ 10, 23 }, -180), QSize(10, 23));
++ QCOMPARE(qRotatedFrameSize({ 10, 24 }, 180), QSize(10, 24));
++ QCOMPARE(qRotatedFrameSize({ 10, 25 }, 360), QSize(10, 25));
++ QCOMPARE(qRotatedFrameSize({ 11, 26 }, 540), QSize(11, 26));
++
++ QCOMPARE(qRotatedFrameSize({ 10, 22 }, -90), QSize(22, 10));
++ QCOMPARE(qRotatedFrameSize({ 10, 23 }, 90), QSize(23, 10));
++ QCOMPARE(qRotatedFrameSize({ 10, 24 }, 270), QSize(24, 10));
++ QCOMPARE(qRotatedFrameSize({ 10, 25 }, 450), QSize(25, 10));
++
++ QCOMPARE(qRotatedFrameSize({ 10, 22 }, QtVideo::Rotation::None), QSize(10, 22));
++ QCOMPARE(qRotatedFrameSize({ 10, 22 }, QtVideo::Rotation::Clockwise180), QSize(10, 22));
++
++ QCOMPARE(qRotatedFrameSize({ 11, 22 }, QtVideo::Rotation::Clockwise90), QSize(22, 11));
++ QCOMPARE(qRotatedFrameSize({ 11, 22 }, QtVideo::Rotation::Clockwise270), QSize(22, 11));
++ }
++
++ void tst_QMultimediaUtils::qClosestSupportedResolution_returnsBestMatch_data()
++ {
++ QTest::addColumn<QSize>("requested");
++ QTest::addColumn<QList<QSize>>("supportedResolutions");
++ QTest::addColumn<QSize>("expected");
++
++ QTest::newRow("exact match is returned")
++ << QSize(1280, 720) << QList<QSize>{ { 640, 480 }, { 1280, 720 }, { 1920, 1080 } }
++ << QSize(1280, 720);
++
++ QTest::newRow("empty requested resolution gives invalid size")
++ << QSize() << QList<QSize>{ { 640, 480 } } << QSize();
++
++ QTest::newRow("empty list of supported resolutions gives invalid size")
++ << QSize(640, 480) << QList<QSize>{} << QSize();
++
++ QTest::newRow("only invalid supported resolutions gives invalid size")
++ << QSize(640, 480) << QList<QSize>{ { 0, 0 }, { 640, 0 } } << QSize();
++
++ QTest::newRow("invalid supported resolutions are skipped")
++ << QSize(600, 400) << QList<QSize>{ { 0, 0 }, { 640, 480 } } << QSize(640, 480);
++
++ QTest::newRow("matching aspect ratio is preferred over matching pixel count")
++ << QSize(1600, 1200) << QList<QSize>{ { 1920, 1080 }, { 640, 480 } } << QSize(640, 480);
++
++ QTest::newRow("closest pixel count is used among equal aspect ratios")
++ << QSize(1000, 750)
++ << QList<QSize>{ { 640, 480 }, { 1280, 960 }, { 2048, 1536 } } << QSize(640, 480);
++
++ QTest::newRow("aspect ratios within tolerance compete on pixel count")
++ << QSize(3840, 2160) << QList<QSize>{ { 640, 360 }, { 4000, 2248 } }
++ << QSize(4000, 2248);
++
++ QTest::newRow("aspect ratios outside tolerance are rejected")
++ << QSize(3840, 2160) << QList<QSize>{ { 640, 360 }, { 4000, 3000 } } << QSize(640, 360);
++ }
++
++ void tst_QMultimediaUtils::qClosestSupportedResolution_returnsBestMatch()
++ {
++ QFETCH(const QSize, requested);
++ QFETCH(const QList<QSize>, supportedResolutions);
++ QFETCH(const QSize, expected);
++
++ QCOMPARE(qClosestSupportedResolution(requested, supportedResolutions), expected);
++ }
++
++ void tst_QMultimediaUtils::qMediaFromUserInput_addsFilePrefix_whenCalledWithLocalFile()
++ {
++ using namespace Qt::Literals;
++
++ QCOMPARE(qMediaFromUserInput(QUrl(u"/foo/bar/baz"_s)), QUrl(u"file:///foo/bar/baz"_s));
++ QCOMPARE(qMediaFromUserInput(QUrl(u"file:///foo/bar/baz"_s)), QUrl(u"file:///foo/bar/baz"_s));
++ QCOMPARE(qMediaFromUserInput(QUrl(u"http://foo/bar/baz"_s)), QUrl(u"http://foo/bar/baz"_s));
++
++ QCOMPARE(qMediaFromUserInput(QUrl(u"foo/bar/baz"_s)),
++ QUrl::fromLocalFile(QDir::currentPath() + u"/foo/bar/baz"_s));
++
++ #ifdef Q_OS_WIN
++ QCOMPARE(qMediaFromUserInput(QUrl(u"C:/foo/bar/baz"_s)), QUrl(u"file:///c:/foo/bar/baz"_s));
++ #else
++ QCOMPARE(qMediaFromUserInput(QUrl(u"C:/foo/bar/baz"_s)), QUrl(u"c:/foo/bar/baz"_s));
++ #endif
++ }
++
++ void tst_QMultimediaUtils::
++ qGetRequiredSwapChainFormat_returnsSdr_whenMaxLuminanceIsBelowSdrThreshold_data()
++ {
++ QTest::addColumn<float>("maxLuminance");
++
++ QTest::newRow("0") << 0.0f;
++ QTest::newRow("80") << 80.0f;
++ QTest::newRow("100") << 100.0f;
++ }
++
++ void tst_QMultimediaUtils::
++ qGetRequiredSwapChainFormat_returnsSdr_whenMaxLuminanceIsBelowSdrThreshold()
++ {
++ // Arrange
++ QFETCH(float, maxLuminance);
++
++ QVideoFrameFormat format;
++ format.setMaxLuminance(maxLuminance);
++
++ // Act
++ QRhiSwapChain::Format requiredSwapChainFormat = qGetRequiredSwapChainFormat(format);
++
++ // Assert
++ QCOMPARE(requiredSwapChainFormat, QRhiSwapChain::Format::SDR);
++ }
++
++ void tst_QMultimediaUtils::
++ qGetRequiredSwapChainFormat_returnsHdr_whenMaxLuminanceIsBelowHdrThreshold_data()
++ {
++ QTest::addColumn<float>("maxLuminance");
++
++ QTest::newRow("101") << 101.0f;
++ QTest::newRow("300") << 300.0f;
++ QTest::newRow("1600") << 1600.0f;
++ }
++
++ void tst_QMultimediaUtils::
++ qGetRequiredSwapChainFormat_returnsHdr_whenMaxLuminanceIsBelowHdrThreshold()
++ {
++ // Arrange
++ QVideoFrameFormat format;
++ format.setMaxLuminance(300.0f);
++
++ // Act
++ QRhiSwapChain::Format requiredSwapChainFormat = qGetRequiredSwapChainFormat(format);
++
++ // Assert
++ QCOMPARE(requiredSwapChainFormat, QRhiSwapChain::Format::HDRExtendedSrgbLinear);
++ }
++
++ void tst_QMultimediaUtils::qShouldUpdateSwapChainFormat_returnsFalse_whenSwapChainIsNullPointer()
++ {
++ // Arrange
++ QRhiSwapChain *swapChain = nullptr;
++ QRhiSwapChain::Format requiredSwapChainFormat = QRhiSwapChain::Format::SDR;
++
++ // Act
++ bool shouldUpdate = qShouldUpdateSwapChainFormat(swapChain, requiredSwapChainFormat);
++
++ // Assert
++ QCOMPARE(shouldUpdate, false);
++ }
++
++ void tst_QMultimediaUtils::qNormalizedFrameTransformation_normilizesInputTransformation_data()
++ {
++ QTest::addColumn<QtVideo::Rotation>("surfaceRotation");
++ QTest::addColumn<bool>("surfaceMirrored");
++ QTest::addColumn<QVideoFrameFormat::Direction>("scanLineDirection");
++
++ QTest::addColumn<QtVideo::Rotation>("frameRotation");
++ QTest::addColumn<bool>("frameMirrored");
++ QTest::addColumn<int>("additionalFrameRotation");
++
++ const auto rotations = { QtVideo::Rotation::None, QtVideo::Rotation::Clockwise90,
++ QtVideo::Rotation::Clockwise180, QtVideo::Rotation::Clockwise270 };
++ const auto scanLineDirections = { QVideoFrameFormat::TopToBottom,
++ QVideoFrameFormat::BottomToTop };
++
++ auto newRow = [](QtVideo::Rotation surfaceRotation, bool surfaceMirrored,
++ QVideoFrameFormat::Direction scanLineDirection,
++ QtVideo::Rotation frameRotation, bool frameMirrored, int additionalRotation) {
++ QString tag;
++ QDebug stream(&tag);
++ stream << "Surface transform:" << surfaceRotation;
++ if (surfaceMirrored)
++ stream << "mirrored";
++ if (scanLineDirection == QVideoFrameFormat::BottomToTop)
++ stream << "opposite_scanline";
++
++ stream << "Frame transform:" << frameRotation;
++ if (frameMirrored)
++ stream << "mirrored";
++ if (additionalRotation)
++ stream << "additionalRotation:" << additionalRotation;
++
++ QTest::newRow(tag.toLatin1().data())
++ << surfaceRotation << surfaceMirrored << scanLineDirection << frameRotation
++ << frameMirrored << additionalRotation;
++ };
++
++ for (QtVideo::Rotation surfaceRotation : rotations)
++ for (bool surfaceMirrored : { false, true })
++ for (QVideoFrameFormat::Direction scanLineDirection : scanLineDirections)
++ for (QtVideo::Rotation frameRotation : rotations)
++ for (bool frameMirrored : { false, true })
++ newRow(surfaceRotation, surfaceMirrored, scanLineDirection, frameRotation,
++ frameMirrored, 0);
++
++ for (int additionalRotation : { -3690, -3600, -90, 90, 1800, 1980 })
++ newRow(QtVideo::Rotation::Clockwise90, true, QVideoFrameFormat::TopToBottom,
++ QtVideo::Rotation::None, false, additionalRotation);
++ }
++
++ static void rotate(QTransform &transform, qreal clockwiseDegrees)
++ {
++ // QTransform::rotate rotates clockwise as long as the Y-axis points downwards
++ if (clockwiseDegrees != 0)
++ transform.rotate(clockwiseDegrees);
++ }
++
++ static void rotate(QTransform &transform, QtVideo::Rotation rotation)
++ {
++ rotate(transform, qToUnderlying(rotation));
++ }
++
++ static void xMirror(QTransform &transform, bool mirror)
++ {
++ if (mirror)
++ transform.scale(-1., 1.);
++ }
++
++ static void yMirror(QTransform &transform, bool mirror)
++ {
++ if (mirror)
++ transform.scale(1., -1.);
++ }
++
++ static QTransform makeTransformMatrix(QtVideo::Rotation surfaceRotation, bool surfaceMirrored,
++ QVideoFrameFormat::Direction scanLineDirection,
++ QtVideo::Rotation frameRotation, bool frameMirrored,
++ int additionalRotation)
++ {
++ QTransform result;
++
++ yMirror(result, scanLineDirection == QVideoFrameFormat::BottomToTop);
++ rotate(result, surfaceRotation);
++ xMirror(result, surfaceMirrored);
++ rotate(result, frameRotation);
++ xMirror(result, frameMirrored);
++ rotate(result, additionalRotation);
++
++ return result;
++ }
++
++ void tst_QMultimediaUtils::qNormalizedFrameTransformation_normilizesInputTransformation()
++ {
++ // Arrange
++ QFETCH(const QtVideo::Rotation, surfaceRotation);
++ QFETCH(const bool, surfaceMirrored);
++ QFETCH(const QVideoFrameFormat::Direction, scanLineDirection);
++
++ QFETCH(const QtVideo::Rotation, frameRotation);
++ QFETCH(const bool, frameMirrored);
++ QFETCH(const int, additionalFrameRotation);
++
++ QVideoFrameFormat format(QSize(4, 4), QVideoFrameFormat::Format_ARGB8888);
++ format.setRotation(surfaceRotation);
++ format.setMirrored(surfaceMirrored);
++ format.setScanLineDirection(scanLineDirection);
++
++ QVideoFrame frame(format);
++
++ frame.setRotation(frameRotation);
++ frame.setMirrored(frameMirrored);
++
++ // Act
++ const VideoTransformation actual =
++ qNormalizedFrameTransformation(frame, additionalFrameRotation);
++
++ const QTransform expectedTransform =
++ makeTransformMatrix(surfaceRotation, surfaceMirrored, scanLineDirection, frameRotation,
++ frameMirrored, additionalFrameRotation);
++
++ const VideoTransformationOpt expected = qVideoTransformationFromMatrix(expectedTransform);
++
++ QTEST_ASSERT(expected);
++
++ // Assert
++ QCOMPARE(actual, *expected);
++ }
++
++ void tst_QMultimediaUtils::qVideoRotationFromDegrees_basicValues_data()
++ {
++ QTest::addColumn<int>("inputDegrees");
++ QTest::addColumn<QtVideo::Rotation>("expectedOutput");
++
++ QTest::newRow("0") << 0 << QtVideo::Rotation::None;
++ QTest::newRow("90") << 90 << QtVideo::Rotation::Clockwise90;
++ QTest::newRow("180") << 180 << QtVideo::Rotation::Clockwise180;
++ QTest::newRow("270") << 270 << QtVideo::Rotation::Clockwise270;
++ QTest::newRow("360") << 360 << QtVideo::Rotation::None;
++ QTest::newRow("450") << 450 << QtVideo::Rotation::Clockwise90;
++ QTest::newRow("630") << 630 << QtVideo::Rotation::Clockwise270;
++ QTest::newRow("-90") << -90 << QtVideo::Rotation::Clockwise270;
++ QTest::newRow("-270") << -270 << QtVideo::Rotation::Clockwise90;
++ QTest::newRow("-450") << -450 << QtVideo::Rotation::Clockwise270;
++ QTest::newRow("1") << 1 << QtVideo::Rotation::None;
++ }
++
++ void tst_QMultimediaUtils::qVideoRotationFromDegrees_basicValues()
++ {
++ QFETCH(int, inputDegrees);
++ QFETCH(QtVideo::Rotation, expectedOutput);
++
++ auto actual = qVideoRotationFromDegrees(inputDegrees);
++
++ QCOMPARE(actual, expectedOutput);
++ }
++
++ void tst_QMultimediaUtils::
++ qVideoTransformationFromMatrix_returnsNormalizedTransformation_whenInputIsValid_data()
++ {
++ QTest::addColumn<QTransform>("matrix");
++ QTest::addColumn<VideoTransformation>("expected");
++
++ const auto clockwiseDegreesList = { -44.7, 0., 44., 45.01, 90., 180., 270., 359. };
++ const auto scales = { -20., -1., -0.1, 0.1, 1., 20. };
++
++ for (double xScale : scales)
++ for (double yScale : { 1., 100. })
++ for (double clockwiseDegrees : clockwiseDegreesList) {
++ const VideoTransformation transform{
++ QtVideo::Rotation(qRound(clockwiseDegrees / 90.) % 4 * 90), xScale < 0
++ };
++
++ const QString tag = QStringLiteral("clockwiseDegrees: %1; yScale: %2; yScale: %3")
++ .arg(QString::number(clockwiseDegrees),
++ QString::number(xScale), QString::number(yScale));
++ QTransform matrix;
++ rotate(matrix, clockwiseDegrees);
++ matrix.scale(xScale, yScale);
++
++ QTest::newRow(tag.toLocal8Bit().constData()) << matrix << transform;
++ }
++ }
++
++ void tst_QMultimediaUtils::
++ qVideoTransformationFromMatrix_returnsNormalizedTransformation_whenInputIsValid()
++ {
++ QFETCH(const QTransform, matrix);
++ QFETCH(const VideoTransformation, expected);
++
++ const VideoTransformationOpt actual = qVideoTransformationFromMatrix(matrix);
++
++ QVERIFY(actual);
++ QCOMPARE(*actual, expected);
++ }
++
++ void tst_QMultimediaUtils::
++ qVideoTransformationFromMatrix_returnsNormalizedTransformation_whenInputIsInvalid_data()
++ {
++ QTest::addColumn<QTransform>("matrix");
++
++ QTest::newRow("zeros") << QTransform(0., 0., 0., 0., 0., 0.);
++ QTest::newRow("xScale is zero") << QTransform(0., 1., 0., 1., 0., 0.);
++ QTest::newRow("yScale is zero") << QTransform(1., 0., 1., 0., 0., 0.);
++ QTest::newRow("sheared: m22 is zero") << QTransform(1., 1., 1., 0., 0., 0.);
++ QTest::newRow("sheared: m11 is zero") << QTransform(0., 1., 1., 1., 0., 0.);
++ QTest::newRow("sheared: m12 is zero") << QTransform(1., 0., 1., 1., 0., 0.);
++ QTest::newRow("sheared: m21 is zero") << QTransform(1., 1., 0., 1., 0., 0.);
++ QTest::newRow("sheared: ambiguous mirror") << QTransform(1., 1., 1., 1., 0., 0.);
++ }
++
++ void tst_QMultimediaUtils::
++ qVideoTransformationFromMatrix_returnsNormalizedTransformation_whenInputIsInvalid()
++ {
++ QFETCH(const QTransform, matrix);
++
++ const VideoTransformationOpt actual = qVideoTransformationFromMatrix(matrix);
++
++ QVERIFY(!actual);
++ }
++
++ #ifdef Q_OS_APPLE
++ void tst_QMultimediaUtils::QOSStatus_qDebug()
++ {
++ using namespace QtMultimediaPrivate;
++
++ QTest::ignoreMessage(QtMsgType::QtWarningMsg, "noErr");
++ qWarning() << QOSStatus(noErr);
++ QTest::ignoreMessage(QtMsgType::QtWarningMsg, "!pth");
++ qWarning() << QOSStatus(kAudio_BadFilePathError);
++
++ QTest::failOnWarning();
++ }
++ #endif
++
++ QTEST_MAIN(tst_QMultimediaUtils)
++
++ Q_DECLARE_METATYPE(QTransform)
++
++ #include "tst_qmultimediautils.moc"