Source code

Revision control

Copy as Markdown

// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "tst_gstreamer_backend.h"
#include <QtGstreamerMediaPluginImpl/private/qgst_debug_p.h>
#include <QtGstreamerMediaPluginImpl/private/qgst_discoverer_p.h>
#include <QtGstreamerMediaPluginImpl/private/qgst_handle_types_p.h>
#include <QtGstreamerMediaPluginImpl/private/qgst_p.h>
#include <QtGstreamerMediaPluginImpl/private/qgstpipeline_p.h>
#include <QtGstreamerMediaPluginImpl/private/qgstreamermetadata_p.h>
#include <QtTest/qtest.h>
#include <QtMultimedia/qmediaformat.h>
#include <QtMultimedia/private/qmultimediautils_p.h>
#include <QtGui/qimage.h>
#include <QtCore/qtemporaryfile.h>
#include <gst/gstversion.h>
#include <array>
#include <cstdlib>
#include <set>
#include <variant>
// NOLINTBEGIN(readability-convert-member-functions-to-static)
using namespace Qt::Literals;
namespace {
template <typename... Pairs>
QMediaMetaData makeQMediaMetaData(Pairs &&...pairs)
{
QMediaMetaData metadata;
auto addKeyValuePair = [&](auto &&pair) {
metadata.insert(pair.first, pair.second);
return;
};
(addKeyValuePair(pairs), ...);
return metadata;
}
QGString makeQGString(std::string_view str)
{
char *s = (char *)g_malloc(str.size() + 1);
snprintf(s, str.size() + 1, "%s", str.data());
return QGString{ s };
};
const bool validateBitRates = GST_CHECK_VERSION(1, 24, 0);
} // namespace
QGstTagListHandle tst_GStreamer::parseTagList(const char *str)
{
QGstTagListHandle tagList{
gst_tag_list_new_from_string(str),
QGstTagListHandle::HasRef,
};
return tagList;
}
QGstTagListHandle tst_GStreamer::parseTagList(const QByteArray &ba)
{
return parseTagList(ba.constData());
}
void tst_GStreamer::QGString_conversions()
{
QGString str = makeQGString("yada");
QCOMPARE(str.toQString(), u"yada"_s);
QCOMPARE(str.asStringView(), "yada"_L1);
QCOMPARE(str.asByteArrayView(), "yada"_ba);
}
void tst_GStreamer::QGString_transparentCompare()
{
QGString str = makeQGString("yada");
std::set<QByteArray, std::less<>> set;
set.emplace(str);
QVERIFY(set.find(str) != set.end());
}
void tst_GStreamer::qGstCasts_withElement()
{
QGstElement element = QGstElement::createFromFactory("identity", "myPipeline");
QVERIFY(element);
QVERIFY(!qIsGstObjectOfType<GstPipeline>(element.element()));
QVERIFY(!qIsGstObjectOfType<GstBin>(element.element()));
}
void tst_GStreamer::qGstCasts_withBin()
{
QGstBin bin = QGstBin::create("bin");
QVERIFY(bin);
QVERIFY(!qIsGstObjectOfType<GstPipeline>(bin.element()));
QVERIFY(qIsGstObjectOfType<GstBin>(bin.element()));
}
void tst_GStreamer::qGstCasts_withPipeline()
{
QGstPipeline pipeline = QGstPipeline::create("myPipeline");
QGstElement element{
qGstSafeCast<GstElement>(pipeline.pipeline()),
QGstElement::NeedsRef,
};
QVERIFY(element);
QVERIFY(qIsGstObjectOfType<GstPipeline>(element.element()));
QVERIFY(qIsGstObjectOfType<GstBin>(element.element()));
}
void tst_GStreamer::metadata_taglistToMetaData()
{
QGstTagListHandle tagList = parseTagList(R"(taglist, title="My Video", comment="yada")");
QMediaMetaData parsed = taglistToMetaData(tagList);
QCOMPARE(parsed.stringValue(QMediaMetaData::Title), u"My Video"_s);
QCOMPARE(parsed.stringValue(QMediaMetaData::Comment), u"yada"_s);
}
void tst_GStreamer::metadata_taglistToMetaData_extractsOrientation()
{
QFETCH(QByteArray, taglist);
QFETCH(QtVideo::Rotation, rotation);
QGstTagListHandle tagList = parseTagList(taglist);
QMediaMetaData parsed = taglistToMetaData(tagList);
QCOMPARE(parsed[QMediaMetaData::Orientation].value<QtVideo::Rotation>(), rotation);
}
void tst_GStreamer::metadata_taglistToMetaData_extractsOrientation_data()
{
QTest::addColumn<QByteArray>("taglist");
QTest::addColumn<QtVideo::Rotation>("rotation");
QTest::newRow("no rotation") << R"(taglist, title="My Video", comment="yada")"_ba
<< QtVideo::Rotation::None;
QTest::newRow("90 degree")
<< R"(taglist, title="My Video", comment="yada", image-orientation=(string)rotate-90)"_ba
<< QtVideo::Rotation::Clockwise90;
QTest::newRow("180 degree")
<< R"(taglist, title="My Video", comment="yada", image-orientation=(string)rotate-180)"_ba
<< QtVideo::Rotation::Clockwise180;
QTest::newRow("270 degree")
<< R"(taglist, title="My Video", comment="yada", image-orientation=(string)rotate-270)"_ba
<< QtVideo::Rotation::Clockwise270;
}
void tst_GStreamer::metadata_taglistToMetaData_extractsDuration()
{
QGstTagListHandle tagList = parseTagList(
R"__(taglist, video-codec=(string)"On2\ VP9", container-specific-track-id=(string)1, extended-comment=(string){ "ALPHA_MODE\=1", "HANDLER_NAME\=Apple\ Video\ Media\ Handler", "VENDOR_ID\=appl", "TIMECODE\=00:00:00:00", "DURATION\=00:00:00.400000000" }, encoder=(string)"Lavc59.37.100\ libvpx-vp9")__");
QMediaMetaData parsed = taglistToMetaData(tagList);
QCOMPARE(parsed[QMediaMetaData::Duration].value<int>(), 400);
}
void tst_GStreamer::metadata_taglistToMetaData_extractsLanguage()
{
QFETCH(QByteArray, tagListString);
QFETCH(QLocale::Language, language);
QGstTagListHandle tagList = parseTagList(tagListString);
QVERIFY(tagList);
QMediaMetaData parsed = taglistToMetaData(tagList);
QCOMPARE(parsed[QMediaMetaData::Language].value<QLocale::Language>(), language);
}
void tst_GStreamer::metadata_taglistToMetaData_extractsLanguage_data()
{
QTest::addColumn<QByteArray>("tagListString");
QTest::addColumn<QLocale::Language>("language");
QTest::newRow("english, en")
<< R"__(taglist, container-format=(string)Matroska, audio-codec=(string)"MPEG-4\ AAC", language-code=(string)en, container-specific-track-id=(string)5, encoder=(string)Lavf60.16.100, extended-comment=(string)"DURATION\=00:00:05.055000000")__"_ba
<< QLocale::Language::English;
QTest::newRow("spanish, es")
<< R"__(taglist, container-format=(string)Matroska, audio-codec=(string)"MPEG-4\ AAC", language-code=(string)es, container-specific-track-id=(string)5, encoder=(string)Lavf60.16.100, extended-comment=(string)"DURATION\=00:00:05.055000000")__"_ba
<< QLocale::Language::Spanish;
QTest::newRow("english, eng")
<< R"__(taglist, container-format=(string)Matroska, audio-codec=(string)"MPEG-4\ AAC", language-code=(string)eng, container-specific-track-id=(string)5, encoder=(string)Lavf60.16.100, extended-comment=(string)"DURATION\=00:00:05.055000000")__"_ba
<< QLocale::Language::English;
QTest::newRow("spanish, spa")
<< R"__(taglist, container-format=(string)Matroska, audio-codec=(string)"MPEG-4\ AAC", language-code=(string)spa, container-specific-track-id=(string)5, encoder=(string)Lavf60.16.100, extended-comment=(string)"DURATION\=00:00:05.055000000")__"_ba
<< QLocale::Language::Spanish;
}
void tst_GStreamer::metadata_taglistToMetaData_extractsDate()
{
QFETCH(QByteArray, tagListString);
QFETCH(QDateTime, expectedDate);
QGstTagListHandle tagList = parseTagList(tagListString);
QVERIFY(tagList);
QMediaMetaData parsed = taglistToMetaData(tagList);
QCOMPARE(parsed[QMediaMetaData::Date].value<QDateTime>(), expectedDate);
}
void tst_GStreamer::metadata_taglistToMetaData_extractsDate_data()
{
QTest::addColumn<QByteArray>("tagListString");
QTest::addColumn<QDateTime>("expectedDate");
QTest::newRow("datetime") << R"__(taglist, datetime=(datetime)2024)__"_ba
<< QDateTime(QDate(2024, 0, 0), QTime{});
QTest::newRow("date") << R"__(taglist, date=(date)2024-01-01)__"_ba
<< QDateTime(QDate(2024, 1, 1), QTime{});
QTest::newRow("date and datetime")
<< R"__(taglist, datetime=(datetime)2024, date=(date)2024-01-01)__"_ba
<< QDateTime(QDate(2024, 1, 1), QTime{});
}
void tst_GStreamer::metadata_capsToMetaData()
{
QFETCH(QByteArray, capsString);
QFETCH(QMediaMetaData, expectedMetadata);
QGstCaps caps{
gst_caps_from_string(capsString.constData()),
QGstCaps::HasRef,
};
QMediaMetaData md = capsToMetaData(caps);
QCOMPARE(md, expectedMetadata);
}
void tst_GStreamer::metadata_capsToMetaData_data()
{
using Key = QMediaMetaData::Key;
using KVPair = std::pair<QMediaMetaData::Key, QVariant>;
auto makeKVPair = [](Key key, auto value) {
return KVPair{
key,
QVariant::fromValue(value),
};
};
QTest::addColumn<QByteArray>("capsString");
QTest::addColumn<QMediaMetaData>("expectedMetadata");
QTest::newRow("container") << R"(video/quicktime, variant=(string)iso)"_ba
<< makeQMediaMetaData(makeKVPair(Key::FileFormat,
QMediaFormat::FileFormat::MPEG4));
QTest::newRow("video")
<< R"(video/x-h264, stream-format=(string)avc, alignment=(string)au, level=(string)3.1, profile=(string)main, codec_data=(buffer)014d401fffe10017674d401fda014016ec0440000003004000000c83c60ca801000468ef3c80, width=(int)1280, height=(int)720, framerate=(fraction)25/1, pixel-aspect-ratio=(fraction)1/1)"_ba
<< makeQMediaMetaData(makeKVPair(Key::VideoCodec, QMediaFormat::VideoCodec::H264),
makeKVPair(Key::VideoFrameRate, 25),
makeKVPair(Key::Resolution, QSize(1280, 720)));
QTest::newRow("audio")
<< R"(audio/mpeg, mpegversion=(int)4, framed=(boolean)true, stream-format=(string)raw, level=(string)4, base-profile=(string)lc, profile=(string)lc, codec_data=(buffer)11b0, rate=(int)48000, channels=(int)6)"_ba
<< makeQMediaMetaData(makeKVPair(Key::AudioCodec, QMediaFormat::AudioCodec::AAC));
}
void tst_GStreamer::parseRotationTag_returnsCorrectResults()
{
QCOMPARE_EQ(parseRotationTag("rotate-0"), (RotationResult{ QtVideo::Rotation::None, false }));
QCOMPARE_EQ(parseRotationTag("rotate-90"),
(RotationResult{ QtVideo::Rotation::Clockwise90, false }));
QCOMPARE_EQ(parseRotationTag("rotate-180"),
(RotationResult{ QtVideo::Rotation::Clockwise180, false }));
QCOMPARE_EQ(parseRotationTag("rotate-270"),
(RotationResult{ QtVideo::Rotation::Clockwise270, false }));
QCOMPARE_EQ(parseRotationTag("flip-rotate-0"),
(RotationResult{ QtVideo::Rotation::Clockwise180, true }));
QCOMPARE_EQ(parseRotationTag("flip-rotate-90"),
(RotationResult{ QtVideo::Rotation::Clockwise270, true }));
QCOMPARE_EQ(parseRotationTag("flip-rotate-180"),
(RotationResult{ QtVideo::Rotation::None, true }));
QCOMPARE_EQ(parseRotationTag("flip-rotate-270"),
(RotationResult{ QtVideo::Rotation::Clockwise90, true }));
}
void tst_GStreamer::parseImage_returnsCorrectResults()
{
QFETCH(QByteArray, mimeType);
QFETCH(QByteArray, payload);
QFETCH(bool, expectedNull);
QFETCH(QSize, expectedSize);
gpointer copy = g_memdup2(reinterpret_cast<const guint8 *>(payload.constData()),
payload.size());
QGstBufferHandle buffer{
gst_buffer_new_wrapped(copy, payload.size()),
QGstBufferHandle::HasRef,
};
QGstCaps caps = mimeType.isEmpty()
? QGstCaps{ gst_caps_new_empty(), QGstCaps::HasRef }
: QGstCaps{ gst_caps_new_empty_simple(mimeType.constData()), QGstCaps::HasRef };
// gst_sample_new() takes its own references on buffer/caps, so the handles above
// still own theirs and release them at scope exit (see fuzz_qgstreamerparseimage.cpp
// for the same pattern).
GstSample *sample = gst_sample_new(buffer.get(), caps.caps(), nullptr, nullptr);
GValue val = G_VALUE_INIT;
g_value_init(&val, GST_TYPE_SAMPLE);
// Transfers ownership of "sample" into the GValue; do not unref sample separately.
g_value_take_boxed(&val, sample);
QImage image = parseImage(val);
QCOMPARE_EQ(image.isNull(), expectedNull);
if (!expectedNull)
QCOMPARE_EQ(image.size(), expectedSize);
g_value_unset(&val);
}
void tst_GStreamer::parseImage_returnsCorrectResults_data()
{
// A minimal, valid 1x1 grayscale+alpha PNG, decode verified independently via
// QImage::fromData(). kUndecodablePng1x1 below is a different, commonly-quoted "smallest
// PNG" byte sequence that looks plausible (correct signature/IHDR) but fails to decode
// ("libpng error: Not enough image data") -- reused as the undecodable-bytes test case.
static constexpr std::array<unsigned char, 68> kValidPng1x1 = {
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x04, 0x00, 0x00, 0x00, 0xb5,
0x1c, 0x0c, 0x02, 0x00, 0x00, 0x00, 0x0b, 0x49, 0x44, 0x41, 0x54, 0x78, 0xda, 0x63, 0x64,
0xf8, 0x0f, 0x00, 0x01, 0x05, 0x01, 0x01, 0x27, 0x18, 0xe3, 0x66, 0x00, 0x00, 0x00, 0x00,
0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
};
static constexpr std::array<unsigned char, 69> kUndecodablePng1x1 = {
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F,
0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8,
0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, 0xB0, 0x00, 0x00, 0x00,
0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
};
const QByteArray validPng(reinterpret_cast<const char *>(kValidPng1x1.data()),
kValidPng1x1.size());
const QByteArray undecodablePng(reinterpret_cast<const char *>(kUndecodablePng1x1.data()),
kUndecodablePng1x1.size());
QTest::addColumn<QByteArray>("mimeType");
QTest::addColumn<QByteArray>("payload");
QTest::addColumn<bool>("expectedNull");
QTest::addColumn<QSize>("expectedSize");
QTest::newRow("valid png") << "image/png"_ba << validPng << false << QSize(1, 1);
QTest::newRow("non-image mime type") << "audio/x-raw"_ba << validPng << true << QSize();
QTest::newRow("empty caps") << QByteArray() << validPng << true << QSize();
QTest::newRow("undecodable image bytes")
<< "image/png"_ba << undecodablePng << true << QSize();
}
void tst_GStreamer::QGstBin_createFromPipelineDescription()
{
QGstBin bin = QGstBin::createFromPipelineDescription("identity name=foo ! identity name=bar");
QVERIFY(bin);
QVERIFY(bin.findByName("foo"));
QCOMPARE_EQ(bin.findByName("foo").getParent(), bin);
QVERIFY(bin.findByName("bar"));
QVERIFY(!bin.findByName("baz"));
bin.dumpGraph("QGstBin_createFromPipelineDescription");
}
void tst_GStreamer::QGstElement_createFromPipelineDescription()
{
using namespace std::string_view_literals;
QGstElement element = QGstElement::createFromPipelineDescription("identity name=foo");
QCOMPARE_EQ(element.name(), "foo"sv);
QCOMPARE_EQ(element.typeName(), "GstIdentity"sv);
}
void tst_GStreamer::QGstElement_createFromPipelineDescription_multipleElementsCreatesBin()
{
using namespace std::string_view_literals;
QGstElement element =
QGstElement::createFromPipelineDescription("identity name=foo ! identity name=bar");
QVERIFY(element);
QCOMPARE_EQ(element.typeName(), "GstPipeline"sv);
QGstBin bin{
qGstSafeCast<GstBin>(element.element()),
QGstBin::NeedsRef,
};
QVERIFY(bin);
QVERIFY(bin.findByName("foo"));
QCOMPARE_EQ(bin.findByName("foo").getParent(), bin);
QVERIFY(bin.findByName("bar"));
QVERIFY(!bin.findByName("baz"));
bin.dumpGraph("QGstElement_createFromPipelineDescription_multipleElements");
}
void tst_GStreamer::QGstPad_inferTypeFromName()
{
auto makePad = [](const char *name, GstPadDirection direction) {
return QGstPad{
gst_pad_new(name, direction),
QGstPad::NeedsRef,
};
};
QVERIFY(makePad("audio_0", GST_PAD_SRC).inferTrackTypeFromName()
== QPlatformMediaPlayer::TrackType::AudioStream);
QVERIFY(makePad("video_0", GST_PAD_SRC).inferTrackTypeFromName()
== QPlatformMediaPlayer::TrackType::VideoStream);
QVERIFY(makePad("text_0", GST_PAD_SRC).inferTrackTypeFromName()
== QPlatformMediaPlayer::TrackType::SubtitleStream);
QVERIFY(makePad("src_0", GST_PAD_SRC).inferTrackTypeFromName() == std::nullopt);
QVERIFY(makePad("text", GST_PAD_SRC).inferTrackTypeFromName() == std::nullopt);
}
void tst_GStreamer::qDebug_GstPadDirection()
{
auto validate = [](GstPadDirection direction, QString expectedString) {
QString str;
QDebug dbg(&str);
dbg << direction;
QCOMPARE_EQ(str, expectedString);
};
validate(GST_PAD_UNKNOWN, u"GST_PAD_UNKNOWN "_s);
validate(GST_PAD_SRC, u"GST_PAD_SRC "_s);
validate(GST_PAD_SINK, u"GST_PAD_SINK "_s);
}
void tst_GStreamer::qDebug_GstStreamStatusType()
{
auto validate = [](GstStreamStatusType type, QString expectedString) {
QString str;
QDebug dbg(&str);
dbg << type;
QCOMPARE_EQ(str, expectedString);
};
validate(GST_STREAM_STATUS_TYPE_CREATE, u"GST_STREAM_STATUS_TYPE_CREATE "_s);
validate(GST_STREAM_STATUS_TYPE_ENTER, u"GST_STREAM_STATUS_TYPE_ENTER "_s);
validate(GST_STREAM_STATUS_TYPE_LEAVE, u"GST_STREAM_STATUS_TYPE_LEAVE "_s);
validate(GST_STREAM_STATUS_TYPE_DESTROY, u"GST_STREAM_STATUS_TYPE_DESTROY "_s);
validate(GST_STREAM_STATUS_TYPE_START, u"GST_STREAM_STATUS_TYPE_START "_s);
validate(GST_STREAM_STATUS_TYPE_PAUSE, u"GST_STREAM_STATUS_TYPE_PAUSE "_s);
validate(GST_STREAM_STATUS_TYPE_STOP, u"GST_STREAM_STATUS_TYPE_STOP "_s);
}
void tst_GStreamer::QGstStructureView_parseCameraFormat()
{
auto makeStructure = [](const char *str) {
return QUniqueGstStructureHandle{
gst_structure_new_from_string(str),
};
};
auto compareFraction = [](std::optional<Fraction> lhs, Fraction rhs) {
if (!lhs)
return false;
return std::tie(lhs->numerator, lhs->denominator)
== std::tie(rhs.numerator, rhs.denominator);
};
// single frame rate (taken from Logitech Brio 300)
{
auto structure = makeStructure(
R"__(video/x-raw, format=(string)YUY2, width=(int)1920, height=(int)1080, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction)5/1)__");
Fraction expectedFracion{ 1, 1 };
QGRange<float> expectedFramerateRange{ 5, 5 };
QCOMPARE(QGstStructureView(structure).resolution(), QSize(1920, 1080));
QVERIFY(compareFraction(QGstStructureView(structure).pixelAspectRatio(), expectedFracion));
QCOMPARE(QGstStructureView(structure).frameRateRange(), expectedFramerateRange);
QCOMPARE(QGstStructureView(structure).pixelFormats(),
QList{QVideoFrameFormat::PixelFormat::Format_YUYV});
}
// multiple frame rates (taken from Logitech Brio 300)
{
auto structure = makeStructure(
R"__(video/x-raw, format=(string)YUY2, width=(int)640, height=(int)480, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 })__");
Fraction expectedFracion{ 1, 1 };
QGRange<float> expectedFramerateRange{ 5, 30 };
QCOMPARE(QGstStructureView(structure).resolution(), QSize(640, 480));
QVERIFY(compareFraction(QGstStructureView(structure).pixelAspectRatio(), expectedFracion));
QCOMPARE(QGstStructureView(structure).frameRateRange(), expectedFramerateRange);
QCOMPARE(QGstStructureView(structure).pixelFormats(),
QList{QVideoFrameFormat::PixelFormat::Format_YUYV});
}
// jpeg (taken from Logitech Brio 300)
{
auto structure = makeStructure(
R"__(image/jpeg, parsed=(boolean)true, width=(int)1920, height=(int)1080, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction){ 30/1, 24/1, 20/1, 15/1, 10/1, 15/2, 5/1 })__");
Fraction expectedFracion{ 1, 1 };
QGRange<float> expectedFramerateRange{ 5, 30 };
QCOMPARE(QGstStructureView(structure).resolution(), QSize(1920, 1080));
QVERIFY(compareFraction(QGstStructureView(structure).pixelAspectRatio(), expectedFracion));
QCOMPARE(QGstStructureView(structure).frameRateRange(), expectedFramerateRange);
QCOMPARE(QGstStructureView(structure).pixelFormats(),
QList{QVideoFrameFormat::PixelFormat::Format_Jpeg});
}
// steped frame rate, undefined frame rate (taken from Raspberry Pi 4, Camera Module v2)
{
QGRange<float> expectedFramerateRange{ 0.0, 2147483647.0 };
auto cameraFormat = makeStructure(
R"__(video/x-raw, format=(string)YUY2, width=(int)[ 64, 16384, 2 ], height=(int)[ 64, 16384, 2 ], framerate=(fraction)[ 0/1, 2147483647/1 ])__");
QCOMPARE(QGstStructureView(cameraFormat).pixelAspectRatio(), std::nullopt);
QCOMPARE(QGstStructureView(cameraFormat).frameRateRange(), expectedFramerateRange);
QCOMPARE(QGstStructureView(cameraFormat).pixelFormats(),
QList{QVideoFrameFormat::PixelFormat::Format_YUYV});
}
// steped frame rate, valid rate range (taken from Raspberry Pi 4, Camera Module v2)
{
auto cameraFormat = makeStructure(
R"__(video/x-raw, format=(string)YUY2, width=(int)[ 32, 3280, 2 ], height=(int)[ 32, 2464, 2 ], framerate=(fraction)[ 1/1, 90/1 ])__");
QGRange<float> expectedFramerateRange{ 1.0, 90.0 };
QGRange<QSize> expectedResolutionRange{
QSize(32, 32),
QSize(3280, 2464),
};
QCOMPARE(QGstStructureView(cameraFormat).resolutionRange(), expectedResolutionRange);
QCOMPARE(QGstStructureView(cameraFormat).pixelAspectRatio(), std::nullopt);
QCOMPARE(QGstStructureView(cameraFormat).frameRateRange(), expectedFramerateRange);
QCOMPARE(QGstStructureView(cameraFormat).pixelFormats(),
QList{QVideoFrameFormat::PixelFormat::Format_YUYV});
}
}
using MediaSource = std::variant<QUrl, std::shared_ptr<QTemporaryFile>, std::shared_ptr<QIODevice>>;
void tst_GStreamer::QGstDiscoverer_discoverMedia()
{
using namespace Qt::Literals;
using namespace QGst;
using namespace QtMultimediaPrivate;
using namespace std::chrono_literals;
QFETCH(MediaSource, media);
QGstDiscoverer discoverer;
auto discoverUrl = [&](const QUrl &media) {
return discoverer.discover(media);
};
auto discoverFromFile = [&](const std::shared_ptr<QTemporaryFile> &media) {
return discoverer.discover(QUrl::fromLocalFile(media->fileName()));
};
auto discoverFromStream = [&](const std::shared_ptr<QIODevice> &media) {
return discoverer.discover(media.get());
};
auto result = std::visit(
qOverloadedVisitor{
discoverUrl,
discoverFromFile,
discoverFromStream,
},
media);
QVERIFY(result);
QVERIFY(!result->isLive);
QVERIFY(result->isSeekable);
QCOMPARE(result->videoStreams.size(), 1u);
QCOMPARE(result->audioStreams.size(), 1u);
QCOMPARE(result->duration, 1003000000ns);
using Key = QMediaMetaData::Key;
// container metadata
QMediaMetaData containerMetaData = toContainerMetadata(*result);
QCOMPARE(containerMetaData.value(Key::AlbumTitle), u"My Album"_s);
QCOMPARE(containerMetaData.value(Key::ContributingArtist), u"My Artist"_s);
QCOMPARE(containerMetaData.value(Key::Title), u"My Title"_s);
// QCOMPARE(containerMetaData.value(Key::Date), u"My Album"s);
// video metadata
QMediaMetaData videoStreamMetaData = toStreamMetadata(result->videoStreams[0]);
QCOMPARE(videoStreamMetaData.value(Key::Resolution), QSize(1920, 1080));
if (validateBitRates) {
constexpr int expectedVideoBitRate = 30029;
constexpr int allowedVideoBitRateError = expectedVideoBitRate / 5;
const int actualVideoBitRate = videoStreamMetaData.value(Key::VideoBitRate).toInt();
QCOMPARE_LE(std::abs(actualVideoBitRate - expectedVideoBitRate), allowedVideoBitRateError);
}
QCOMPARE(videoStreamMetaData.value(Key::VideoFrameRate), 25);
QCOMPARE(videoStreamMetaData.value(Key::VideoCodec).value<QMediaFormat::VideoCodec>(),
QMediaFormat::VideoCodec::H265);
// audio metadata
QMediaMetaData audioStreamMetaData = toStreamMetadata(result->audioStreams[0]);
if (validateBitRates)
QCOMPARE(audioStreamMetaData.value(Key::AudioBitRate), 159554);
QCOMPARE(audioStreamMetaData.value(Key::Language), QLocale::Language::AnyLanguage);
}
void tst_GStreamer::QGstDiscoverer_discoverMedia_data()
{
QTest::addColumn<MediaSource>("media");
auto makeTemporaryFile = [] {
auto file = QFile(":/metadata_test_file.mp4");
return std::shared_ptr<QTemporaryFile>(QTemporaryFile::createNativeFile(file));
};
QTest::newRow("qrc") << MediaSource{
QUrl("qrc:/metadata_test_file.mp4"),
};
QTest::newRow("QIODevice") << MediaSource{
std::make_shared<QFile>(":/metadata_test_file.mp4"),
};
QTest::newRow("filesystem file") << MediaSource{
makeTemporaryFile(),
};
}
void tst_GStreamer::QGstDiscoverer_discoverMedia_withRotation()
{
using namespace QGst;
QGstDiscoverer discoverer;
auto result = discoverer.discover(QUrl("qrc:/color_matrix_90_deg_clockwise.mp4"));
QMediaMetaData videoStreamMetaData = toStreamMetadata(result->videoStreams[0]);
QCOMPARE(videoStreamMetaData.value(QMediaMetaData::Key::Orientation).value<QtVideo::Rotation>(),
QtVideo::Rotation::Clockwise90);
}
void tst_GStreamer::QGstDiscoverer_filtersOutVideoStream_whenStreamIdIsNull()
{
using namespace QGst;
QGstDiscoverer discoverer;
auto result = discoverer.discover(QUrl("qrc:/color_matrix_90_deg_clockwise.m2v"));
QCOMPARE_EQ(result->videoStreams.size(), 1u);
QVERIFY(!result->videoStreams[0].streamID.isNull());
}
QTEST_GUILESS_MAIN(tst_GStreamer)
#include "moc_tst_gstreamer_backend.cpp"