AVFoundation 元数据读取及写入

不同容器格式的媒体文件在保存元数据的格式上有很大区别, 为了将不同格式文件的元数据展示在一个标准的可视化界面上,方便查看和修改。需要设计以下几个类。

1 THMediaItem

这个类负责管理文件资源的主要类,在文件列表中每一行都是一个该类的实例,其保存AVAsset媒体资源,THMetadata元数据容器等。

THMediaItem头文件

typedef void(^THCompletionHandler)(BOOL complete);

@interface THMediaItem : NSObject
// 用于展示在文件列表中的名字
@property (strong, readonly) NSString *filename;
@property (strong, readonly) NSString *filetype;
@property (strong, readonly) THMetadata *metadata;
// 判断资源对象是否支持元数据写入,MP3文件不支持写入
@property (readonly, getter = isEditable) BOOL editable;

- (id)initWithURL:(NSURL *)url;
// 当选中某个文件时调用,回调中通常用于展示数据
- (void)prepareWithCompletionHandler:(THCompletionHandler)handler;
- (void)saveWithCompletionHandler:(THCompletionHandler)handler;
@end

THMediaItem.m文件

#define META_KEY            @"metadata"

@interface THMediaItem ()
@property (strong) NSURL *url;
@property (strong) AVAsset *asset;
@property (strong) THMetadata *metadata;
@property BOOL prepared;
@end

@implementation THMediaItem

- (id)initWithURL:(NSURL *)url {
    if (self = [super init]) {
        _url = url;
        _asset = [AVAsset assetWithURL:url];
        _filename = [url lastPathComponent];
        _filetype = [self fileTypeForURL:url];
        // MP3受专利保护,无法写入
        _editable = ![_filetype isEqualToString:AVFileTypeMPEGLayer3];
    }
    return self;
}

// 此处仅做简单的类型判断,根据文件后缀名判断。AVFoundation提供了高级接口确定文件的真实类型,当做大型项目时应使用高级接口。
- (NSString *)fileTypeForURL:(NSURL *)url {
    NSString *ext = [[self.url lastPathComponent] pathExtension];
    NSString *type = nil;
    // 此处未做容错处理,后缀名可能为无效字符串,真实开发此处应做错误处理。
    if ([ext isEqualToString:@"m4a"]) {
        type = AVFileTypeAppleM4A;
    } else if ([ext isEqualToString:@"m4v"]) {
        type = AVFileTypeAppleM4V;
    } else if ([ext isEqualToString:@"mov"]) {
        type = AVFileTypeQuickTimeMovie;
    } else if ([ext isEqualToString:@"mp4"]) {
        type = AVFileTypeMPEG4;
    } else {
        type = AVFileTypeMPEGLayer3;
    }
    return type;
}

- (void)prepareWithCompletionHandler:(THCompletionHandler)completionHandler {
    if (self.prepared) {
        completionHandler(self.prepared);
        return;
    }
    
    self.metadata = [[THMetadata alloc] init];
    NSArray *keys = @[META_KEY];
    [self.asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
        AVKeyValueStatus metadataStatus = [self.asset statusOfValueForKey:META_KEY error:nil];
        self.prepared = metadataStatus == AVKeyValueStatusLoaded;
        if (self.prepared) {
            for (AVMetadataItem *item in self.asset.metadata) {
                [self.metadata addMetadataItem:item withKey:item.identifier];
            }
        }
        completionHandler(self.prepared);
    }];
}

- (NSURL *)tempURL {
    NSString *tempDir = NSTemporaryDirectory();
    NSString *ext = [[self.url lastPathComponent] pathExtension];
    NSString *tempName = [NSString stringWithFormat:@"temp.%@",ext];
    NSString *temPath = [tempDir stringByAppendingPathComponent:tempName];
    return [NSURL fileURLWithPath:temPath];
}

- (void)reset {
    _prepared = NO;
    _asset = [AVAsset assetWithURL:self.url];
}

- (void)saveWithCompletionHandler:(THCompletionHandler)handler {
    // 使用该预设直接将媒体数据拷贝,不会对其编码,这样只对元数据进行操作耗时很少。但是其只允许修改元数据,不允许添加新的元数据(需要使用转码预设值)。另外不能修改ID3标签,不支持MP3文件写入。
    NSString *presetName = AVAssetExportPresetPassthrough;
    AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:self.asset presetName:presetName];
    
    NSURL *outputURL = [self tempURL];
    session.outputURL = outputURL;
    session.outputFileType = self.filetype;
    session.metadata = [self.metadata metadataItems];
    
    [session exportAsynchronouslyWithCompletionHandler:^{
        AVAssetExportSessionStatus status = session.status;
        BOOL success = (status == AVAssetExportSessionStatusCompleted);
        if (success) {
            NSURL *sourceURL = self.url;
            NSFileManager *manager = [NSFileManager defaultManager];
            [manager removeItemAtURL:sourceURL error:nil];
            [manager moveItemAtURL:outputURL toURL:sourceURL error:nil];
            [self reset];
        }
        if (handler) {
            handler(success);
        }
    }];
}
@end

