主页 Learning AV Foundation(四)AVAsset元数据(高级篇)
Post
Cancel

Learning AV Foundation(四)AVAsset元数据(高级篇)

前言

先上图

这一篇 我们将学习解决如何一套代码解析大部分 多媒体格式的文件然后形成通用的 model - 元数据键值空间标准化

内容介绍

结构图


class 代码

  • MediaItem (一个直接对外的接口)
  • MetaData (元数据model)
  • Genre (风格)
  • AVMetadataItem+Additions
  • MetadataDefines
  • MetadataKit
  • Converters (文件夹包含如下:)
    • MetadataConverter (Protocol 存取 AVMetadataItem)
    • MetadataConverterFactory
    • DefaultMetadataConverter
    • ArtworkMetadataConverter
    • CommentMetadataConverter
    • TrackMetadataConverter
    • DiscMetadataConverter
    • GenreMetadataConverter

MediaItem

这个类主要对外直接暴露接口 如下代码即可调用使用

1
2
3
4
5
6
7
8
9
__weak typeof(self) weakSelf = self;
MediaItem *item = [[MediaItem alloc] initWithURL:self.url];
[item prepareWithCompletionHandler:^(BOOL complete) {
    __strong typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf refreshDataByItem:item];
    NSLog(@"%@",[item modelDescription]);
}];


代码实现部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import "MetaData.h"
typedef void(^CompletionHandler)(BOOL complete);
@interface MediaItem : NSObject
@property (strong, readonly) NSString *filename;
@property (strong, readonly) NSString *filetype;
@property (strong, readonly) MetaData *metadata;
@property (readonly, getter = isEditable) BOOL editable;
- (id)initWithURL:(NSURL *)url;
/**
 此方法完成之后如果成功即可取metadata

 @param handler 回调 block
 */
- (void)prepareWithCompletionHandler:(CompletionHandler)handler;
- (void)saveWithCompletionHandler:(CompletionHandler)handler;
@end
@end

.m可参考源码 比较多就不赘述了

当 block 完成时使用 目前支持获取元数据信息的媒体格式如下:

  • m4a
  • mov
  • mp4
  • mp3

注意:mp3文件是不可编辑的文件故而不能进行编辑 比如改变歌手名称之类 如果要编辑可使用其它专业软件尝试

我尝试了 mac 版本的 demo 编辑 文件 是 OK 的 但是在 iOS 上 我更改其它格式也没能保存成功 如果你看到有解决办法 可以留言给我或者发邮件给我 非常感谢.

MetaData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
@class Genre; //风格  eg: 蓝调、 古典 ....
@interface MetaData : 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) UIImage *artwork;
@property (strong) Genre *genre;
@property NSString *year;
@property id bpm;
@property NSNumber *trackNumber;
@property NSNumber *trackCount;
@property NSNumber *discNumber;
@property NSNumber *discCount;
- (void)addMetadataItem:(AVMetadataItem *)item withKey:(id)key;
- (NSArray *)metadataItems;
@end


看到上边的代码估计你也猜到了 这就是我们需要的 比如 mp3文件解析出来的真正 model

这里东西比较多 有些值有可能没有 请自行做好 check

MetadataConverter

这个协议是为了支持所有多媒体文件统一解析使用,比如:mp3文件和mp4文件两个是不一样的文件格式,虽然里面有很多相同的key,但是肯定数据结构是不一样的,这样就要求,搞一个统一的协议,比如输入的是一个URL返回一个 model那么为了解决key value参差不齐问题 就搞了这个协议.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@protocol zh <NSObject>
@optional
/**
 AVMetadataItem to Model 转换 用于UI显示的model

 @param item AVMetadataItem
 @return model
 */
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item;
/**
 AVMetadataItem映射通用字段
 
 @param value 通过媒体元数据取出的某个key的value
 @param item AVMetadataItem
 @return AVMetadataItem
 */
- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item;
@end

MetadataConverterFactory

这个类用于统一输出遵守MetadataConverter协议的model并且找到适当的转换器去转换响应的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@interface MetadataConverterFactory : DefaultMetadataConverter
- (id <MetadataConverter>)converterForKey:(NSString *)key;
@end

@implementation MetadataConverterFactory
- (id <MetadataConverter>)converterForKey:(NSString *)key{
    id <MetadataConverter> converter = nil;
    if ([key isEqualToString:MetadataKeyArtwork]) {
        converter = [[ArtworkMetadataConverter alloc] init];
    } else if ([key isEqualToString:MetadataKeyTrackNumber]) {
        converter = [[TrackMetadataConverter alloc] init];
    } else if ([key isEqualToString:MetadataKeyDiscNumber]) {
        converter = [[DiscMetadataConverter alloc] init];
    } else if ([key isEqualToString:MetadataKeyComments]) {
        converter = [[CommentMetadataConverter alloc] init];
    } else if ([key isEqualToString:MetadataKeyGenre]) {
        converter = [[GenreMetadataConverter alloc] init];
    } else {
        converter = [[DefaultMetadataConverter alloc] init];
    }
    return converter;
}
@end

