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>
15#include <gst/gstversion.h>
16#include <common/qgst_handle_types_p.h>
17#include <common/qgstutils_p.h>
18#include <qgstreamerformatinfo_p.h>
21namespace ranges = QtMultimediaPrivate::ranges;
25 using namespace std::string_view_literals;
26 Q_ASSERT(!tag.empty());
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 };
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 };
52 qCritical() <<
"cannot parse orientation: {}" << tag;
53 return { QtVideo::Rotation::None,
false };
60#ifdef __cpp_lib_constexpr_algorithms
61# define constexpr_lookup constexpr
63# define constexpr_lookup
66struct MetadataKeyValuePair
69 QMediaMetaData::Key key;
72constexpr const char *toTag(
const char *t)
76constexpr const char *toTag(
const MetadataKeyValuePair &kv)
81constexpr QMediaMetaData::Key toKey(QMediaMetaData::Key k)
85constexpr QMediaMetaData::Key toKey(
const MetadataKeyValuePair &kv)
90constexpr auto compareByKey = [](
const auto &lhs,
const auto &rhs) {
91 return toKey(lhs) < toKey(rhs);
94constexpr auto compareByTag = [](
const auto &lhs,
const auto &rhs) {
95 return std::strcmp(toTag(lhs), toTag(rhs)) < 0;
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 },
108 { GST_TAG_LANGUAGE_CODE, QMediaMetaData::Language },
110 { GST_TAG_ORGANIZATION, QMediaMetaData::Publisher },
111 { GST_TAG_COPYRIGHT, QMediaMetaData::Copyright },
114 { GST_TAG_DURATION, QMediaMetaData::Duration },
117 { GST_TAG_BITRATE, QMediaMetaData::AudioBitRate },
118 { GST_TAG_AUDIO_CODEC, QMediaMetaData::AudioCodec },
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 },
126 { GST_TAG_IMAGE, QMediaMetaData::CoverArtImage },
129 {
"resolution", QMediaMetaData::Resolution },
130 { GST_TAG_IMAGE_ORIENTATION, QMediaMetaData::Orientation },
133 { GST_TAG_VIDEO_CODEC, QMediaMetaData::VideoCodec },
136 { GST_TAG_PERFORMER, QMediaMetaData::LeadPerformer },
139 ranges::sort(lookupTable, [](
const MetadataKeyValuePair &lhs,
const MetadataKeyValuePair &rhs) {
140 return std::string_view(lhs.tag) < std::string_view(rhs.tag);
147 auto array = gstTagToMetaDataKey;
148 ranges::sort(array, compareByKey);
154QMediaMetaData::Key tagToKey(
const char *tag)
157 return QMediaMetaData::Key(-1);
160 auto foundIterator = ranges::lower_bound(gstTagToMetaDataKey, tag, compareByTag);
161 if (std::strcmp(foundIterator->tag, tag) == 0)
162 return foundIterator->key;
164 return QMediaMetaData::Key(-1);
167const char *keyToTag(QMediaMetaData::Key key)
170 auto foundIterator = ranges::lower_bound(metaDataKeyToGstTag, key, compareByKey);
171 if (foundIterator->key == key)
172 return foundIterator->tag;
177#undef constexpr_lookup
179QDateTime parseDate(
const GDate *date)
181 if (!g_date_valid(date))
184 int year = g_date_get_year(date);
185 int month = g_date_get_month(date);
186 int day = g_date_get_day(date);
187 return QDateTime(QDate(year, month, day), QTime());
190QDateTime parseDate(
const GValue &val)
192 Q_ASSERT(G_VALUE_TYPE(&val) == G_TYPE_DATE);
193 const GDate *date = (
const GDate *)g_value_get_boxed(&val);
194 return parseDate(date);
197QDateTime parseDateTime(
const GstDateTime *dateTime)
199 int year = gst_date_time_has_year(dateTime) ? gst_date_time_get_year(dateTime) : 0;
200 int month = gst_date_time_has_month(dateTime) ? gst_date_time_get_month(dateTime) : 0;
201 int day = gst_date_time_has_day(dateTime) ? gst_date_time_get_day(dateTime) : 0;
206 if (gst_date_time_has_time(dateTime)) {
207 hour = gst_date_time_get_hour(dateTime);
208 minute = gst_date_time_get_minute(dateTime);
209 second = gst_date_time_get_second(dateTime);
210 tz = gst_date_time_get_time_zone_offset(dateTime);
213 QDate(year, month, day),
214 QTime(hour, minute, second),
215 QTimeZone(tz * 60 * 60),
219QDateTime parseDateTime(
const GValue &val)
221 Q_ASSERT(G_VALUE_TYPE(&val) == GST_TYPE_DATE_TIME);
222 const GstDateTime *dateTime = (
const GstDateTime *)g_value_get_boxed(&val);
223 return parseDateTime(dateTime);
226QImage parseImage(
const GValue &val)
228 Q_ASSERT(G_VALUE_TYPE(&val) == GST_TYPE_SAMPLE);
230 GstSample *sample = (GstSample *)g_value_get_boxed(&val);
231 GstCaps *caps = gst_sample_get_caps(sample);
232 if (caps && !gst_caps_is_empty(caps)) {
233 GstStructure *structure = gst_caps_get_structure(caps, 0);
234 const gchar *name = gst_structure_get_name(structure);
235 if (QByteArray(name).startsWith(
"image/")) {
236 GstBuffer *buffer = gst_sample_get_buffer(sample);
239 gst_buffer_map(buffer, &info, GST_MAP_READ);
240 QImage image = QImage::fromData(info.data, info.size, name);
241 gst_buffer_unmap(buffer, &info);
250std::optional<
double> parseFractionAsDouble(
const GValue &val)
252 Q_ASSERT(G_VALUE_TYPE(&val) == GST_TYPE_FRACTION);
254 int nom = gst_value_get_fraction_numerator(&val);
255 int denom = gst_value_get_fraction_denominator(&val);
258 return double(nom) /
double(denom);
261constexpr std::string_view extendedComment{ GST_TAG_EXTENDED_COMMENT };
263void addTagsFromExtendedComment(
const GstTagList *list,
const gchar *tag, QMediaMetaData &metadata)
265 using namespace Qt::Literals;
266 assert(tag == extendedComment);
268 int entryCount = gst_tag_list_get_tag_size(list, tag);
269 for (
int i = 0; i != entryCount; ++i) {
270 const GValue *value = gst_tag_list_get_value_index(list, tag, i);
272 const QLatin1StringView strValue{ g_value_get_string(value) };
274 auto equalIndex = strValue.indexOf(QLatin1StringView(
"="));
275 if (equalIndex == -1) {
276 qDebug() <<
"Cannot parse GST_TAG_EXTENDED_COMMENT entry: " << value;
280 const QLatin1StringView key = strValue.first(equalIndex);
281 const QLatin1StringView valueString = strValue.last(strValue.size() - equalIndex - 1);
283 if (key ==
"DURATION"_L1) {
284 QGstDateTimeHandle duration{
285 gst_date_time_new_from_iso8601_string(valueString.data()),
286 QGstDateTimeHandle::HasRef,
290 using namespace std::chrono;
292 auto chronoDuration = hours(gst_date_time_get_hour(duration.get()))
293 + minutes(gst_date_time_get_minute(duration.get()))
294 + seconds(gst_date_time_get_second(duration.get()))
295 + microseconds(gst_date_time_get_microsecond(duration.get()));
297 metadata.insert(QMediaMetaData::Duration,
298 QVariant::fromValue(round<milliseconds>(chronoDuration).count()));
304void addTagToMetaData(
const GstTagList *list,
const gchar *tag,
void *userdata)
306 QMediaMetaData &metadata = *
reinterpret_cast<QMediaMetaData *>(userdata);
308 QMediaMetaData::Key key = tagToKey(tag);
309 if (key == QMediaMetaData::Key::Date)
312 if (key == QMediaMetaData::Key(-1)) {
313 if (tag == extendedComment)
314 addTagsFromExtendedComment(list, tag, metadata);
320 gst_tag_list_copy_value(&val, list, tag);
322 GType type = G_VALUE_TYPE(&val);
324 if (
auto entryCount = gst_tag_list_get_tag_size(list, tag) != 0; entryCount != 1)
325 qWarning() <<
"addTagToMetaData: invaled entry count for" << tag <<
"-" << entryCount;
327 if (type == G_TYPE_STRING) {
328 const gchar *str_value = g_value_get_string(&val);
331 case QMediaMetaData::Language: {
332 metadata.insert(key, QVariant::fromValue(QGstUtils::codeToLanguage(str_value)));
335 case QMediaMetaData::Orientation: {
337 metadata.insert(key, QVariant::fromValue(result.rotation));
341 metadata.insert(key, QString::fromUtf8(str_value));
344 }
else if (type == G_TYPE_INT) {
345 metadata.insert(key, g_value_get_int(&val));
346 }
else if (type == G_TYPE_UINT) {
347 metadata.insert(key, g_value_get_uint(&val));
348 }
else if (type == G_TYPE_LONG) {
349 metadata.insert(key, qint64(g_value_get_long(&val)));
350 }
else if (type == G_TYPE_BOOLEAN) {
351 metadata.insert(key, g_value_get_boolean(&val));
352 }
else if (type == G_TYPE_CHAR) {
353 metadata.insert(key, g_value_get_schar(&val));
354 }
else if (type == G_TYPE_DOUBLE) {
355 metadata.insert(key, g_value_get_double(&val));
356 }
else if (type == G_TYPE_DATE) {
357 if (!metadata.keys().contains(key)) {
358 QDateTime date = parseDate(val);
360 metadata.insert(key, date);
362 }
else if (type == GST_TYPE_DATE_TIME) {
363 QDateTime date = parseDateTime(val);
365 metadata.insert(key, parseDateTime(val));
366 }
else if (type == GST_TYPE_SAMPLE) {
367 QImage image = parseImage(val);
368 Q_ASSERT(key == QMediaMetaData::CoverArtImage);
369 QtMultimediaPrivate::setCoverArtImage(metadata, image);
370 }
else if (type == GST_TYPE_FRACTION) {
371 std::optional<
double> fraction = parseFractionAsDouble(val);
374 metadata.insert(key, *fraction);
395 auto readDateTime = [&]() -> std::optional<QDateTime> {
396 GstDateTime *dateTimeHandle{};
397 gst_tag_list_get_date_time(handle.get(), GST_TAG_DATE_TIME, &dateTimeHandle);
398 if (dateTimeHandle) {
399 QDateTime ret = parseDateTime(dateTimeHandle);
400 gst_date_time_unref(dateTimeHandle);
407 auto readDate = [&]() -> std::optional<QDateTime> {
409 gst_tag_list_get_date(handle.get(), GST_TAG_DATE, &dateHandle);
411 QDateTime ret = parseDate(dateHandle);
412 g_date_free(dateHandle);
419 std::optional<QDateTime> date = readDateTime();
424 metadata.insert(QMediaMetaData::Key::Date, *date);
426 gst_tag_list_foreach(handle.get(),
reinterpret_cast<GstTagForeachFunc>(&addTagToMetaData),
433 gst_tag_setter_reset_tags(element);
435 for (QMediaMetaData::Key key : metadata.keys()) {
436 const char *tagName = keyToTag(key);
439 const QVariant &tagValue = metadata.value(key);
441 auto setTag = [&](
const auto &value) {
442 gst_tag_setter_add_tags(element, GST_TAG_MERGE_REPLACE, tagName, value,
nullptr);
445 switch (tagValue.typeId()) {
446 case QMetaType::QString:
447 setTag(tagValue.toString().toUtf8().constData());
450 case QMetaType::LongLong:
451 setTag(tagValue.toInt());
453 case QMetaType::Double:
454 setTag(tagValue.toDouble());
457 case QMetaType::QDateTime: {
462 QDateTime date = tagValue.toDateTime();
464 QGstGstDateTimeHandle dateTime{
465 gst_date_time_new(date.offsetFromUtc() / 60. / 60., date.date().year(),
466 date.date().month(), date.date().day(), date.time().hour(),
467 date.time().minute(), date.time().second()),
468 QGstGstDateTimeHandle::HasRef,
471 gst_tag_setter_add_tags(element, GST_TAG_MERGE_REPLACE, GST_TAG_DATE_TIME,
472 dateTime.get(),
nullptr);
475 case QMetaType::QDate: {
476 QDate date = tagValue.toDate();
478 QUniqueGDateHandle dateHandle{
479 g_date_new_dmy(date.day(), GDateMonth(date.month()), date.year()),
482 gst_tag_setter_add_tags(element, GST_TAG_MERGE_REPLACE, GST_TAG_DATE, dateHandle.get(),
487 if (tagValue.typeId() == qMetaTypeId<QLocale::Language>()) {
488 QByteArray language = QLocale::languageToCode(tagValue.value<QLocale::Language>(),
489 QLocale::ISO639Part2)
491 setTag(language.constData());
502 GstTagSetter *tagSetter = qGstSafeCast<GstTagSetter>(element.element());
504 applyMetaDataToTagSetter(metadata, tagSetter);
506 qWarning() <<
"applyMetaDataToTagSetter failed: element not a GstTagSetter"
512 GstIterator *elements = gst_bin_iterate_all_by_interface(bin.bin(), GST_TYPE_TAG_SETTER);
515 while (gst_iterator_next(elements, &item) == GST_ITERATOR_OK) {
516 GstElement *element =
static_cast<GstElement *>(g_value_get_object(&item));
520 GstTagSetter *tagSetter = qGstSafeCast<GstTagSetter>(element);
523 applyMetaDataToTagSetter(metadata, tagSetter);
526 gst_iterator_free(elements);
531 QGstStructureView structure = caps.at(0);
533 QMediaFormat::FileFormat fileFormat = QGstreamerFormatInfo::fileFormatForCaps(structure);
534 if (fileFormat != QMediaFormat::FileFormat::UnspecifiedFormat) {
536 metadata.insert(QMediaMetaData::FileFormat, fileFormat);
540 QMediaFormat::AudioCodec audioCodec = QGstreamerFormatInfo::audioCodecForCaps(structure);
541 if (audioCodec != QMediaFormat::AudioCodec::Unspecified) {
543 metadata.insert(QMediaMetaData::AudioCodec, QVariant::fromValue(audioCodec));
547 QMediaFormat::VideoCodec videoCodec = QGstreamerFormatInfo::videoCodecForCaps(structure);
548 if (videoCodec != QMediaFormat::VideoCodec::Unspecified) {
550 metadata.insert(QMediaMetaData::VideoCodec, QVariant::fromValue(videoCodec));
551 std::optional<
float> framerate = structure[
"framerate"].getFraction();
553 metadata.insert(QMediaMetaData::VideoFrameRate, *framerate);
555 QSize resolution = structure.resolution();
556 if (resolution.isValid())
557 metadata.insert(QMediaMetaData::Resolution, resolution);
Combined button and popup list for selecting options.