2 THMetaDataItem

该类管理某个文件的所有可用元数据的类,是元数据的容器。他通过KVC的方式将一个AVAsset资源的所有可用元数据保存为自己的属性。在这个过程中遵守THMetadataConverter协议的转换器负责将数据在可展示的格式即THMetaDataItem的属性和AVMetadataItem之间转换, 该类是转换器的调用者。转换器下文介绍。另外由于风格数据非常复杂,这里使用THGenre来处理风格数据,具体细节下文介绍。

THMetaDataItem头文件

@interface THMetadata : NSObject
@property (copy) NSString *name;
@property (copy) NSString *artist;
@property (copy) NSString *albumArtist;
@property (copy) NSString *album;
@property (copy) NSString *grouping;
@property (copy) NSString *composer;
@property (copy) NSString *comments;
@property (strong) NSImage *artwork;
@property (strong) THGenre *genre;

@property NSString *year;
@property NSNumber *bpm;
@property NSNumber *trackNumber;
@property NSNumber *trackCount;
@property NSNumber *discNumber;
@property NSNumber *discCount;

- (void)addMetadataItem:(AVMetadataItem *)item withKey:(id)key;
- (NSArray *)metadataItems;
@end

THMetaDataItem.m文件:其中buildKeyMapping方法负责将不同格式中描述同一个属性,如各个格式中描述艺术家的AVAssetMetadaItem的identifier值映射为通用的标识符。改方法中只给出部分标识符映射,需要完整映射的方法是在载入一个媒体资源文件时,将其所有的AVAssetMetadaItem的identifier属性打印出来,再对照文章末尾的各个不同格式下所有AVAssetMetadaItem的identifier实际字符串,找出AVFoundation所定义的Identifier常量,将其映射为通用标识符。

@interface THMetadata ()
@property (strong) NSDictionary *keyMapping;
@property (strong) NSMutableDictionary *metadata;
@property (strong) THMetadataConverterFactory *converterFactory;
@end

@implementation THMetadata

- (id)init {
    if (self = [super init]) {
        _keyMapping = [self buildKeyMapping];
        _metadata = [[NSMutableDictionary alloc] initWithCapacity:5];
        _converterFactory = [[THMetadataConverterFactory alloc] init];
    }
    return self;
}

