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
avfmetadata.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies).
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <mediaplayer/avfmediaplayer_p.h>
7#include <qdarwinformatsinfo_p.h>
8
9#include <QtMultimedia/qvideoframe.h>
10#include <QtMultimedia/private/qmediametadata_p.h>
11#include <QtGui/qimage.h>
12#include <QtCore/qbuffer.h>
13#include <QtCore/qdatetime.h>
14#include <QtCore/qiodevice.h>
15#include <QtCore/qlocale.h>
16#include <QtCore/qsemaphore.h>
17#include <QtCore/qurl.h>
18#include <QtCore/private/qcore_mac_p.h>
19
20#if __has_include(<AppKit/AppKit.h>)
21#import <AppKit/AppKit.h>
22#endif
23
24#import <CoreFoundation/CoreFoundation.h>
25
26QT_USE_NAMESPACE
27
28using namespace std::chrono_literals;
29
38
40 // Title
41 { AVMetadataCommonIdentifierTitle, AVMetadataIdentifieriTunesMetadataSongName,
42 AVMetadataIdentifierQuickTimeMetadataTitle,
43 AVMetadataIdentifierID3MetadataTitleDescription,
44 nil, AVMetadata3GPUserDataKeyTitle },
45 // Author
46 { AVMetadataCommonIdentifierAuthor,AVMetadataIdentifieriTunesMetadataAuthor,
47 AVMetadataIdentifierQuickTimeMetadataAuthor, nil,
48 AVMetadataQuickTimeUserDataKeyAuthor, AVMetadata3GPUserDataKeyAuthor },
49 // Comment
50 { nil, AVMetadataIdentifieriTunesMetadataUserComment,
51 AVMetadataIdentifierQuickTimeMetadataComment, AVMetadataIdentifierID3MetadataComments,
52 AVMetadataQuickTimeUserDataKeyComment, nil },
53 // Description
54 { AVMetadataCommonIdentifierDescription,AVMetadataIdentifieriTunesMetadataDescription,
55 AVMetadataIdentifierQuickTimeMetadataDescription, nil,
56 AVMetadataQuickTimeUserDataKeyDescription, AVMetadata3GPUserDataKeyDescription },
57 // Genre
58 { nil, AVMetadataIdentifieriTunesMetadataUserGenre,
59 AVMetadataIdentifierQuickTimeMetadataGenre, nil,
60 AVMetadataQuickTimeUserDataKeyGenre, AVMetadata3GPUserDataKeyGenre },
61 // Date
62 { AVMetadataCommonIdentifierCreationDate, AVMetadataIdentifieriTunesMetadataReleaseDate,
63 AVMetadataIdentifierQuickTimeMetadataCreationDate, AVMetadataIdentifierID3MetadataDate,
64 AVMetadataQuickTimeUserDataKeyCreationDate, AVMetadataISOUserDataKeyDate },
65 // Language
66 { AVMetadataCommonIdentifierLanguage, nil, nil, AVMetadataIdentifierID3MetadataLanguage, nil, nil },
67 // Publisher
68 { AVMetadataCommonIdentifierPublisher, AVMetadataIdentifieriTunesMetadataPublisher,
69 AVMetadataIdentifierQuickTimeMetadataPublisher, AVMetadataIdentifierID3MetadataPublisher, nil, nil },
70 // Copyright
71 { AVMetadataCommonIdentifierCopyrights, AVMetadataIdentifieriTunesMetadataCopyright,
72 AVMetadataIdentifierQuickTimeMetadataCopyright, AVMetadataIdentifierID3MetadataCopyright,
73 AVMetadataQuickTimeUserDataKeyCopyright, AVMetadataISOUserDataKeyCopyright },
74 // Url
75 { nil, nil, nil, AVMetadataIdentifierID3MetadataOfficialAudioSourceWebpage, nil, nil },
76 // Duration
77 { nil, nil, nil, AVMetadataIdentifierID3MetadataLength, nil, nil },
78 // MediaType
79 { AVMetadataCommonIdentifierType, nil, nil, AVMetadataIdentifierID3MetadataContentType, nil, nil },
80 // FileFormat
81 { nil, nil, nil, AVMetadataIdentifierID3MetadataFileType, nil, nil },
82 // AudioBitRate
83 { nil, nil, nil, nil, nil, nil },
84 // AudioCodec
85 { nil, nil, nil, nil, nil, nil },
86 // VideoBitRate
87 { nil, nil, nil, nil, nil, nil },
88 // VideoCodec
89 { nil, nil, nil, nil, nil, nil },
90 // VideoFrameRate
91 { nil, nil, AVMetadataIdentifierQuickTimeMetadataCameraFrameReadoutTime, nil, nil, nil },
92 // AlbumTitle
93 { AVMetadataCommonIdentifierAlbumName, AVMetadataIdentifieriTunesMetadataAlbum,
94 AVMetadataIdentifierQuickTimeMetadataAlbum, AVMetadataIdentifierID3MetadataAlbumTitle,
95 AVMetadataQuickTimeUserDataKeyAlbum, AVMetadata3GPUserDataKeyAlbumAndTrack },
96 // AlbumArtist
97 { nil, AVMetadataIdentifieriTunesMetadataAlbumArtist, nil, nil,
98 AVMetadataQuickTimeUserDataKeyArtist, AVMetadata3GPUserDataKeyPerformer },
99 // ContributingArtist
100 { AVMetadataCommonIdentifierArtist, AVMetadataIdentifieriTunesMetadataArtist,
101 AVMetadataIdentifierQuickTimeMetadataArtist, nil, nil, nil },
102 // TrackNumber
103 { nil, AVMetadataIdentifieriTunesMetadataTrackNumber,
104 nil, AVMetadataIdentifierID3MetadataTrackNumber, nil, nil },
105 // Composer
106 { nil, AVMetadataIdentifieriTunesMetadataComposer,
107 AVMetadataIdentifierQuickTimeMetadataComposer, AVMetadataIdentifierID3MetadataComposer, nil, nil },
108 // LeadPerformer
109 { nil, AVMetadataIdentifieriTunesMetadataPerformer,
110 AVMetadataIdentifierQuickTimeMetadataPerformer, AVMetadataIdentifierID3MetadataLeadPerformer, nil, nil },
111 // ThumbnailImage
112 { nil, nil, nil, AVMetadataIdentifierID3MetadataAttachedPicture, nil, nil },
113 // CoverArtImage
114 { AVMetadataCommonIdentifierArtwork, AVMetadataIdentifieriTunesMetadataCoverArt,
115 AVMetadataIdentifierQuickTimeMetadataArtwork, nil, nil, nil },
116 // Orientation
117 { nil, nil, AVMetadataIdentifierQuickTimeMetadataVideoOrientation, nil, nil, nil },
118 // Resolution
119 { nil, nil, nil, nil, nil, nil },
120 // HasHdrContent
121 { nil, nil, nil, nil, nil, nil }
122};
123
124static AVMetadataIdentifier toIdentifier(QMediaMetaData::Key key, AVMetadataKeySpace keySpace)
125{
126 static_assert(sizeof(keyToAVMetaDataID) / sizeof(AVMetadataIDs) == QMediaMetaData::NumMetaData);
127
128 AVMetadataIdentifier identifier = nil;
129 if ([keySpace isEqualToString:AVMetadataKeySpaceiTunes]) {
130 identifier = keyToAVMetaDataID[key].iTunes;
131 } else if ([keySpace isEqualToString:AVMetadataKeySpaceID3]) {
132 identifier = keyToAVMetaDataID[key].ID3;
133 } else if ([keySpace isEqualToString:AVMetadataKeySpaceQuickTimeMetadata]) {
134 identifier = keyToAVMetaDataID[key].quickTime;
135 } else {
136 identifier = keyToAVMetaDataID[key].common;
137 }
138 return identifier;
139}
140
141static std::optional<QMediaMetaData::Key> toKey(AVMetadataItem *item)
142{
143 static_assert(sizeof(keyToAVMetaDataID) / sizeof(AVMetadataIDs) == QMediaMetaData::NumMetaData);
144
145 // The item identifier may be different than the ones we support,
146 // so check by common key first, as it will get the metadata
147 // irrespective of the format.
148 AVMetadataKey commonKey = item.commonKey;
149 if (commonKey.length != 0) {
150 if ([commonKey isEqualToString:AVMetadataCommonKeyTitle]) {
151 return QMediaMetaData::Title;
152 } else if ([commonKey isEqualToString:AVMetadataCommonKeyDescription]) {
153 return QMediaMetaData::Description;
154 } else if ([commonKey isEqualToString:AVMetadataCommonKeyPublisher]) {
155 return QMediaMetaData::Publisher;
156 } else if ([commonKey isEqualToString:AVMetadataCommonKeyCreationDate]) {
157 return QMediaMetaData::Date;
158 } else if ([commonKey isEqualToString:AVMetadataCommonKeyType]) {
159 return QMediaMetaData::MediaType;
160 } else if ([commonKey isEqualToString:AVMetadataCommonKeyLanguage]) {
161 return QMediaMetaData::Language;
162 } else if ([commonKey isEqualToString:AVMetadataCommonKeyCopyrights]) {
163 return QMediaMetaData::Copyright;
164 } else if ([commonKey isEqualToString:AVMetadataCommonKeyAlbumName]) {
165 return QMediaMetaData::AlbumTitle;
166 } else if ([commonKey isEqualToString:AVMetadataCommonKeyAuthor]) {
167 return QMediaMetaData::Author;
168 } else if ([commonKey isEqualToString:AVMetadataCommonKeyArtist]) {
169 return QMediaMetaData::ContributingArtist;
170 } else if ([commonKey isEqualToString:AVMetadataCommonKeyArtwork]) {
171 return QMediaMetaData::CoverArtImage;
172 }
173 }
174
175 // Check by identifier if no common key found
176 // No need to check for the common keySpace since there's no common key
177 enum keySpaces { iTunes, QuickTime, QuickTimeUserData, IsoUserData, ID3, Other } itemKeySpace;
178 itemKeySpace = Other;
179 AVMetadataKeySpace keySpace = [item keySpace];
180 AVMetadataIdentifier identifier = [item identifier];
181
182 if ([keySpace isEqualToString:AVMetadataKeySpaceiTunes]) {
183 itemKeySpace = iTunes;
184 } else if ([keySpace isEqualToString:AVMetadataKeySpaceQuickTimeMetadata]) {
185 itemKeySpace = QuickTime;
186 } else if ([keySpace isEqualToString:AVMetadataKeySpaceQuickTimeUserData]) {
187 itemKeySpace = QuickTimeUserData;
188 } else if ([keySpace isEqualToString:AVMetadataKeySpaceISOUserData]) {
189 itemKeySpace = IsoUserData;
190 } else if (([keySpace isEqualToString:AVMetadataKeySpaceID3])) {
191 itemKeySpace = ID3;
192 }
193
194 for (int key = 0; key < QMediaMetaData::NumMetaData; key++) {
195 AVMetadataIdentifier idForKey = nil;
196 switch (itemKeySpace) {
197 case iTunes:
198 idForKey = keyToAVMetaDataID[key].iTunes;
199 break;
200 case QuickTime:
201 idForKey = keyToAVMetaDataID[key].quickTime;
202 break;
203 case ID3:
204 idForKey = keyToAVMetaDataID[key].ID3;
205 break;
206 case QuickTimeUserData:
207 idForKey = keyToAVMetaDataID[key].quickTimeUserData;
208 break;
209 case IsoUserData:
210 idForKey = keyToAVMetaDataID[key].isoUserData;
211 break;
212 default:
213 continue;
214 }
215
216 if ([identifier isEqualToString:idForKey])
217 return QMediaMetaData::Key(key);
218 }
219
220 return std::nullopt;
221}
222
223static QMediaMetaData fromAVMetadata(NSArray *metadataItems)
224{
225 QMediaMetaData metadata;
226
227 for (AVMetadataItem* item in metadataItems) {
228 auto key = toKey(item);
229 if (!key)
230 continue;
231
232 // Handle artwork (binary image data)
233 if (*key == QMediaMetaData::CoverArtImage) {
234 NSData *data = [item dataValue];
235 if (data) {
236 QImage image;
237 image.loadFromData(QByteArray::fromNSData(data));
238 QtMultimediaPrivate::setCoverArtImage(metadata, image);
239 }
240 continue;
241 }
242
243 // Handle dates — prefer dateValue over stringValue, as some
244 // items (e.g. creation time from MP4 mvhd) have no stringValue
245 if (*key == QMediaMetaData::Date) {
246 NSDate *dateValue = [item dateValue];
247 if (dateValue) {
248 QDateTime dt = QDateTime::fromNSDate(dateValue);
249 if (dt.isValid()) {
250 metadata.insert(*key, dt);
251 continue;
252 }
253 }
254 // Fall through to try stringValue as ISO 8601
255 const QString str = QString::fromNSString([item stringValue]);
256 if (!str.isNull()) {
257 QDateTime dt = QDateTime::fromString(str, Qt::ISODate);
258 if (dt.isValid()) {
259 metadata.insert(*key, dt);
260 continue;
261 }
262 metadata.insert(*key, str);
263 }
264 continue;
265 }
266
267 const QString value = QString::fromNSString([item stringValue]);
268 if (!value.isNull())
269 metadata.insert(*key, value);
270 }
271 return metadata;
272}
273
274QMediaMetaData AVFMetaData::fromAsset(AVAsset *asset)
275{
276#ifdef QT_DEBUG_AVF
277 qDebug() << Q_FUNC_INFO;
278#endif
279 QMediaMetaData metadata = fromAVMetadata([asset metadata]);
280
281 // On macOS 15 and below, [asset metadata] returns an empty array for certain
282 // formats (e.g. MP3 with ID3 tags), while [asset commonMetadata] still provides
283 // the data. Merge commonMetadata to fill any gaps.
284 {
285 QMediaMetaData common = fromAVMetadata([asset commonMetadata]);
286 for (auto key : common.keys()) {
287 if (metadata.value(key).isNull())
288 metadata.insert(key, common.value(key));
289 }
290 }
291
292 // add duration
293 const CMTime time = [asset duration];
294 const qint64 duration = static_cast<qint64>(float(time.value) / float(time.timescale) * 1000.0f);
295 metadata.insert(QMediaMetaData::Duration, duration);
296
297 // add creation date from asset if not already extracted from metadata items
298 // (e.g. MP4 mvhd creation_time is only available via asset.creationDate)
299 if (metadata.value(QMediaMetaData::Date).isNull()) {
300 AVMetadataItem *creationDate = asset.creationDate;
301 if (creationDate) {
302 NSDate *dateValue = creationDate.dateValue;
303 if (dateValue) {
304 QDateTime dt = QDateTime::fromNSDate(dateValue);
305 if (dt.isValid())
306 metadata.insert(QMediaMetaData::Date, dt);
307 }
308 }
309 }
310
311 std::optional<QtVideo::Rotation> rotationData;
312 std::optional<QSize> resolutionData;
313 QSemaphore sem(0);
314 [asset loadTracksWithMediaType:AVMediaTypeVideo
315 completionHandler:[&](NSArray<AVAssetTrack *> *tracks, NSError *error) {
316 if (!error && tracks && tracks.count > 0) {
317 // only check the first video track
318 AVAssetTrack *videoTrack = tracks[0];
319
320 // add orientation
321 QtVideo::Rotation rotation;
322 bool mirrored = false;
323 AVFMediaPlayer::videoOrientationForAssetTrack(videoTrack, rotation, mirrored);
324 rotationData = rotation;
325
326 // add resolution (coded frame size, without PAR adjustment)
327 NSArray *formatDescriptions = [videoTrack formatDescriptions];
328 if (formatDescriptions.count > 0) {
329 const auto *desc = (__bridge CMVideoFormatDescriptionRef)formatDescriptions[0];
330 CMVideoDimensions dims = CMVideoFormatDescriptionGetDimensions(desc);
331 if (dims.width > 0 && dims.height > 0)
332 resolutionData = QSize(dims.width, dims.height);
333 }
334 }
335 sem.release();
336 }];
337
338 if (!sem.try_acquire_for(5s)) {
339 qWarning() << "Timed out waiting for video tracks to load, proceeding without orientation "
340 "metadata.";
341 return metadata;
342 }
343
344 if (rotationData)
345 metadata.insert(QMediaMetaData::Orientation, int(*rotationData));
346
347 if (resolutionData)
348 metadata.insert(QMediaMetaData::Resolution, *resolutionData);
349
350 return metadata;
351}
352
353QMediaMetaData AVFMetaData::fromAssetTrack(AVAssetTrack *asset)
354{
355 QMediaMetaData metadata = fromAVMetadata([asset metadata]);
356 if ([asset.mediaType isEqualToString:AVMediaTypeAudio]) {
357 if (metadata.value(QMediaMetaData::Language).isNull()) {
358 auto *languageCode = asset.languageCode;
359 if (languageCode) {
360 // languageCode is encoded as ISO 639-2, which QLocale does not handle.
361 // Convert it to 639-1 first.
362 QCFString lang = CFLocaleCreateCanonicalLanguageIdentifierFromString(
363 kCFAllocatorDefault, (__bridge CFStringRef)languageCode);
364 metadata.insert(QMediaMetaData::Language, QLocale::codeToLanguage(QString{ lang }));
365 }
366 }
367 }
368 if ([asset.mediaType isEqualToString:AVMediaTypeVideo]) {
369 // add orientation
370 if (metadata.value(QMediaMetaData::Orientation).isNull()) {
371 QtVideo::Rotation angle = QtVideo::Rotation::None;
372 bool mirrored;
373 AVFMediaPlayer::videoOrientationForAssetTrack(asset, angle, mirrored);
374 Q_UNUSED(mirrored);
375 metadata.insert(QMediaMetaData::Orientation, int(angle));
376 }
377
378 // add HDR content
379 if (metadata.value(QMediaMetaData::HasHdrContent).isNull()) {
380 auto hasHdrContent = false;
381
382 NSArray *formatDescriptions = [asset formatDescriptions];
383 for (id formatDescription in formatDescriptions) {
384 NSDictionary *extensions = (__bridge NSDictionary *)CMFormatDescriptionGetExtensions((CMFormatDescriptionRef)formatDescription);
385 NSString *transferFunction = extensions[(__bridge NSString *)kCMFormatDescriptionExtension_TransferFunction];
386 if ([transferFunction isEqualToString:(__bridge NSString *)kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ]
387 || [transferFunction isEqualToString:(__bridge NSString *)kCVImageBufferTransferFunction_ITU_R_2100_HLG]) {
388 hasHdrContent = true;
389 break;
390 }
391 }
392
393 metadata.insert(QMediaMetaData::HasHdrContent, hasHdrContent);
394 }
395 }
396 return metadata;
397}
398
399static AVMutableMetadataItem *setAVMetadataItemForKey(QMediaMetaData::Key key, const QVariant &value,
400 AVMetadataKeySpace keySpace = AVMetadataKeySpaceCommon)
401{
402 AVMetadataIdentifier identifier = toIdentifier(key, keySpace);
403 if (!identifier.length)
404 return nil;
405
406 AVMutableMetadataItem *item = [AVMutableMetadataItem metadataItem];
407 item.keySpace = keySpace;
408 item.identifier = identifier;
409
410 switch (key) {
411#if QT_DEPRECATED_SINCE(6, 12)
412 case QtMultimediaPrivate::deprecatedThumbnailImage:
413#endif
414 case QMediaMetaData::CoverArtImage: {
415#if defined(Q_OS_MACOS)
416 QImage img = value.value<QImage>();
417 if (!img.isNull()) {
418 QByteArray arr;
419 QBuffer buffer(&arr);
420 buffer.open(QIODevice::WriteOnly);
421 img.save(&buffer);
422 NSData *data = arr.toNSData();
423 NSImage *nsImg = [[NSImage alloc] initWithData:data];
424 item.value = nsImg;
425 [nsImg release];
426 }
427#endif
428 break;
429 }
430 case QMediaMetaData::FileFormat: {
431 QMediaFormat::FileFormat qtFormat = value.value<QMediaFormat::FileFormat>();
432 AVFileType avFormat = QDarwinFormatInfo::avFileTypeForContainerFormat(qtFormat);
433 item.value = avFormat;
434 break;
435 }
436 case QMediaMetaData::Language: {
437 QString lang = QLocale::languageToCode(value.value<QLocale::Language>());
438 if (!lang.isEmpty())
439 item.value = lang.toNSString();
440 break;
441 }
442 case QMediaMetaData::Orientation: {
443 bool ok;
444 int rotation = value.toInt(&ok);
445 if (ok)
446 item.value = [NSNumber numberWithInt:rotation];
447 }
448 default: {
449 switch (value.typeId()) {
450 case QMetaType::QString: {
451 item.value = value.toString().toNSString();
452 break;
453 }
454 case QMetaType::Int: {
455 item.value = [NSNumber numberWithInt:value.toInt()];
456 break;
457 }
458 case QMetaType::LongLong: {
459 item.value = [NSNumber numberWithLongLong:value.toLongLong()];
460 break;
461 }
462 case QMetaType::Double: {
463 item.value = [NSNumber numberWithDouble:value.toDouble()];
464 break;
465 }
466 case QMetaType::QDate:
467 case QMetaType::QDateTime: {
468 item.value = value.toDateTime().toNSDate();
469 break;
470 }
471 case QMetaType::QUrl: {
472 item.value = value.toUrl().toNSURL();
473 break;
474 }
475 default:
476 break;
477 }
478 }
479 }
480
481 return item;
482}
483
484NSMutableArray<AVMetadataItem *> *AVFMetaData::toAVMetadataForFormat(QMediaMetaData metadata, AVFileType format)
485{
486 NSMutableArray<AVMetadataKeySpace> *keySpaces = [NSMutableArray<AVMetadataKeySpace> array];
487 if (format == AVFileTypeAppleM4A) {
488 [keySpaces addObject:AVMetadataKeySpaceiTunes];
489 } else if (format == AVFileTypeMPEGLayer3) {
490 [keySpaces addObject:AVMetadataKeySpaceID3];
491 [keySpaces addObject:AVMetadataKeySpaceiTunes];
492 } else if (format == AVFileTypeQuickTimeMovie) {
493 [keySpaces addObject:AVMetadataKeySpaceQuickTimeMetadata];
494 } else {
495 [keySpaces addObject:AVMetadataKeySpaceCommon];
496 }
497 NSMutableArray<AVMetadataItem *> *avMetaDataArr = [NSMutableArray array];
498 for (const auto &key : metadata.keys()) {
499 for (NSUInteger i = 0; i < [keySpaces count]; i++) {
500 const QVariant &value = metadata.value(key);
501 // set format-specific metadata
502 AVMetadataItem *item = setAVMetadataItemForKey(key, value, keySpaces[i]);
503 if (item)
504 [avMetaDataArr addObject:item];
505 }
506 }
507 return avMetaDataArr;
508}
static AVMetadataIdentifier toIdentifier(QMediaMetaData::Key key, AVMetadataKeySpace keySpace)
static AVMutableMetadataItem * setAVMetadataItemForKey(QMediaMetaData::Key key, const QVariant &value, AVMetadataKeySpace keySpace=AVMetadataKeySpaceCommon)
static std::optional< QMediaMetaData::Key > toKey(AVMetadataItem *item)
static QMediaMetaData fromAVMetadata(NSArray *metadataItems)
const AVMetadataIDs keyToAVMetaDataID[]
#define __has_include(x)
AVMetadataIdentifier quickTimeUserData
AVMetadataIdentifier common
AVMetadataIdentifier ID3
AVMetadataIdentifier iTunes
AVMetadataIdentifier isoUserData
AVMetadataIdentifier quickTime