DefaultMetadataConverter

简单实现MetadataConverter协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface DefaultMetadataConverter : NSObject <MetadataConverter>

@end

@implementation DefaultMetadataConverter

- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    return item.value;
}

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {    
    AVMutableMetadataItem *metadataItem = [item mutableCopy];
    metadataItem.value = value;
    return metadataItem;
}


ArtworkMetadataConverter

实现MetadataConverter协议 取出专辑封面

此处省略 .h 文件只贴出.m ( .h里面啥也没有 大家可参考 demo)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@implementation ArtworkMetadataConverter
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    UIImage *image = nil;  //下面是核心代码取出图片 
    if ([item.value isKindOfClass:[NSData class]]) {                        // 1
        image = [[UIImage alloc] initWithData:item.dataValue];
    }
    else if ([item.value isKindOfClass:[NSDictionary class]]) {             // 2
        NSDictionary *dict = (NSDictionary *)item.value;
        image = [[UIImage alloc] initWithData:dict[@"data"]];
    }
    return image;
}

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {
    
    AVMutableMetadataItem *metadataItem = [item mutableCopy];
    
    UIImage *image = (UIImage *)value;
    metadataItem.value = UIImagePNGRepresentation(image);                          // 3
    
    return metadataItem;
}

@end

这里 mp3 (id3v2格式)取图片的方式可能有不一样的地方 1出判断 属于哪种格式 3处把 UIImage 转 NSData 再放回去

需要注意一个地方是 返回AVMetadataItem的类型 由于AV Foundation无法写入 ID3元数据 所以这里使用了 AVMutableMetadataItem来存储封面图

AVMutableMetadataItemAVMetadataItem的子类

CommentMetadataConverter 注释转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@implementation CommentMetadataConverter

- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    
    NSString *value = nil;
    if ([item.value isKindOfClass:[NSString class]]) {                      // 1
        value = item.stringValue;
    }
    else if ([item.value isKindOfClass:[NSDictionary class]]) {             // 2
        NSDictionary *dict = (NSDictionary *) item.value;
        if ([dict[@"identifier"] isEqualToString:@""]) {
            value = dict[@"text"];
        }
    }
    return value;
}

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {
    
    AVMutableMetadataItem *metadataItem = [item mutableCopy];               // 3
    metadataItem.value = value;
    return metadataItem;
}
@end
  1. MPEG-4QuickTime媒体的 value 为 NSString
  2. MP3的注释保存在一个定义ID3 COMM帧NSDictionary中(如果处理的是ID3V2.2,则为COM),所有类型的值都保存在这个帧中. eg: iTune 在这个帧中保存音频标准化和无缝播放设置等,意味着当请求 ID3元数据时需要多接收多个COMM帧.包含实际注释内容的特定COMM帧被存储在一个带有空字符串标识的帧中.找到需要的条目后 通过请求text key 来检索出注释内容

TrackMetadataConverter 音轨数据转换

音轨: 通常包含一首歌曲在整个唱片中的编号位置信息(eg: 12首歌中的第4首 4/12)等信息.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@implementation TrackMetadataConverter
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    
    NSNumber *number = nil;
    NSNumber *count = nil;
    
    if ([item.value isKindOfClass:[NSString class]]) {                      // 1
        NSArray *components =
        [item.stringValue componentsSeparatedByString:@"/"];
        if (components.count > 0) {
            number = @([components[0] integerValue]);
        }
        if (components.count > 1) {
            count = @([components[1] integerValue]);
        }
    }
    else if ([item.value isKindOfClass:[NSData class]]) {                   // 2
        NSData *data = item.dataValue;
        if (data.length == 8) {
            uint16_t *values = (uint16_t *) [data bytes];
            if (values[1] > 0) {
                number = @(CFSwapInt16BigToHost(values[1]));                // 3
            }
            if (values[2] > 0) {
                count = @(CFSwapInt16BigToHost(values[2]));                 // 4
            }
        }
    }
    
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];           // 5
    [dict setObject:number ?: [NSNull null] forKey:MetadataKeyTrackNumber];
    [dict setObject:count ?: [NSNull null] forKey:MetadataKeyTrackCount];
    
    return dict;
}
- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {
    AVMutableMetadataItem *metadataItem = [item mutableCopy];
    
    NSDictionary *trackData = (NSDictionary *)value;
    NSNumber *trackNumber = trackData[MetadataKeyTrackNumber];
    NSNumber *trackCount = trackData[MetadataKeyTrackCount];
    
    uint16_t values[4] = {0};                                                // 6
    
    if (trackNumber && ![trackNumber isKindOfClass:[NSNull class]]) {
        values[1] = CFSwapInt16HostToBig([trackNumber unsignedIntValue]);   // 7
    }
    
    if (trackCount && ![trackCount isKindOfClass:[NSNull class]]) {
        values[2] = CFSwapInt16HostToBig([trackCount unsignedIntValue]);    // 8
    }
    size_t length = sizeof(values);
    metadataItem.value = [NSData dataWithBytes:values length:length];       // 9
    
    return metadataItem;
}
@end
  1. 刚才所说 mp3格式已 xx/xx 格式的字符串标识一个歌曲 在整个唱片中的第几首 所以我们用/分割
  2. iTunes M4A文件的唱片信息保存在一个 NSData 中,NSData包含3个16位的big encoding数字,如果直接在控制台打印 NSData 会输出<00000008 000a0000>这是4个16位的big endian数字数组的十六进制表现形式. 数组中第2个和第3个元素分别保存唱片编号和唱片计数值
  3. 如果唱片编号 != 0, 则获取该值并使用CFSwapInt16BigToHost()函数执行endian转换,转换成一个little endian 并打包成NSNumber
  4. 同样如果音轨计数值不为0, 则获取该值并在字节上执行endian转换并打包成NSNumber
  5. 步骤反过来换成3个uint16_t 保存音轨编号和计数值.
  6. 如果音轨编号有效, 将字节转换为big endian格式并保存到数组第2个位置
  7. 如果音轨计数值有效, 将字节转换为big endian格式并保存到数组第3个位置
  8. 打成 NSData 保存将其设置为元数据项的 value