- (NSDictionary *)buildKeyMapping {
    return @{
        // Name Mapping
        // Mp4
        AVMetadataCommonIdentifierTitle : THMetadataIdentifierName,
        // M4v,M4a
        AVMetadataIdentifieriTunesMetadataSongName : THMetadataIdentifierName,
        // Mp3-ID3V2.2
        @"id3/%00TT2" : THMetadataIdentifierName,
        // Mp3-ID3V2.3 and later
        AVMetadataIdentifierID3MetadataTitleDescription : THMetadataIdentifierName,
        // Mov
        AVMetadataIdentifierQuickTimeMetadataDisplayName : THMetadataIdentifierName,

        // Artist Mapping
        AVMetadataCommonIdentifierArtist : THMetadataIdentifierArtist,
        AVMetadataIdentifieriTunesMetadataArtist : THMetadataIdentifierArtist,
        @"id3/%00TP1" : THMetadataIdentifierArtist,
        AVMetadataIdentifierID3MetadataLeadPerformer : THMetadataIdentifierArtist,
        AVMetadataIdentifierQuickTimeMetadataProducer : THMetadataIdentifierArtist,

        // Album Artist Mapping
        AVMetadataIdentifieriTunesMetadataAlbumArtist : THMetadataIdentifierAlbumArtist,
        @"id3/%00TP2" : THMetadataIdentifierAlbumArtist,
        AVMetadataIdentifierID3MetadataBand : THMetadataIdentifierAlbumArtist,
        AVMetadataIdentifierQuickTimeMetadataDirector : THMetadataIdentifierAlbumArtist,

        // Album Mapping
        AVMetadataCommonIdentifierAlbumName : THMetadataIdentifierAlbum,
        AVMetadataIdentifieriTunesMetadataAlbum : THMetadataIdentifierAlbum,
        @"id3/%00TAL" : THMetadataIdentifierAlbum,
        AVMetadataIdentifierID3MetadataAlbumTitle : THMetadataIdentifierAlbum,
        AVMetadataIdentifierQuickTimeMetadataAlbum : THMetadataIdentifierAlbum,
        
        // Artwork Mapping
        AVMetadataCommonIdentifierArtwork : THMetadataIdentifierArtwork,
        AVMetadataIdentifieriTunesMetadataCoverArt : THMetadataIdentifierArtwork,
        @"id3/%00PIC" : THMetadataIdentifierArtwork,
        AVMetadataIdentifierID3MetadataAttachedPicture : THMetadataIdentifierArtwork,
        AVMetadataIdentifierQuickTimeMetadataArtwork : THMetadataIdentifierArtwork,

        // Year Mapping
        @"TYE" : THMetadataIdentifierYear,
        AVMetadataCommonIdentifierCreationDate : THMetadataIdentifierYear,
        AVMetadataIdentifieriTunesMetadataReleaseDate : THMetadataIdentifierYear,
        @"id3/%00TYE" : THMetadataIdentifierYear,
        AVMetadataIdentifierID3MetadataYear : THMetadataIdentifierYear,
        AVMetadataIdentifierQuickTimeMetadataYear : THMetadataIdentifierYear,
        AVMetadataIdentifierID3MetadataRecordingTime : THMetadataIdentifierYear,

        // BPM Mapping
        AVMetadataIdentifieriTunesMetadataBeatsPerMin : THMetadataIdentifierBPM,
        AVMetadataIdentifierID3MetadataBeatsPerMinute : THMetadataIdentifierBPM,
        @"TBP" : THMetadataIdentifierBPM,

        // Grouping Mapping
        AVMetadataCommonIdentifierSubject : THMetadataIdentifierGrouping,
        @"itsk/%A9grp" : THMetadataIdentifierGrouping,
        AVMetadataIdentifieriTunesMetadataGrouping : THMetadataIdentifierGrouping,
        @"id3/%00TT1" : THMetadataIdentifierGrouping,
        AVMetadataIdentifierID3MetadataContentGroupDescription : THMetadataIdentifierGrouping,

        // Track Number Mapping
        AVMetadataIdentifieriTunesMetadataTrackNumber : THMetadataIdentifierTrackNumber,
        @"id3/%00TRK" : THMetadataIdentifierTrackNumber,
        AVMetadataIdentifierID3MetadataTrackNumber : THMetadataIdentifierTrackNumber,
        @"TRK" : THMetadataIdentifierTrackNumber,

        // Composer Mapping
        AVMetadataCommonIdentifierCreator : THMetadataIdentifierComposer,
        AVMetadataIdentifieriTunesMetadataComposer : THMetadataIdentifierComposer,
        @"id3/%00TCM" : THMetadataIdentifierComposer,
        AVMetadataIdentifierID3MetadataComposer : THMetadataIdentifierComposer,
        AVMetadataIdentifierQuickTimeMetadataDirector : THMetadataIdentifierComposer,

        // Disc Number Mapping
        AVMetadataIdentifieriTunesMetadataDiscNumber : THMetadataIdentifierDiscNumber,
        @"id3/%00TPA" : THMetadataIdentifierDiscNumber,
        AVMetadataIdentifierID3MetadataPartOfASet : THMetadataIdentifierDiscNumber,

        // Comments Mapping
        AVMetadataCommonIdentifierDescription : THMetadataIdentifierComments,
        AVMetadataIdentifieriTunesMetadataUserComment : THMetadataIdentifierComments,
        @"id3/%00COM" : THMetadataIdentifierComments,
        AVMetadataIdentifierID3MetadataComments : THMetadataIdentifierComments,
        AVMetadataIdentifierQuickTimeMetadataDescription : THMetadataIdentifierComments,

        // Genre Mapping
        AVMetadataCommonIdentifierType : THMetadataIdentifierGenre,
        AVMetadataIdentifieriTunesMetadataPredefinedGenre : THMetadataIdentifierGenre,
        AVMetadataIdentifieriTunesMetadataUserGenre : THMetadataIdentifierGenre,
        @"id3/%00TCO" : THMetadataIdentifierGenre,
        AVMetadataIdentifierID3MetadataContentType : THMetadataIdentifierGenre,
        AVMetadataIdentifierQuickTimeMetadataGenre : THMetadataIdentifierGenre,
    };
}


- (void)addMetadataItem:(AVMetadataItem *)item withKey:(id)key {
    // 由于文件格式不同可能导致标识不同,这里将不同格式的标识统一
    NSString *normalizedKey = self.keyMapping[key];
    if (normalizedKey) {
        id <THMetadataConverter> converter = [self.converterFactory converterForKey:normalizedKey];
        id value = [converter displayValueFromMetadataItem:item];
        if ([value isKindOfClass:[NSDictionary class]]) {
            NSDictionary *data = (NSDictionary *)value;
            for (NSString *currentKey in data) {
                [self setValue:data[currentKey] forKey:currentKey];
            }
        } else {
            [self setValue:value forKey:normalizedKey];
        }
        self.metadata[normalizedKey] = item;
    }
}

