Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "mfmetadata_p.h"
#include "qwindowsasfpicture_p.h"
#include <QtMultimedia/qmediametadata.h>
#include <QtMultimedia/private/qmediametadata_p.h>
#include <QtMultimedia/private/qwindows_scopedpropvariant_p.h>
#include <QtMultimedia/private/qwindowsmultimediautils_p.h>
#include <QtGui/qimage.h>
#include <QtCore/qdatetime.h>
#include <QtCore/qtimezone.h>
#include <QtCore/quuid.h>
#include <QtCore/qvarlengtharray.h>
#include <QtCore/private/qcomptr_p.h>
#include <QtCore/private/qflatmap_p.h>
#include <cguid.h>
#include <guiddef.h>
#include <mfapi.h>
#include <mfidl.h>
#include <propkey.h>
#include <propvarutil.h>
#include <wmsdkidl.h>
//#define DEBUG_MEDIAFOUNDATION
using namespace Qt::StringLiterals;
static const PROPERTYKEY PROP_KEY_NULL = {GUID_NULL, 0};
static QVariant convertValue(const PROPVARIANT& var)
{
QVariant value;
switch (var.vt) {
case VT_LPWSTR:
value = QString::fromUtf16(reinterpret_cast<const char16_t *>(var.pwszVal));
break;
case VT_I4:
value = int(var.lVal);
break;
case VT_UI4:
value = uint(var.ulVal);
break;
case VT_UI8:
value = qulonglong(var.uhVal.QuadPart);
break;
case VT_BOOL:
value = bool(var.boolVal);
break;
case VT_FILETIME:
SYSTEMTIME t;
if (!FileTimeToSystemTime(&var.filetime, &t))
break;
value = QDateTime(QDate(t.wYear, t.wMonth, t.wDay),
QTime(t.wHour, t.wMinute, t.wSecond, t.wMilliseconds),
QTimeZone(QTimeZone::UTC));
break;
case VT_STREAM:
{
STATSTG stat;
if (FAILED(var.pStream->Stat(&stat, STATFLAG_NONAME)))
break;
void *data = malloc(stat.cbSize.QuadPart);
ULONG read = 0;
if (FAILED(var.pStream->Read(data, stat.cbSize.QuadPart, &read))) {
free(data);
break;
}
value = QImage::fromData((const uchar*)data, read);
free(data);
}
break;
case VT_VECTOR | VT_LPWSTR:
QStringList vList;
for (ULONG i = 0; i < var.calpwstr.cElems; ++i)
vList.append(QString::fromUtf16(reinterpret_cast<const char16_t *>(var.calpwstr.pElems[i])));
value = vList;
break;
}
return value;
}
static QVariant metaDataValue(IPropertyStore *content, const PROPERTYKEY &key)
{
if (!content)
return {};
QtMultimediaPrivate::ScopedPropVariant pv;
if (FAILED(content->GetValue(key, pv.get())))
return {};
QVariant value = convertValue(pv.var);
if (!value.isValid())
return value;
// some metadata needs to be reformatted
if (key == PKEY_Media_ClassPrimaryID /*QMediaMetaData::MediaType*/) {
QString v = value.toString();
if (v == u"{D1607DBC-E323-4BE2-86A1-48A42A28441E}")
value = u"Music"_s;
else if (v == u"{DB9830BD-3AB3-4FAB-8A37-1A995F7FF74B}")
value = u"Video"_s;
else if (v == u"{01CD0F29-DA4E-4157-897B-6275D50C4F11}")
value = u"Audio"_s;
else if (v == u"{FCF24A76-9A57-4036-990D-E35DD8B244E1}")
value = u"Other"_s;
} else if (key == PKEY_Media_Duration) {
// duration is provided in 100-nanosecond units, convert to milliseconds
value = (value.toLongLong() + 10000) / 10000;
} else if (key == PKEY_Video_Compression) {
value = int(QWMF::codecForVideoFormat(value.toUuid()));
} else if (key == PKEY_Audio_Format) {
value = int(QWMF::codecForAudioFormat(value.toUuid()));
} else if (key == PKEY_Video_FrameHeight /*Resolution*/) {
QSize res;
res.setHeight(value.toUInt());
if (SUCCEEDED(content->GetValue(PKEY_Video_FrameWidth, pv.get())))
res.setWidth(convertValue(pv.var).toUInt());
value = res;
} else if (key == PKEY_Video_Orientation) {
uint orientation = 0;
if (SUCCEEDED(content->GetValue(PKEY_Video_Orientation, pv.get())))
orientation = convertValue(pv.var).toUInt();
value = orientation;
} else if (key == PKEY_Video_FrameRate) {
value = value.toReal() / 1000.f;
}
return value;
}
QMediaMetaData MFMetaData::fromNative(IMFMediaSource* mediaSource)
{
QMediaMetaData metaData;
// Shell property handler first — provides the richest metadata
// (thumbnails, duration, codecs, resolution, bitrates, etc.)
// but only works for file:// sources.
ComPtr<IPropertyStore> content;
if (SUCCEEDED(MFGetService(mediaSource, MF_PROPERTY_HANDLER_SERVICE, IID_PPV_ARGS(&content))))
metaData = fromNative(content.Get());
// IMFMetadataProvider fallback — works for all source types
// including byte streams (qrc://, QIODevice). Fills in any keys
// not already provided by IPropertyStore.
ComPtr<IMFMetadataProvider> provider;
if (SUCCEEDED(MFGetService(mediaSource, MF_METADATA_PROVIDER_SERVICE, IID_PPV_ARGS(&provider)))) {
ComPtr<IMFPresentationDescriptor> pd;
if (SUCCEEDED(mediaSource->CreatePresentationDescriptor(&pd))) {
ComPtr<IMFMetadata> metadata;
if (SUCCEEDED(provider->GetMFMetadata(pd.Get(), 0, 0, &metadata))) {
const QMediaMetaData mfData = fromNative(metadata.Get());
for (const auto &[key, value] : mfData.asKeyValueRange()) {
if (!metaData.value(key).isValid())
metaData.insert(key, value);
}
}
}
}
return metaData;
}
QMediaMetaData MFMetaData::fromNative(IMFMetadata *metadata)
{
if (!metadata)
return {};
QtMultimediaPrivate::ScopedPropVariant names;
if (FAILED(metadata->GetAllPropertyNames(names.get())))
return {};
QMediaMetaData metaData;
// Property name strings match the Windows SDK g_wszWM* constants from
// wmsdkidl.h but are hardcoded here as they are missing in older MinGW
// variants of the Windows SDK.
static const QVarLengthFlatMap<QStringView, QMediaMetaData::Key, 12> nameToKey({
{ u"Title", QMediaMetaData::Title },
{ u"Author", QMediaMetaData::ContributingArtist },
{ u"WM/AlbumTitle", QMediaMetaData::AlbumTitle },
{ u"WM/AlbumArtist", QMediaMetaData::AlbumArtist },
{ u"WM/Composer", QMediaMetaData::Composer },
{ u"WM/Genre", QMediaMetaData::Genre },
{ u"WM/TrackNumber", QMediaMetaData::TrackNumber },
{ u"Description", QMediaMetaData::Description },
{ u"Copyright", QMediaMetaData::Copyright },
{ u"WM/Publisher", QMediaMetaData::Publisher },
{ u"WM/Language", QMediaMetaData::Language },
{ u"WM/AuthorURL", QMediaMetaData::Url },
});
if (names->vt == (VT_VECTOR | VT_LPWSTR)) {
for (ULONG i = 0; i < names->calpwstr.cElems; ++i) {
const QStringView name(names->calpwstr.pElems[i]);
// WM/Picture blob: QMM_ASF_FLAT_PICTURE header followed by
// MIME type string, description string, and image data.
if (name == u"WM/Picture") {
QtMultimediaPrivate::ScopedPropVariant value;
if (SUCCEEDED(metadata->GetProperty(names->calpwstr.pElems[i], value.get()))
&& value->vt == VT_BLOB) {
QImage img = imageFromAsfFlatPicture(value->blob);
if (!img.isNull())
metaData.insert(QMediaMetaData::CoverArtImage, img);
}
continue;
}
auto it = nameToKey.find(name);
if (it == nameToKey.end())
continue;
QtMultimediaPrivate::ScopedPropVariant value;
if (SUCCEEDED(metadata->GetProperty(names->calpwstr.pElems[i], value.get()))) {
QVariant v = convertValue(value.var);
if (v.isValid())
metaData.insert(it.value(), v);
}
}
}
return metaData;
}
QMediaMetaData MFMetaData::fromNative(IPropertyStore *content)
{
QMediaMetaData metaData;
if (!content)
return metaData;
DWORD cProps;
if (SUCCEEDED(content->GetCount(&cProps))) {
for (DWORD i = 0; i < cProps; i++)
{
PROPERTYKEY key;
if (FAILED(content->GetAt(i, &key)))
continue;
QMediaMetaData::Key mediaKey;
if (key == PKEY_Author) {
mediaKey = QMediaMetaData::Author;
} else if (key == PKEY_Title) {
mediaKey = QMediaMetaData::Title;
// } else if (key == PKEY_Media_SubTitle) {
// mediaKey = QMediaMetaData::SubTitle;
// } else if (key == PKEY_ParentalRating) {
// mediaKey = QMediaMetaData::ParentalRating;
} else if (key == PKEY_Media_EncodingSettings) {
mediaKey = QMediaMetaData::Description;
} else if (key == PKEY_Copyright) {
mediaKey = QMediaMetaData::Copyright;
} else if (key == PKEY_Comment) {
mediaKey = QMediaMetaData::Comment;
} else if (key == PKEY_Media_ProviderStyle) {
mediaKey = QMediaMetaData::Genre;
} else if (key == PKEY_Media_DateEncoded) {
mediaKey = QMediaMetaData::Date;
// } else if (key == PKEY_Rating) {
// mediaKey = QMediaMetaData::UserRating;
// } else if (key == PKEY_Keywords) {
// mediaKey = QMediaMetaData::Keywords;
} else if (key == PKEY_Language) {
mediaKey = QMediaMetaData::Language;
} else if (key == PKEY_Media_Publisher) {
mediaKey = QMediaMetaData::Publisher;
} else if (key == PKEY_Media_ClassPrimaryID) {
mediaKey = QMediaMetaData::MediaType;
} else if (key == PKEY_Media_Duration) {
mediaKey = QMediaMetaData::Duration;
} else if (key == PKEY_Audio_EncodingBitrate) {
mediaKey = QMediaMetaData::AudioBitRate;
} else if (key == PKEY_Audio_Format) {
mediaKey = QMediaMetaData::AudioCodec;
// } else if (key == PKEY_Media_AverageLevel) {
// mediaKey = QMediaMetaData::AverageLevel;
// } else if (key == PKEY_Audio_ChannelCount) {
// mediaKey = QMediaMetaData::ChannelCount;
// } else if (key == PKEY_Audio_PeakValue) {
// mediaKey = QMediaMetaData::PeakValue;
// } else if (key == PKEY_Audio_SampleRate) {
// mediaKey = QMediaMetaData::SampleRate;
} else if (key == PKEY_Music_AlbumTitle) {
mediaKey = QMediaMetaData::AlbumTitle;
} else if (key == PKEY_Music_AlbumArtist) {
mediaKey = QMediaMetaData::AlbumArtist;
} else if (key == PKEY_Music_Artist) {
mediaKey = QMediaMetaData::ContributingArtist;
} else if (key == PKEY_Music_Composer) {
mediaKey = QMediaMetaData::Composer;
// } else if (key == PKEY_Music_Conductor) {
// mediaKey = QMediaMetaData::Conductor;
// } else if (key == PKEY_Music_Lyrics) {
// mediaKey = QMediaMetaData::Lyrics;
// } else if (key == PKEY_Music_Mood) {
// mediaKey = QMediaMetaData::Mood;
} else if (key == PKEY_Music_TrackNumber) {
mediaKey = QMediaMetaData::TrackNumber;
} else if (key == PKEY_Music_Genre) {
mediaKey = QMediaMetaData::Genre;
} else if (key == PKEY_ThumbnailStream) {
QVariant val = metaDataValue(content, key);
if (val.canConvert<QImage>())
QtMultimediaPrivate::setCoverArtImage(metaData, val.value<QImage>());
continue;
} else if (key == PKEY_Video_FrameHeight) {
mediaKey = QMediaMetaData::Resolution;
} else if (key == PKEY_Video_Orientation) {
mediaKey = QMediaMetaData::Orientation;
} else if (key == PKEY_Video_FrameRate) {
mediaKey = QMediaMetaData::VideoFrameRate;
} else if (key == PKEY_Video_EncodingBitrate) {
mediaKey = QMediaMetaData::VideoBitRate;
} else if (key == PKEY_Video_Compression) {
mediaKey = QMediaMetaData::VideoCodec;
// } else if (key == PKEY_Video_Director) {
// mediaKey = QMediaMetaData::Director;
// } else if (key == PKEY_Media_Writer) {
// mediaKey = QMediaMetaData::Writer;
} else {
continue;
}
metaData.insert(mediaKey, metaDataValue(content, key));
}
}
return metaData;
}
static REFPROPERTYKEY propertyKeyForMetaDataKey(QMediaMetaData::Key key)
{
switch (key) {
case QMediaMetaData::Key::Title:
return PKEY_Title;
case QMediaMetaData::Key::Author:
return PKEY_Author;
case QMediaMetaData::Key::Comment:
return PKEY_Comment;
case QMediaMetaData::Key::Genre:
return PKEY_Music_Genre;
case QMediaMetaData::Key::Copyright:
return PKEY_Copyright;
case QMediaMetaData::Key::Publisher:
return PKEY_Media_Publisher;
case QMediaMetaData::Key::Url:
return PKEY_Media_AuthorUrl;
case QMediaMetaData::Key::AlbumTitle:
return PKEY_Music_AlbumTitle;
case QMediaMetaData::Key::AlbumArtist:
return PKEY_Music_AlbumArtist;
case QMediaMetaData::Key::TrackNumber:
return PKEY_Music_TrackNumber;
case QMediaMetaData::Key::Date:
return PKEY_Media_DateEncoded;
case QMediaMetaData::Key::Composer:
return PKEY_Music_Composer;
case QMediaMetaData::Key::Duration:
return PKEY_Media_Duration;
case QMediaMetaData::Key::Language:
return PKEY_Language;
case QMediaMetaData::Key::Description:
return PKEY_Media_EncodingSettings;
case QMediaMetaData::Key::AudioBitRate:
return PKEY_Audio_EncodingBitrate;
case QMediaMetaData::Key::ContributingArtist:
return PKEY_Music_Artist;
#if QT_DEPRECATED_SINCE(6, 12)
case QtMultimediaPrivate::deprecatedThumbnailImage:
#endif
case QMediaMetaData::Key::CoverArtImage:
return PKEY_ThumbnailStream;
case QMediaMetaData::Key::Orientation:
return PKEY_Video_Orientation;
case QMediaMetaData::Key::VideoFrameRate:
return PKEY_Video_FrameRate;
case QMediaMetaData::Key::VideoBitRate:
return PKEY_Video_EncodingBitrate;
case QMediaMetaData::MediaType:
return PKEY_Media_ClassPrimaryID;
default:
return PROP_KEY_NULL;
}
}
static void setStringProperty(IPropertyStore *content, REFPROPERTYKEY key, const QString &value)
{
QtMultimediaPrivate::ScopedPropVariant propValue;
if (SUCCEEDED(InitPropVariantFromString(reinterpret_cast<LPCWSTR>(value.utf16()), propValue.get()))) {
if (SUCCEEDED(PSCoerceToCanonicalValue(key, propValue.get())))
content->SetValue(key, propValue.var);
}
}
static void setUInt32Property(IPropertyStore *content, REFPROPERTYKEY key, quint32 value)
{
QtMultimediaPrivate::ScopedPropVariant propValue;
if (SUCCEEDED(InitPropVariantFromUInt32(ULONG(value), propValue.get()))) {
if (SUCCEEDED(PSCoerceToCanonicalValue(key, propValue.get())))
content->SetValue(key, propValue.var);
}
}
static void setUInt64Property(IPropertyStore *content, REFPROPERTYKEY key, quint64 value)
{
QtMultimediaPrivate::ScopedPropVariant propValue;
if (SUCCEEDED(InitPropVariantFromUInt64(ULONGLONG(value), propValue.get()))) {
if (SUCCEEDED(PSCoerceToCanonicalValue(key, propValue.get())))
content->SetValue(key, propValue.var);
}
}
static void setFileTimeProperty(IPropertyStore *content, REFPROPERTYKEY key, const FILETIME *ft)
{
QtMultimediaPrivate::ScopedPropVariant propValue;
if (SUCCEEDED(InitPropVariantFromFileTime(ft, propValue.get()))) {
if (SUCCEEDED(PSCoerceToCanonicalValue(key, propValue.get())))
content->SetValue(key, propValue.var);
}
}
void MFMetaData::toNative(const QMediaMetaData &metaData, IPropertyStore *content)
{
Q_ASSERT(content);
for (const auto &key : metaData.keys()) {
QVariant value = metaData.value(key);
if (key == QMediaMetaData::Key::MediaType) {
QString strValue = metaData.stringValue(key);
QString v;
// Sets property to one of the MediaClassPrimaryID values defined by Microsoft:
if (strValue == u"Music")
v = u"{D1607DBC-E323-4BE2-86A1-48A42A28441E}"_s;
else if (strValue == u"Video")
v = u"{DB9830BD-3AB3-4FAB-8A37-1A995F7FF74B}"_s;
else if (strValue == u"Audio")
v = u"{01CD0F29-DA4E-4157-897B-6275D50C4F11}"_s;
else
v = u"{FCF24A76-9A57-4036-990D-E35DD8B244E1}"_s;
setStringProperty(content, PKEY_Media_ClassPrimaryID, v);
} else if (key == QMediaMetaData::Key::Duration) {
setUInt64Property(content, PKEY_Media_Duration, value.toULongLong() * 10000);
} else if (key == QMediaMetaData::Key::Resolution) {
QSize res = value.toSize();
setUInt32Property(content, PKEY_Video_FrameWidth, quint32(res.width()));
setUInt32Property(content, PKEY_Video_FrameHeight, quint32(res.height()));
} else if (key == QMediaMetaData::Key::Orientation) {
setUInt32Property(content, PKEY_Video_Orientation, value.toUInt());
} else if (key == QMediaMetaData::Key::VideoFrameRate) {
qreal fps = value.toReal();
setUInt32Property(content, PKEY_Video_FrameRate, quint32(fps * 1000));
} else if (key == QMediaMetaData::Key::TrackNumber) {
setUInt32Property(content, PKEY_Music_TrackNumber, value.toUInt());
} else if (key == QMediaMetaData::Key::AudioBitRate) {
setUInt32Property(content, PKEY_Audio_EncodingBitrate, value.toUInt());
} else if (key == QMediaMetaData::Key::VideoBitRate) {
setUInt32Property(content, PKEY_Video_EncodingBitrate, value.toUInt());
} else if (key == QMediaMetaData::Key::Date) {
// Convert QDateTime to FILETIME by converting to 100-nsecs since
// 01/01/1970 UTC and adding the difference from 1601 to 1970.
ULARGE_INTEGER t = {};
t.QuadPart = ULONGLONG(value.toDateTime().toUTC().toMSecsSinceEpoch() * 10000
+ 116444736000000000LL);
FILETIME ft = {};
ft.dwHighDateTime = t.HighPart;
ft.dwLowDateTime = t.LowPart;
setFileTimeProperty(content, PKEY_Media_DateEncoded, &ft);
} else {
// By default use as string and let PSCoerceToCanonicalValue()
// do validation and type conversion.
REFPROPERTYKEY propKey = propertyKeyForMetaDataKey(key);
if (propKey != PROP_KEY_NULL) {
QString strValue = metaData.stringValue(key);
if (!strValue.isEmpty())
setStringProperty(content, propKey, strValue);
}
}
}
}