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