- (NSArray *)metadataItems {
    NSMutableArray *items = [NSMutableArray array];
    // 音轨编号/计数 唱片编号/计数 都需要额外处理
    [self addmetadataItemForNumber:self.trackNumber count:self.trackCount numberKey:THMetadataIdentifierTrackNumber countKey:THMetadataIdentifierTrackCount toArray:items];
    
    [self addmetadataItemForNumber:self.discNumber count:self.discCount numberKey:THMetadataIdentifierDiscNumber countKey:THMetadataIdentifierDiscCount toArray:items];
    
    NSMutableDictionary *metaDict = [self.metadata mutableCopy];
    // 移除处理过的数据
    [metaDict removeObjectForKey:THMetadataIdentifierTrackNumber];
    [metaDict removeObjectForKey:THMetadataIdentifierDiscNumber];
    
    // 创建新的AVMetadataItem实例
    for (NSString *key in metaDict) {
        id<THMetadataConverter> converter = [self.converterFactory converterForKey:key];
        id value = [self valueForKey:key];
        AVMetadataItem *item = [converter metadataItemFromDisplayValue:value withMetadataItem:metaDict[key]];
        if (item) {
            [items addObject:item];
        }
    }
    return items;
}

- (void)addmetadataItemForNumber:(NSNumber *)number count:(NSNumber *)count numberKey:(NSString *)numberKey countKey:(NSString *)countKey toArray:(NSMutableArray *)items {
    id <THMetadataConverter> converter = [self.converterFactory converterForKey:numberKey];
    NSDictionary *data = @{numberKey : number ?: [NSNull null], countKey : count ?: [NSNull null]};
    AVMetadataItem *sourceItem = self.metadata[numberKey];
    AVMetadataItem *item = [converter metadataItemFromDisplayValue:data withMetadataItem:sourceItem];
    if (item) {
        [items addObject:item];
    }
}
@end

3 THMetadataConverter协议

该协议定义了两个方法,负责将元数据在可展示的格式即THMetaDataItem的属性和AVMetadataItem之间转换。所有的转换器都必须遵守该协议。

@protocol THMetadataConverter <NSObject>
// 将AVMetadataItem中的数据取出转换为可展示数据
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item;

// 将可展示数据转化为AVMetadataItem的Value,方便以后存储
- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item;
@end

4 THMetadataConverterFactory

通过所需要转换的键值,确定具体使用的转化器。

@interface THMetadataConverterFactory : THDefaultMetadataConverter
- (id <THMetadataConverter>)converterForKey:(NSString *)key;
@end

@implementation THMetadataConverterFactory
- (id <THMetadataConverter>)converterForKey:(NSString *)key {
    id <THMetadataConverter> converter = nil;

    if ([key isEqualToString:THMetadataIdentifierArtwork]) {
        converter = [[THArtworkMetadataConverter alloc] init];
    }  else if ([key isEqualToString:THMetadataIdentifierTrackNumber]) {
        converter = [[THTrackMetadataConverter alloc] init];
    }  else if ([key isEqualToString:THMetadataIdentifierDiscNumber]) {
        converter = [[THDiscMetadataConverter alloc] init];
    }  else if ([key isEqualToString:THMetadataIdentifierComments]) {
        converter = [[THCommentMetadataConverter alloc] init];
    }  else if ([key isEqualToString:THMetadataIdentifierGenre]) {
        converter = [[THGenreMetadataConverter alloc] init];
    }  else {
        converter = [[THDefaultMetadataConverter alloc] init];
    }
    return converter;
}
@end

5 转换器

由于对于不同的元数据类型,转换的方式不一样,因此需要将元数据分类,并分别为其建立元数据转换器类。

5.1 简单转换

对于简单的将元数据以固定的字符串或者数字的方式存储的元数据类型,使用默认转换器。

// 处理简单字符串和数字值
@implementation THDefaultMetadataConverter
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    return item.value;
}

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {
    AVMutableMetadataItem *metadataItem = [item mutableCopy];
    metadataItem.value = value;
    return metadataItem;
}
@end
5.2 转换专辑封面Artwork
@implementation THArtworkMetadataConverter
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    // 也可以使用UIImage
    NSImage *image = nil;
    // 由于文件类型不同,因此其保存唱片封面海报或电影海报的格式不一致
    if ([item.value isKindOfClass:[NSData class]]) {
        image = [[NSImage alloc] initWithData:item.dataValue];
    } else if ([item.value isKindOfClass:[NSDictionary class]]) {
        // MP3文件需单独处理
        NSDictionary *dict = (NSDictionary *)item.value;
        image = [[NSImage alloc] initWithData:dict[@"data"]];
    }
    return image;
}

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {
    // 由于后期MP3文件无法写入,此处不对其对应的元数据进行处理
    AVMutableMetadataItem *metadataItem = [item mutableCopy];
    NSImage *image = (NSImage *)value;
    metadataItem.value = image.TIFFRepresentation;
    // 如果存储PNG和JPG格式的图片,使用NSBitmapImageReq或者UIImage方法或者Quartz
    // NSData *imageData = image.TIFFRepresentation;
    // NSBitmapImageRep *imageReq = [NSBitmapImageRep imageRepWithData:imageData];
    // [imageReq setSize:image.size];
    // PNG
    // NSData *imageDataPNG = [imageReq representationUsingType:NSPNGFileType properties:@{}];
    // JPG
    // NSDictionary *imageProps = @{NSImageCompressionFactor : @(0.85)};
    // NSData *imageDataJPG = [imageReq representationUsingType:NSJPEGFileType properties:imageProps];
    return metadataItem.copy;
}
5.3 转换注释

