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  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 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  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 
// 将AVMetadataItem中的数据取出转换为可展示数据
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item;

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

4 THMetadataConverterFactory

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

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

@implementation THMetadataConverterFactory
- (id )converterForKey:(NSString *)key {
    id  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 
@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";

你可能感兴趣的:(AVFoundation 元数据读取及写入)