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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtmultimedia/src/plugins/multimedia/gstreamer/common/qgstreamermetadata.cpp