实际操作时发现MP3格式文件也直接存储为字符串,可能是由于ID3版本不同导致。

@implementation THCommentMetadataConverter
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    NSString *value = nil;
    if ([item.value isKindOfClass:[NSString class]]) {
        value = item.stringValue;
    } else if ([item.value isKindOfClass:[NSDictionary class]]) {
        // MP3单独处理,其注释信息保存在一个标识符为空的字典中,其内容通过text可以取到
        NSDictionary *dict = (NSDictionary *)item.value;
        if ([dict[@"identifier"] isEqualToString:@""]) {
            value = dict[@"text"];
        }
    }
    return value;
}

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {
    // 由于后期MP3文件无法写入,此处不对其对应的元素据进行处理
    AVMutableMetadataItem *metadataItem = item.mutableCopy;
    metadataItem.value = value;
    return metadataItem;
}
@end
5.4 转换音轨数据
// 音轨数据包含一首歌在整个唱片中的编号位置信息
@implementation THTrackMetadataConverter
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    NSNumber *number = nil;
    NSNumber *count = nil;
    
    if ([item.value isKindOfClass:[NSString class]]) {
        // MP3文件可以很方便的获取音轨信息,其格式为xx/xx
        NSArray *compponents = [item.stringValue componentsSeparatedByString:@"/"];
        number = @([compponents[0] integerValue]);
        count = @([compponents[1] integerValue]);
    } else if ([item.value isKindOfClass:[NSData class]]) {
        // M4A获取音轨信息比较复杂,由4个16进制为big endian的数字组成,如<0000 0008  000a 0000>,其第二和第三个元素分别包含音轨编号和音轨计数,获取时要将其转化为小端模式
        NSData *data = item.dataValue;
        if (data.length == 8) {
            uint16_t *values = (uint16_t *)[data bytes];
            if (values[1] > 0) {
                number = @(CFSwapInt16BigToHost(values[1]));
            }
            if (values[2] > 0) {
                count = @(CFSwapInt16BigToHost(values[2]));
            }
        }
    }
    
    NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:5];
    [dict setObject:number ?: [NSNull null] forKey:THMetadataIdentifierTrackNumber];
    [dict setObject:count ?: [NSNull null] forKey:THMetadataIdentifierTrackCount];
    return dict;
}

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {
    // 由于后期MP3文件无法写入,此处不对其对应的元数据进行处理
    AVMutableMetadataItem *metaDataItem = [item mutableCopy];
    NSDictionary *trackData = (NSDictionary *)value;
    NSNumber *trackNumber = trackData[THMetadataIdentifierTrackNumber];
    NSNumber *trackCount = trackData[THMetadataIdentifierTrackCount];
    
    uint16_t values[4] = {0};
    // 同获取相反,写入文件时候必须以大端格式写入,因此这里进行转换
    if (trackNumber && ![trackNumber isKindOfClass:[NSNull class]]) {
        values[1] = CFSwapInt16HostToBig([trackNumber unsignedIntValue]);
    }
    if (trackCount && ![trackCount isKindOfClass:[NSNull class]]) {
        values[2] = CFSwapInt16HostToBig([trackCount unsignedIntValue]);
    }
    
    size_t length = sizeof(values);
    metaDataItem.value = [NSData dataWithBytes:values length:length];
    return metaDataItem.copy;
}
@end
5.5 转换唱片数据
// 表示某首歌曲所在的唱片是属于唱片集合中的第几个唱片,通常是1/1
@implementation THDiscMetadataConverter
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    NSNumber *number = nil;
    NSNumber *count = nil;
    
    if ([item.value isKindOfClass:[NSString class]]) {
        // MP3文件可以很方便的获取音轨信息,其格式为xx/xx
        NSArray *components = [item.stringValue componentsSeparatedByString:@"/"];
        number = @([components[0] integerValue]);
        count = @([components[1] integerValue]);
    } else if ([item.value isKindOfClass:[NSData class]]) {
        // M4A获取音轨信息比较复杂,由3个16进制为big endian的数字组成,如<0000 0008 000a>,其第二个和第三元素分别包含音轨编号和音轨计数,获取时要将其转化为小端模式
        NSData *data = item.dataValue;
        if (data.length == 6) {
            uint16_t *values = (uint16_t *)[data bytes];
            if (values[1] > 0) {
                number = @(CFSwapInt16BigToHost(values[1]));
            }
            if (values[2] > 0) {
                count = @(CFSwapInt16BigToHost(values[2]));
            }
        }
    }
    NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:5];
    [dict setObject:number ?: [NSNull null] forKey:THMetadataIdentifierDiscNumber];
    [dict setObject:count ?: [NSNull null] forKeyedSubscript:THMetadataIdentifierDiscCount];
    
    return dict;
}

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {
    // 由于后期MP3文件无法写入,此处不对其对应的元数据进行处理
    AVMutableMetadataItem *metadataItem = [item mutableCopy];
    NSDictionary *discData = (NSDictionary *)value;
    NSNumber *discNumber = discData[THMetadataIdentifierDiscNumber];
    NSNumber *discCount = discData[THMetadataIdentifierDiscCount];
    
    uint16_t values[3] = {0};
    
    if (discNumber && ![discNumber isKindOfClass:[NSNull class]]) {
        values[1] = CFSwapInt16HostToBig([discNumber unsignedIntValue]);
    }
    if (discCount && ![discCount isKindOfClass:[NSNull class]]) {
        values[2] = CFSwapInt16HostToBig([discCount unsignedIntValue]);
    }
    
    size_t length = sizeof(values);
    metadataItem.value = [NSData dataWithBytes:values length:length];
    return metadataItem.copy;
}
@end
5.6 转换风格数据

