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