DiscMetadataConverter 唱片数据转换

唱片计数信息用于表示一首歌曲所在的CD是所有唱片中的第几张 通常都是 1/1 (通常都是一个 cd 一首)

上下的和音轨 非常类似了 如果是4/10就是 10首里面的第4首 由于唱片这玩意都过时了 你现在应该很少看到 屌丝 带着 walkman 在大街上压马路了都看不到了

但是逻辑还是在的 这里逻辑看代码吧 和 音轨 基本一模一样

GenreMetadataConverter 风格转换

数字音频使用的标准风格最初来自 MP3. ID3 规范定义了80个默认的风格类型及 另外46个 WinAmp 扩展,共计 126个风格. 不过这些都不属于正式格式. 由于 mp3风格的主导地位比较明显, iTunes 没有另造轮子,而是基本遵循 ID3 的风格分类,不过做了点小变化。iTunes 音乐风格的标号比响应的 ID3标识符大 1 .

虽然 iTunes 使用了 ID3集合中的预定义音乐风格, 不过 iTunes 对电视、电影和有声读物等定义了自己的风格集. Apple’s Genre IDs Appendix

示例代码已经包含了这些类型 虽不在赘述 请参考 demo

保存元数据

AVAsset是一个不可变类型 我们不能直接修改 AVAsset 而是使用AVAssetExportSession类来导出新的资源副本以及元数据的改动.

使用AVAssetExportSession

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- (void)saveWithCompletionHandler:(CompletionHandler)handler {
    
    NSString *presetName = AVAssetExportPresetPassthrough;                  // 1
    AVAssetExportSession *session =
    [[AVAssetExportSession alloc] initWithAsset:self.asset
                                     presetName:presetName];
    
    NSURL *outputURL = [self tempURL];                                      // 2
    session.outputURL = outputURL;
    session.outputFileType = self.filetype;
    session.metadata = [self.metadata metadataItems];                       // 3
    
    [session exportAsynchronouslyWithCompletionHandler:^{
        AVAssetExportSessionStatus status = session.status;
        BOOL success = (status == AVAssetExportSessionStatusCompleted);
        if (success) {                                                      // 4
            NSURL *sourceURL = self.url;
            NSFileManager *manager = [NSFileManager defaultManager];
            [manager removeItemAtURL:sourceURL error:nil];
            [manager moveItemAtURL:outputURL toURL:sourceURL error:nil];
            [self reset];                                                   // 5
        }
        
        if (handler) {
            dispatch_async(dispatch_get_main_queue(), ^{
                handler(success);
            });
        }
        NSLog(@"sessionError:%@",session.error);
    }];
}


- (NSURL *)tempURL {
    // 获取Caches目录路径
    NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    NSString *tempDir = cachesDir;
    NSString *ext = [[self.url lastPathComponent] pathExtension];
    NSString *tempName = [NSString stringWithFormat:@"temp.%@", ext];
    NSString *tempPath = [tempDir stringByAppendingPathComponent:tempName];
    return [NSURL fileURLWithPath:tempPath];
}

注意: **AVAssetExportPresetPassthrough 这个预设值 确实允许修改MPEG-4QuickTime容器中的存在的元数据信息, 不过它不可以添加新的元数据,添加元数据的唯一方法是使用转码预设值, 此外不能修改 ID3(mp3)标签。 框架不支持写入 MP3数据.**

总结

经过了代码实现和解析多媒体元数据 AVAsset,我们也熟悉了多媒体文件的构造, ID3(MP3)格式的文件解析 arkwork 功能. 从而在后续开发过程中 提升开发效率. 最后放出 代码的 demo 请大家多多指教

示例 demo

该博客文章由作者通过 CC BY 4.0 进行授权。

iOS 11 新技能

ARKit