Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qgstreamermetadata.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5#include <QtMultimedia/qmediametadata.h>
6#include <QtMultimedia/qtvideo.h>
7#include <QtCore/qdebug.h>
8#include <QtMultimedia/private/qmultimedia_ranges_p.h>
9#include <QtCore/qdatetime.h>
10#include <QtCore/qlocale.h>
11#include <QtCore/qtimezone.h>
12#include <QtGui/qimage.h>
13#include <QtMultimedia/private/qmediametadata_p.h>
14
15#include <gst/gstversion.h>
16#include <common/qgst_handle_types_p.h>
17#include <common/qgstutils_p.h>
18#include <qgstreamerformatinfo_p.h>
19
21
22namespace ranges = QtMultimediaPrivate::ranges;
23using namespace Qt::Literals;
24
25RotationResult parseRotationTag(std::string_view tag)
26{
27 using namespace std::string_view_literals;
28 Q_ASSERT(!tag.empty());
29
30 if (tag[0] == 'r') {
31 if (tag == "rotate-90"sv)
32 return { QtVideo::Rotation::Clockwise90, false };
33 if (tag == "rotate-180"sv)
34 return { QtVideo::Rotation::Clockwise180, false };
35 if (tag == "rotate-270"sv)
36 return { QtVideo::Rotation::Clockwise270, false };
37 if (tag == "rotate-0"sv)
38 return { QtVideo::Rotation::None, false };
39 }
40 if (tag[0] == 'f') {
41 // To flip by horizontal axis is the same as to mirror by vertical axis
42 // and rotate by 180 degrees.
43
44 if (tag == "flip-rotate-90"sv)
45 return { QtVideo::Rotation::Clockwise270, true };
46 if (tag == "flip-rotate-180"sv)
47 return { QtVideo::Rotation::None, true };
48 if (tag == "flip-rotate-270"sv)
49 return { QtVideo::Rotation::Clockwise90, true };
50 if (tag == "flip-rotate-0"sv)
51 return { QtVideo::Rotation::Clockwise180, true };
52 }
53
54 qCritical() << "cannot parse orientation: {}" << tag;
55 return { QtVideo::Rotation::None, false };
56}
57
58QImage parseImage(const GValue &val)
59{
60 Q_ASSERT(G_VALUE_TYPE(&val) == GST_TYPE_SAMPLE);
61
62 GstSample *sample = (GstSample *)g_value_get_boxed(&val);
63 GstCaps *caps = gst_sample_get_caps(sample);
64 if (caps && !gst_caps_is_empty(caps)) {
65 GstStructure *structure = gst_caps_get_structure(caps, 0);
66 const gchar *name = gst_structure_get_name(structure);
67 if (QLatin1StringView(name).startsWith("image/"_L1)) {
68 GstBuffer *buffer = gst_sample_get_buffer(sample);
69 if (buffer) {
70 GstMapInfo info;
71 gst_buffer_map(buffer, &info, GST_MAP_READ);
72 QImage image = QImage::fromData(info.data, info.size, name);
73 gst_buffer_unmap(buffer, &info);
74 return image;
75 }
76 }
77 }
78
79 return {};
80}
81
82namespace {
83
85
86// note: libstdc++ 11 has incomplete support for ranges (missing owning_view, P2415)
87#if defined(__cpp_lib_constexpr_algorithms) && defined(__cpp_lib_ranges)
88 && (!defined(_GLIBCXX_RELEASE) || _GLIBCXX_RELEASE >= 12)
89# define constexpr_lookup constexpr
90#else
91# define constexpr_lookup /*constexpr*/
92#endif
93
94struct MetadataKeyValuePair
95{
96 const char *tag;
97 QMediaMetaData::Key key;
98};
99
100constexpr const char *toTag(const char *t)
101{
102 return t;
103}
104constexpr const char *toTag(const MetadataKeyValuePair &kv)
105{
106 return kv.tag;
107}
108
109constexpr QMediaMetaData::Key toKey(QMediaMetaData::Key k)
110{
111 return k;
112}
113constexpr QMediaMetaData::Key toKey(const MetadataKeyValuePair &kv)
114{
115 return kv.key;
116}
117
118constexpr auto compareByKey = [](const auto &lhs, const auto &rhs) {
119 return toKey(lhs) < toKey(rhs);
120};
121
122constexpr auto compareByTag = [](const auto &lhs, const auto &rhs) {
123 return std::strcmp(toTag(lhs), toTag(rhs)) < 0;
124};
125
126constexpr_lookup auto makeLookupTable()
127{
128 std::array<MetadataKeyValuePair, 21> lookupTable{ {
129 { GST_TAG_TITLE, QMediaMetaData::Title },
130 { GST_TAG_COMMENT, QMediaMetaData::Comment },
131 { GST_TAG_DESCRIPTION, QMediaMetaData::Description },
132 { GST_TAG_GENRE, QMediaMetaData::Genre },
133 { GST_TAG_DATE_TIME, QMediaMetaData::Date },
134 { GST_TAG_DATE, QMediaMetaData::Date },
135
136 { GST_TAG_LANGUAGE_CODE, QMediaMetaData::Language },
137
138 { GST_TAG_ORGANIZATION, QMediaMetaData::Publisher },
139 { GST_TAG_COPYRIGHT, QMediaMetaData::Copyright },
140
141 // Media
142 { GST_TAG_DURATION, QMediaMetaData::Duration },
143
144 // Audio
145 { GST_TAG_BITRATE, QMediaMetaData::AudioBitRate },
146 { GST_TAG_AUDIO_CODEC, QMediaMetaData::AudioCodec },
147
148 // Music
149 { GST_TAG_ALBUM, QMediaMetaData::AlbumTitle },
150 { GST_TAG_ALBUM_ARTIST, QMediaMetaData::AlbumArtist },
151 { GST_TAG_ARTIST, QMediaMetaData::ContributingArtist },
152 { GST_TAG_TRACK_NUMBER, QMediaMetaData::TrackNumber },
153
154 { GST_TAG_IMAGE, QMediaMetaData::CoverArtImage },
155
156 // Image/Video
157 { "resolution", QMediaMetaData::Resolution },
158 { GST_TAG_IMAGE_ORIENTATION, QMediaMetaData::Orientation },
159
160 // Video
161 { GST_TAG_VIDEO_CODEC, QMediaMetaData::VideoCodec },
162
163 // Movie
164 { GST_TAG_PERFORMER, QMediaMetaData::LeadPerformer },
165 } };
166
167 ranges::sort(lookupTable, [](const MetadataKeyValuePair &lhs, const MetadataKeyValuePair &rhs) {
168 return std::string_view(lhs.tag) < std::string_view(rhs.tag);
169 });
170 return lookupTable;
171}
172
173constexpr_lookup auto gstTagToMetaDataKey = makeLookupTable();
174constexpr_lookup auto metaDataKeyToGstTag = [] {
175 auto array = gstTagToMetaDataKey;
176 ranges::sort(array, compareByKey);
177 return array;
178}();
179
180} // namespace MetadataLookupImpl
181
182QMediaMetaData::Key tagToKey(const char *tag)
183{
184 if (tag == nullptr)
185 return QMediaMetaData::Key(-1);
186
187 using namespace MetadataLookupImpl;
188 auto foundIterator = ranges::lower_bound(gstTagToMetaDataKey, tag, compareByTag);
189 if (std::strcmp(foundIterator->tag, tag) == 0)
190 return foundIterator->key;
191
192 return QMediaMetaData::Key(-1);
193}
194
195const char *keyToTag(QMediaMetaData::Key key)
196{
197 using namespace MetadataLookupImpl;
198 auto foundIterator = ranges::lower_bound(metaDataKeyToGstTag, key, compareByKey);
199 if (foundIterator->key == key)
200 return foundIterator->tag;
201
202 return nullptr;
203}
204
205#undef constexpr_lookup
206
207QDateTime parseDate(const GDate *date)
208{
209 if (!g_date_valid(date))
210 return {};
211
212 int year = g_date_get_year(date);
213 int month = g_date_get_month(date);
214 int day = g_date_get_day(date);
215 return QDateTime(QDate(year, month, day), QTime());
216}
217
218QDateTime parseDate(const GValue &val)
219{
220 Q_ASSERT(G_VALUE_TYPE(&val) == G_TYPE_DATE);
221 const GDate *date = (const GDate *)g_value_get_boxed(&val);
222 return parseDate(date);
223}
224
225QDateTime parseDateTime(const GstDateTime *dateTime)
226{
227 int year = gst_date_time_has_year(dateTime) ? gst_date_time_get_year(dateTime) : 0;
228 int month = gst_date_time_has_month(dateTime) ? gst_date_time_get_month(dateTime) : 0;
229 int day = gst_date_time_has_day(dateTime) ? gst_date_time_get_day(dateTime) : 0;
230 int hour = 0;
231 int minute = 0;
232 int second = 0;
233 float tz = 0;
234 if (gst_date_time_has_time(dateTime)) {
235 hour = gst_date_time_get_hour(dateTime);
236 minute = gst_date_time_get_minute(dateTime);
237 second = gst_date_time_get_second(dateTime);
238 tz = gst_date_time_get_time_zone_offset(dateTime);
239 }
240 return QDateTime{
241 QDate(year, month, day),
242 QTime(hour, minute, second),
243 QTimeZone(tz * 60 * 60),
244 };
245}
246
247QDateTime parseDateTime(const GValue &val)
248{
249 Q_ASSERT(G_VALUE_TYPE(&val) == GST_TYPE_DATE_TIME);
250 const GstDateTime *dateTime = (const GstDateTime *)g_value_get_boxed(&val);
251 return parseDateTime(dateTime);
252}
253
254std::optional<double> parseFractionAsDouble(const GValue &val)
255{
256 Q_ASSERT(G_VALUE_TYPE(&val) == GST_TYPE_FRACTION);
257
258 int nom = gst_value_get_fraction_numerator(&val);
259 int denom = gst_value_get_fraction_denominator(&val);
260 if (denom == 0)
261 return std::nullopt;
262 return double(nom) / double(denom);
263}
264
265constexpr std::string_view extendedComment{ GST_TAG_EXTENDED_COMMENT };
266
267void addTagsFromExtendedComment(const GstTagList *list, const gchar *tag, QMediaMetaData &metadata)
268{
269 using namespace Qt::Literals;
270 assert(tag == extendedComment);
271
272 int entryCount = gst_tag_list_get_tag_size(list, tag);
273 for (int i = 0; i != entryCount; ++i) {
274 const GValue *value = gst_tag_list_get_value_index(list, tag, i);
275
276 const QLatin1StringView strValue{ g_value_get_string(value) };
277
278 auto equalIndex = strValue.indexOf(QLatin1StringView("="));
279 if (equalIndex == -1) {
280 qDebug() << "Cannot parse GST_TAG_EXTENDED_COMMENT entry: " << value;
281 continue;
282 }
283
284 const QLatin1StringView key = strValue.first(equalIndex);
285 const QLatin1StringView valueString = strValue.last(strValue.size() - equalIndex - 1);
286
287 if (key == "DURATION"_L1) {
288 QGstDateTimeHandle duration{
289 gst_date_time_new_from_iso8601_string(valueString.data()),
290 QGstDateTimeHandle::HasRef,
291 };
292
293 if (duration) {
294 using namespace std::chrono;
295
296 auto chronoDuration = hours(gst_date_time_get_hour(duration.get()))
297 + minutes(gst_date_time_get_minute(duration.get()))
298 + seconds(gst_date_time_get_second(duration.get()))
299 + microseconds(gst_date_time_get_microsecond(duration.get()));
300
301 metadata.insert(QMediaMetaData::Duration,
302 QVariant::fromValue(round<milliseconds>(chronoDuration).count()));
303 }
304 }
305 }
306}
307
308void addTagToMetaData(const GstTagList *list, const gchar *tag, void *userdata)
309{
310 QMediaMetaData &metadata = *reinterpret_cast<QMediaMetaData *>(userdata);
311
312 QMediaMetaData::Key key = tagToKey(tag);
313 if (key == QMediaMetaData::Key::Date)
314 return; // date/datetime are handled on a higher layer
315
316 if (key == QMediaMetaData::Key(-1)) {
317 if (tag == extendedComment)
318 addTagsFromExtendedComment(list, tag, metadata);
319
320 return;
321 }
322
323 GValue val{};
324 gst_tag_list_copy_value(&val, list, tag);
325
326 GType type = G_VALUE_TYPE(&val);
327
328 if (auto entryCount = gst_tag_list_get_tag_size(list, tag) != 0; entryCount != 1)
329 qWarning() << "addTagToMetaData: invaled entry count for" << tag << "-" << entryCount;
330
331 if (type == G_TYPE_STRING) {
332 const gchar *str_value = g_value_get_string(&val);
333
334 switch (key) {
335 case QMediaMetaData::Language: {
336 metadata.insert(key, QVariant::fromValue(QGstUtils::codeToLanguage(str_value)));
337 break;
338 }
339 case QMediaMetaData::Orientation: {
340 RotationResult result = parseRotationTag(str_value);
341 metadata.insert(key, QVariant::fromValue(result.rotation));
342 break;
343 }
344 default:
345 metadata.insert(key, QString::fromUtf8(str_value));
346 break;
347 };
348 } else if (type == G_TYPE_INT) {
349 metadata.insert(key, g_value_get_int(&val));
350 } else if (type == G_TYPE_UINT) {
351 metadata.insert(key, g_value_get_uint(&val));
352 } else if (type == G_TYPE_LONG) {
353 metadata.insert(key, qint64(g_value_get_long(&val)));
354 } else if (type == G_TYPE_BOOLEAN) {
355 metadata.insert(key, g_value_get_boolean(&val));
356 } else if (type == G_TYPE_CHAR) {
357 metadata.insert(key, g_value_get_schar(&val));
358 } else if (type == G_TYPE_DOUBLE) {
359 metadata.insert(key, g_value_get_double(&val));
360 } else if (type == G_TYPE_DATE) {
361 if (!metadata.keys().contains(key)) {
362 QDateTime date = parseDate(val);
363 if (date.isValid())
364 metadata.insert(key, date);
365 }
366 } else if (type == GST_TYPE_DATE_TIME) {
367 QDateTime date = parseDateTime(val);
368 if (date.isValid())
369 metadata.insert(key, parseDateTime(val));
370 } else if (type == GST_TYPE_SAMPLE) {
371 QImage image = parseImage(val);
372 Q_ASSERT(key == QMediaMetaData::CoverArtImage);
373 QtMultimediaPrivate::setCoverArtImage(metadata, image);
374 } else if (type == GST_TYPE_FRACTION) {
375 std::optional<double> fraction = parseFractionAsDouble(val);
376
377 if (fraction)
378 metadata.insert(key, *fraction);
379 }
380
381 g_value_unset(&val);
382}
383
384} // namespace
385
386QMediaMetaData taglistToMetaData(const QGstTagListHandle &handle)
387{
389 extendMetaDataFromTagList(m, handle);
390 return m;
391}
392
393void extendMetaDataFromTagList(QMediaMetaData &metadata, const QGstTagListHandle &handle)
394{
395 if (handle) {
396 // gstreamer has both GST_TAG_DATE_TIME and GST_TAG_DATE tags.
397 // if both are present, we use GST_TAG_DATE_TIME, else we fall back to GST_TAG_DATE
398
399 auto readDateTime = [&]() -> std::optional<QDateTime> {
400 GstDateTime *dateTimeHandle{};
401 gst_tag_list_get_date_time(handle.get(), GST_TAG_DATE_TIME, &dateTimeHandle);
402 if (dateTimeHandle) {
403 QDateTime ret = parseDateTime(dateTimeHandle);
404 gst_date_time_unref(dateTimeHandle);
405 if (ret.isValid())
406 return ret;
407 }
408 return std::nullopt;
409 };
410
411 auto readDate = [&]() -> std::optional<QDateTime> {
412 GDate *dateHandle{};
413 gst_tag_list_get_date(handle.get(), GST_TAG_DATE, &dateHandle);
414 if (dateHandle) {
415 QDateTime ret = parseDate(dateHandle);
416 g_date_free(dateHandle);
417 if (ret.isValid())
418 return ret;
419 }
420 return std::nullopt;
421 };
422
423 std::optional<QDateTime> date = readDateTime();
424 if (!date)
425 date = readDate();
426
427 if (date)
428 metadata.insert(QMediaMetaData::Key::Date, *date);
429
430 gst_tag_list_foreach(handle.get(), reinterpret_cast<GstTagForeachFunc>(&addTagToMetaData),
431 &metadata);
432 }
433}
434
435static void applyMetaDataToTagSetter(const QMediaMetaData &metadata, GstTagSetter *element)
436{
437 gst_tag_setter_reset_tags(element);
438
439 for (QMediaMetaData::Key key : metadata.keys()) {
440 const char *tagName = keyToTag(key);
441 if (!tagName)
442 continue;
443 const QVariant &tagValue = metadata.value(key);
444
445 auto setTag = [&](const auto &value) {
446 gst_tag_setter_add_tags(element, GST_TAG_MERGE_REPLACE, tagName, value, nullptr);
447 };
448
449 switch (tagValue.typeId()) {
450 case QMetaType::QString:
451 setTag(tagValue.toString().toUtf8().constData());
452 break;
453 case QMetaType::Int:
454 case QMetaType::LongLong:
455 setTag(tagValue.toInt());
456 break;
457 case QMetaType::Double:
458 setTag(tagValue.toDouble());
459 break;
460
461 case QMetaType::QDateTime: {
462 // tagName does not properly disambiguate between GST_TAG_DATE_TIME and
463 // GST_TAG_DATE, as both map to QMediaMetaData::Key::Date. so we set it accordingly to
464 // the QVariant.
465
466 QDateTime date = tagValue.toDateTime();
467
468 QGstGstDateTimeHandle dateTime{
469 gst_date_time_new(date.offsetFromUtc() / 60. / 60., date.date().year(),
470 date.date().month(), date.date().day(), date.time().hour(),
471 date.time().minute(), date.time().second()),
472 QGstGstDateTimeHandle::HasRef,
473 };
474
475 gst_tag_setter_add_tags(element, GST_TAG_MERGE_REPLACE, GST_TAG_DATE_TIME,
476 dateTime.get(), nullptr);
477 break;
478 }
479 case QMetaType::QDate: {
480 QDate date = tagValue.toDate();
481
482 QUniqueGDateHandle dateHandle{
483 g_date_new_dmy(date.day(), GDateMonth(date.month()), date.year()),
484 };
485
486 gst_tag_setter_add_tags(element, GST_TAG_MERGE_REPLACE, GST_TAG_DATE, dateHandle.get(),
487 nullptr);
488 break;
489 }
490 default: {
491 if (tagValue.typeId() == qMetaTypeId<QLocale::Language>()) {
492 QByteArray language = QLocale::languageToCode(tagValue.value<QLocale::Language>(),
493 QLocale::ISO639Part2)
494 .toUtf8();
495 setTag(language.constData());
496 }
497
498 break;
499 }
500 }
501 }
502}
503
504void applyMetaDataToTagSetter(const QMediaMetaData &metadata, const QGstElement &element)
505{
506 GstTagSetter *tagSetter = qGstSafeCast<GstTagSetter>(element.element());
507 if (tagSetter)
508 applyMetaDataToTagSetter(metadata, tagSetter);
509 else
510 qWarning() << "applyMetaDataToTagSetter failed: element not a GstTagSetter"
511 << element.name();
512}
513
514void applyMetaDataToTagSetter(const QMediaMetaData &metadata, const QGstBin &bin)
515{
516 GstIterator *elements = gst_bin_iterate_all_by_interface(bin.bin(), GST_TYPE_TAG_SETTER);
517 GValue item = {};
518
519 while (gst_iterator_next(elements, &item) == GST_ITERATOR_OK) {
520 GstElement *element = static_cast<GstElement *>(g_value_get_object(&item));
521 if (!element)
522 continue;
523
524 GstTagSetter *tagSetter = qGstSafeCast<GstTagSetter>(element);
525
526 if (tagSetter)
527 applyMetaDataToTagSetter(metadata, tagSetter);
528 }
529
530 gst_iterator_free(elements);
531}
532
533void extendMetaDataFromCaps(QMediaMetaData &metadata, const QGstCaps &caps)
534{
535 QGstStructureView structure = caps.at(0);
536
537 QMediaFormat::FileFormat fileFormat = QGstreamerFormatInfo::fileFormatForCaps(structure);
538 if (fileFormat != QMediaFormat::FileFormat::UnspecifiedFormat) {
539 // Container caps
540 metadata.insert(QMediaMetaData::FileFormat, fileFormat);
541 return;
542 }
543
544 QMediaFormat::AudioCodec audioCodec = QGstreamerFormatInfo::audioCodecForCaps(structure);
545 if (audioCodec != QMediaFormat::AudioCodec::Unspecified) {
546 // Audio stream caps
547 metadata.insert(QMediaMetaData::AudioCodec, QVariant::fromValue(audioCodec));
548 return;
549 }
550
551 QMediaFormat::VideoCodec videoCodec = QGstreamerFormatInfo::videoCodecForCaps(structure);
552 if (videoCodec != QMediaFormat::VideoCodec::Unspecified) {
553 // Video stream caps
554 metadata.insert(QMediaMetaData::VideoCodec, QVariant::fromValue(videoCodec));
555 std::optional<float> framerate = structure["framerate"].getFraction();
556 if (framerate)
557 metadata.insert(QMediaMetaData::VideoFrameRate, *framerate);
558
559 QSize resolution = structure.resolution();
560 if (resolution.isValid())
561 metadata.insert(QMediaMetaData::Resolution, resolution);
562 }
563}
564
566{
567 QMediaMetaData metadata;
568 extendMetaDataFromCaps(metadata, caps);
569 return metadata;
570}
571
572QT_END_NAMESPACE
\inmodule QtMultimedia
Combined button and popup list for selecting options.
RotationResult parseRotationTag(std::string_view tag)
void extendMetaDataFromTagList(QMediaMetaData &metadata, const QGstTagListHandle &handle)
QImage parseImage(const GValue &val)
QMediaMetaData taglistToMetaData(const QGstTagListHandle &handle)
QMediaMetaData capsToMetaData(const QGstCaps &caps)
#define constexpr_lookup
void applyMetaDataToTagSetter(const QMediaMetaData &metadata, const QGstElement &element)
void applyMetaDataToTagSetter(const QMediaMetaData &metadata, const QGstBin &bin)
static void applyMetaDataToTagSetter(const QMediaMetaData &metadata, GstTagSetter *element)
void extendMetaDataFromCaps(QMediaMetaData &metadata, const QGstCaps &caps)