风格数据保存方式非常复杂,为了表示某个音频或视频的风格。最初ID3划分了126个风格(完整列表见文章末尾),iTunes继承了这些风格,但是序号相隔1,如ID3中Blues用0表示, iTunes中Blues用1表示。同时iTunes还有自己特有的风格(见Genre IDsAppendix)。在元数据中,有时风格数据被保存为其名字的字符串,有时被保存为对应的序号,因此需要使用THGenre(下文介绍)来处理风格数据。

@implementation THGenreMetadataConverter
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    THGenre *genre = nil;
    
    if ([item.value isKindOfClass:[NSString class]]) {
        // 采用ID3标准的格式文件将风格数据保存为字符串
        if ([item.keySpace isEqualToString:AVMetadataKeySpaceID3]) {
            if (item.numberValue) {
                // 有时保存的是类型序号,可以通过强转nsnumber类型判断
                NSUInteger genreIndex = [item.numberValue unsignedIntValue];
                genre = [THGenre id3GenreWithIndex:genreIndex];
            } else {
                // 有时保存期类型字符串
                genre = [THGenre id3GenreWithName:item.stringValue];
            }
        } else {
            // 部分格式文件(如QuickTime电影或者MPEG-4视频文件)将风格数据保存其名字字符串
            genre = [THGenre videoGenreWithName:item.stringValue];
        }
    } else if ([item.value isKindOfClass:[NSData class]]) {
        // 当使用一个预定于风格时,iTunes M4A音频会返回一个16位的big endian数字
        NSData *data = item.dataValue;
        if (data.length == 2) {
            uint16_t *values = (uint16_t *)[data bytes];
            uint16_t genreIndex = CFSwapInt16BigToHost(values[0]);
            genre = [THGenre iTunesGenreWithIndex:genreIndex];
        }
    }
    return genre;
}

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {
    AVMutableMetadataItem *metadataItem = item.mutableCopy;
    THGenre *genre = (THGenre *)value;
    
    if ([item.value isKindOfClass:[NSString class]]) {
        metadataItem.value = genre.name;
    } else {
        NSData *data = item.dataValue;
        if (data.length == 2) {
            uint16_t value = CFSwapInt16HostToBig(genre.index + 1);
            size_t length = sizeof(value);
            metadataItem.value = [NSData dataWithBytes:&value length:length];
        }
    }
    return metadataItem.copy;
}
@end

6 THGenre

在元数据中,有时风格数据被保存为其名字的字符串,有时被保存为对应的序号,因此需要使用THGenre(下文介绍)来处理风格数据。

@interface THGenre : NSObject <NSCopying>
@property (nonatomic, readonly) NSUInteger index;
@property (nonatomic, copy, readonly) NSString *name;

+ (NSArray *)musicGenres;
+ (NSArray *)videoGenres;
+ (THGenre *)id3GenreWithIndex:(NSUInteger)index;
+ (THGenre *)id3GenreWithName:(NSString *)name;
+ (THGenre *)iTunesGenreWithIndex:(NSUInteger)index;
+ (THGenre *)videoGenreWithName:(NSString *)name;
@end

7 小结

通过设置完上述类,现在当从程序中加载一个AVAsset资源时,通过其URL初始化一个THMediaItem对象,通过调用其prepareWithCompletionHandler: handler方法,在handler中使用THMetadata数据对UI进行更新即可。当需要存储时调用saveWithCompletionHandler: handler即可。

8 附件

8.1 ITunes格式的MetadataIdentifier

