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