AVMetadataIdentifieriTunesMetadataAlbum         == @"itsk/%A9alb";
AVMetadataIdentifieriTunesMetadataArtist        == @"itsk/%A9ART";
AVMetadataIdentifieriTunesMetadataUserComment   == @"itsk/%A9cmt";
AVMetadataIdentifieriTunesMetadataCoverArt      == @"itsk/covr";
AVMetadataIdentifieriTunesMetadataCopyright     == @"itsk/cprt";
AVMetadataIdentifieriTunesMetadataReleaseDate   == @"itsk/%A9day";
AVMetadataIdentifieriTunesMetadataEncodedBy     == @"itsk/%A9enc";
AVMetadataIdentifieriTunesMetadataPredefinedGenre   == @"itsk/gnre";
AVMetadataIdentifieriTunesMetadataUserGenre     == @"itsk/%A9gen";
AVMetadataIdentifieriTunesMetadataSongName      == @"itsk/%A9nam";
AVMetadataIdentifieriTunesMetadataTrackSubTitle == @"itsk/%A9st3";
AVMetadataIdentifieriTunesMetadataEncodingTool  == @"itsk/%A9too";
AVMetadataIdentifieriTunesMetadataComposer      == @"itsk/%A9wrt";
AVMetadataIdentifieriTunesMetadataAlbumArtist   == @"itsk/aART";
AVMetadataIdentifieriTunesMetadataAccountKind   == @"itsk/akID";
AVMetadataIdentifieriTunesMetadataAppleID       == @"itsk/apID";
AVMetadataIdentifieriTunesMetadataArtistID      == @"itsk/atID";
AVMetadataIdentifieriTunesMetadataSongID        == @"itsk/cnID";
AVMetadataIdentifieriTunesMetadataDiscCompilation   == @"itsk/cpil";
AVMetadataIdentifieriTunesMetadataDiscNumber    == @"itsk/disk";
AVMetadataIdentifieriTunesMetadataGenreID       == @"itsk/geID";
AVMetadataIdentifieriTunesMetadataGrouping      == @"itsk/grup";
AVMetadataIdentifieriTunesMetadataPlaylistID    == @"itsk/plID";
AVMetadataIdentifieriTunesMetadataContentRating == @"itsk/rtng";
AVMetadataIdentifieriTunesMetadataBeatsPerMin   == @"itsk/tmpo";
AVMetadataIdentifieriTunesMetadataTrackNumber   == @"itsk/trkn";
AVMetadataIdentifieriTunesMetadataArtDirector   == @"itsk/%A9ard";
AVMetadataIdentifieriTunesMetadataArranger      == @"itsk/%A9arg";
AVMetadataIdentifieriTunesMetadataAuthor        == @"itsk/%A9aut";
AVMetadataIdentifieriTunesMetadataLyrics        == @"itsk/%A9lyr";
AVMetadataIdentifieriTunesMetadataAcknowledgement   == @"itsk/%A9cak";
AVMetadataIdentifieriTunesMetadataConductor     == @"itsk/%A9con";
AVMetadataIdentifieriTunesMetadataDescription   == @"itsk/%A9des";
AVMetadataIdentifieriTunesMetadataDirector      == @"itsk/%A9dir";
AVMetadataIdentifieriTunesMetadataEQ            == @"itsk/%A9equ";
AVMetadataIdentifieriTunesMetadataLinerNotes    == @"itsk/%A9lnt";
AVMetadataIdentifieriTunesMetadataRecordCompany == @"itsk/%A9mak";
AVMetadataIdentifieriTunesMetadataOriginalArtist    == @"itsk/%A9ope";
AVMetadataIdentifieriTunesMetadataPhonogramRights   == @"itsk/%A9phg";
AVMetadataIdentifieriTunesMetadataProducer      == @"itsk/%A9prd";
AVMetadataIdentifieriTunesMetadataPerformer     == @"itsk/%A9prf";
AVMetadataIdentifieriTunesMetadataPublisher     == @"itsk/%A9pub";
AVMetadataIdentifieriTunesMetadataSoundEngineer == @"itsk/%A9sne";
AVMetadataIdentifieriTunesMetadataSoloist       == @"itsk/%A9sol";
AVMetadataIdentifieriTunesMetadataCredits       == @"itsk/%A9src";
AVMetadataIdentifieriTunesMetadataThanks        == @"itsk/%A9thx";
AVMetadataIdentifieriTunesMetadataOnlineExtras  == @"itsk/%A9url";
AVMetadataIdentifieriTunesMetadataExecProducer  == @"itsk/%A9xpd";

8.2 MP3定义的126种完整风格

Index:1 name:@"Classic Rock",
Index:2 name:@"Country",
Index:3 name:@"Dance",
Index:4 name:@"Disco",
Index:5 name:@"Funk",
Index:6 name:@"Grunge",
Index:7 name:@"Hip-Hop",
Index:8 name:@"Jazz",
Index:9 name:@"Metal",
Index:10 name:@"New Age",
Index:11 name:@"Oldies",
Index:12 name:@"Other",
Index:13 name:@"Pop",
Index:14 name:@"R&B",
Index:15 name:@"Rap",
Index:16 name:@"Reggae",
Index:17 name:@"Rock",
Index:18 name:@"Techno",
Index:19 name:@"Industrial",
Index:20 name:@"Alternative",
Index:21 name:@"Ska",
Index:22 name:@"Death Metal",
Index:23 name:@"Pranks",
Index:24 name:@"Soundtrack",
Index:25 name:@"Euro-Techno",
Index:26 name:@"Ambient",
Index:27 name:@"Trip-Hop",
Index:28 name:@"Vocal",
Index:29 name:@"Jazz+Funk",
Index:30 name:@"Fusion",
Index:31 name:@"Trance",
Index:32 name:@"Classical",
Index:33 name:@"Instrumental",
Index:34 name:@"Acid",
Index:35 name:@"House",
Index:36 name:@"Game",
Index:37 name:@"Sound Clip",
Index:38 name:@"Gospel",
Index:39 name:@"Noise",
Index:40 name:@"AlternRock",
Index:41 name:@"Bass",
Index:42 name:@"Soul",
Index:43 name:@"Punk",
Index:44 name:@"Space",
Index:45 name:@"Meditative",
Index:46 name:@"Instrumental Pop",
Index:47 name:@"Instrumental Rock",
Index:48 name:@"Ethnic",
Index:49 name:@"Gothic",
Index:50 name:@"Darkwave",
Index:51 name:@"Techno-Industrial",
Index:52 name:@"Electronic",
Index:53 name:@"Pop-Folk",
Index:54 name:@"Eurodance",
Index:55 name:@"Dream",
Index:56 name:@"Southern Rock",
Index:57 name:@"Comedy",
Index:58 name:@"Cult",
Index:59 name:@"Gangsta",
Index:60 name:@"Top 40",
Index:61 name:@"Christian Rap",
Index:62 name:@"Pop/Funk",
Index:63 name:@"Jungle",
Index:64 name:@"Native American",
Index:65 name:@"Cabaret",
Index:66 name:@"New Wave",
Index:67 name:@"Psychedelic",
Index:68 name:@"Rave",
Index:69 name:@"Showtunes",
Index:70 name:@"Trailer",
Index:71 name:@"Lo-Fi",
Index:72 name:@"Tribal",
Index:73 name:@"Acid Punk",
Index:74 name:@"Acid Jazz",
Index:75 name:@"Polka",
Index:76 name:@"Retro",
Index:77 name:@"Musical",
Index:78 name:@"Rock & Roll",
Index:79 name:@"Hard Rock",
Index:80 name:@"Folk",
Index:81 name:@"Folk-Rock",
Index:82 name:@"National Folk",
Index:83 name:@"Swing",
Index:84 name:@"Fast Fusion",
Index:85 name:@"Bebob",
Index:86 name:@"Latin",
Index:87 name:@"Revival",
Index:88 name:@"Celtic",
Index:89 name:@"Bluegrass",
Index:90 name:@"Avantgarde",
Index:91 name:@"Gothic Rock",
Index:92 name:@"Progressive Rock",
Index:93 name:@"Psychedelic Rock",
Index:94 name:@"Symphonic Rock",
Index:95 name:@"Slow Rock",
Index:96 name:@"Big Band",
Index:97 name:@"Chorus",
Index:98 name:@"Easy Listening",
Index:99 name:@"Acoustic",
Index:100 name:@"Humour",
Index:101 name:@"Speech",
Index:102 name:@"Chanson",
Index:103 name:@"Opera",
Index:104 name:@"Chamber Music",
Index:105 name:@"Sonata",
Index:106 name:@"Symphony",
Index:107 name:@"Booty Bass",
Index:108 name:@"Primus",
Index:109 name:@"Porn Groove",
Index:110 name:@"Satire",
Index:111 name:@"Slow Jam",
Index:112 name:@"Club",
Index:113 name:@"Tango",
Index:114 name:@"Samba",
Index:115 name:@"Folklore",
Index:116 name:@"Ballad",
Index:117 name:@"Power Ballad",
Index:118 name:@"Rhythmic Soul",
Index:119 name:@"Freestyle",
Index:120 name:@"Duet",
Index:121 name:@"Punk Rock",
Index:122 name:@"Drum Solo",
Index:123 name:@"A Capella",
Index:124 name:@"Euro-House",
Index:125 name:@"Dance Hall";

8.3 视频定义的风格类型

Index:4000 name:@"Comedy",
Index:4001 name:@"Drama",
Index:4002 name:@"Animation",
Index:4003 name:@"Action & Adventure",
Index:4004 name:@"Classic",
Index:4005 name:@"Kids",
Index:4006 name:@"Nonfiction",
Index:4007 name:@"Reality TV",
Index:4008 name:@"Sci-Fi & Fantasy",
Index:4009 name:@"Sports",
Index:4010 name:@"Teens",
Index:4011 name:@"Latino TV";
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容