通过使用资源对象,并通过构建它的元数据编辑来了解AVFoundation的特性.
1. 媒体资源AVAsset
AVFoundation 中最重要的就是AVAsset
这个抽象类,它定义了媒体资源混合呈现的方式,将媒体资源的静态属性模块化一个整体.比如标题,时长,元数据等.
我们使用AVAssetTrack
可以从AVAsset资源容器中拿到轨道信息和上面的内容。
AVAsset
主要是抽象化了基本媒体资源的格式 , 跟资源文件一对一映射, 因此我们就不需考虑不种格式,而使用单一统一的方式处理.
- 创建资源
例: 从照片库中视频文件创建一个AVAsset资源
PHPhotoLibrary *libary = [PHPhotoLibrary sharedPhotoLibrary];
[libary performChanges:^{
// 获取视频类资源.可以用类似 NSArray 的接口来访问结果内的集合。它会按需动态加载内容并且缓存最近请求的内容
PHFetchResult *result = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeVideo options:nil];
// 获取第一个视频.
[result enumerateObjectsAtIndexes:[NSIndexSet indexSetWithIndex:0] options:0 usingBlock:^(PHAsset * _Nonnull phAsset, NSUInteger idx, BOOL * _Nonnull stop) {
if (phAsset) {
__weak __typeof(self) weakSelf = self;
// PHAsset转换 AVAsset
[[PHImageManager defaultManager] requestAVAssetForVideo:phAsset options:nil resultHandler:^(AVAsset * avAsset, AVAudioMix * audioMix, NSDictionary * info) {
// dispatch_async(dispatch_get_main_queue(), ^{ // 按需要添加
[weakSelf doSomethingWithAVAsset:avAsset];
//});
}];
}
}];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
if (!success) {
NSLog(@"PHPhotoLibrary error:%@",error);
}
}];
AVAsset资源通常使用
assetWithURL:
方法来获取,但是iOS使用新的了Photos照片库,没有了url的定义,所以这里用PHImageManager来进行转换.
-
访问资源属性:AVAsset使用一种高效的设计模式,即延迟加载资源的属性,只有当使用时候才加载.这样可以快速创建资源 . 但有时资源属性的访问是同步发生的,而正在请求的属性没有预先载入时,就会造成程序阻塞. 所以**要使用异步方式查询资源的属性. **
AVAsset和AVAssetTrack通过AVAsynchronousKeyValueLoading协议
实现异步查询属性功能.
- (void)doSomethingWithAVAsset:(AVAsset*) asset {
NSArray *keys = @[@"tracks"];
// 异步加载资源的tracks属性
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
// 先捕获 tracks 属性状态,再据此做处理
NSError *error = nil;
AVKeyValueStatus status = [asset statusOfValueForKey:@"tracks" error:&error];
switch (status) {
case AVKeyValueStatusLoaded:
break;
case AVKeyValueStatusFailed:
break;
default:
;
}
}];
}
如果想访问资源的多个属性时, 虽然
loadValuesAsynchronouslyForKeys:
只会调用一次,但是每个属性的状态不一定一致,所以要分开判断.
1.1 元数据
每个媒体资源类型都具有唯一的格式,因此开发者通常需要对相应格式的底层读写操作有所了解. 而AVFoundation 框架中的媒体格式都嵌入到了描述其内容的元数据中. 所以我们可以使用一套统一的方法来直接处理元数据 .
**元数据格式: ** Apple环境下媒体类型主要是: QuickTime(mov)、MPEG-4 video(mp4,mpv) 和MPEG-4 audio(m4a)、MPEG-Layer III audio (mp3);
- QuickTime
QuickTime定义了.mov文件的内部结构. 通常一部QuickTime电影会包含两种类型元数据:标准元数据(/moov/meta/ilst/中)和用户元数据(/moov/udta/中).顾名思义: 用户元数据就是包括演唱者,版权信息以及任何对应用程序有帮助的额外信息. - MPEG-4 音频和视频
MP4文件直接派生于QuickTime文件格式,所以很多解析QuickTime文件的工具也能解析MPEG-4.它的元数据保存在/moov/udta/meta/ilst中
该类型文件还有一些变化的扩展名,如.m4v,.m4a,.m4p和.m4b. 它们都使用MPEG-4容器格式,但包含了一些附加的扩展功能;m4v是带有苹果公司针对FairPlay加密和AC3-audio扩展的格式.m4a专门针对音频,告诉使用者此文件只含音频资源.m4b用于有声读物,通常包含章节标签、书签等功能.
- MP3
MP3文件与上面两种有显著区别,MP3不适用容器格式,而使用编码音频数据,使用ID3v2格式来保存音频的描述信息,包含演唱者,唱片公司等信息.AVFoundation只支持MP3的解码读取.
1.2 获取元数据
AVAsset和AVAssetTrack都可以实现查询相关元数据的功能,通过AVMetadataItem
类的接口来访问元数据.大部分情况我们使用AVAsset,除非你要获取低层级元数据的信息. 另外AVFoundation使用键空间(keys space)将相关键组合在一起.以方便实现对AVMetadataItem
实例集合分类筛选.每个资源至少包含两个键空间: commonMetadata
和availableMetadataFormats
. 前者用来定义所有支持的媒体类型的键,包括:曲名,歌手,插图等常见元素. 后者用来包含用来定义元数据格式的NSString对象和相关元数据信息的NSArray.
#define COMMON_META_KEY @"commonMetadata"
#define AVAILABLE_META_KEY @"availableMetadataFormats"
// 两种键空间
NSArray *keys = @[COMMON_META_KEY, AVAILABLE_META_KEY];
// ios支持的元数据格式
NSArray *acceptedFormats = @[
AVMetadataFormatQuickTimeMetadata,
AVMetadataFormatiTunesMetadata,
AVMetadataFormatID3Metadata
];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
// 1. 判断资源属性加载状态.
AVKeyValueStatus commonStatus =
[self.asset statusOfValueForKey:COMMON_META_KEY error:nil];
AVKeyValueStatus formatsStatus =
[self.asset statusOfValueForKey:AVAILABLE_META_KEY error:nil];
self.prepared = (commonStatus == AVKeyValueStatusLoaded) &&
(formatsStatus == AVKeyValueStatusLoaded);
// 2. 获取各键空间的元数据
if (self.prepared) {
for (AVMetadataItem *item in self.asset.commonMetadata) {
//NSLog(@"%@: %@", item.keyString, item.value);
}
for (NSString *format in self.asset.availableMetadataFormats) { // 查询此资源中所包含的所有元数据格式
if ([acceptedFormats containsObject:format]) { // 是否支持此格式.
NSArray *items = [self.asset metadataForFormat:format]; // 访问对应格式元数据.
for (AVMetadataItem *item in items) {
//NSLog(@"%@: %@", item.keyString, item.value);
}
}
}
}
}];
}
AVMetadataItem 最基本形式是一个键值对的容器.可以通过它查询key或commonKey来访问元数据. 但是它的key属性是以id<NSObject,NSCopying>
值的形式定义的,虽然可以保存NSString,但通过上面的打印可以知道Key只是无意义的整数.所以我们最好添加一个AVMetadataItem分类方法,用来转换获取key的内容.代码如下:
@interface AVMetadataItem (THAdditions)
@property (readonly) NSString *keyString;
// .m
- (NSString *)keyString {
if ([self.key isKindOfClass:[NSString class]]) {
return (NSString *)self.key;
} else if ([self.key isKindOfClass:[NSNumber class]]) {
UInt32 keyValue = [(NSNumber *) self.key unsignedIntValue];
//大部分keys 有 4 字符长度,而 ID3v2.2 格式的keys 只有3个字符,下面代码表示移动每个字节来确定length长度是要截短.
size_t length = sizeof(UInt32);
if ((keyValue >> 24) == 0) --length;
if ((keyValue >> 16) == 0) --length;
if ((keyValue >> 8) == 0) --length;
if ((keyValue >> 0) == 0) --length;
long address = (unsigned long)&keyValue;
address += (sizeof(UInt32) - length);
// keys 是以big-endian(高位优先)格式存储的.需要转换成符合主流CPU顺序的little-endian格式.
keyValue = CFSwapInt32BigToHost(keyValue);
// 创建一个字符数组,以keys字符字节填充
char cstring[length];
strncpy(cstring, (char *) address, length);
cstring[length] = '\0';
// 大部分QuickTime和iTunes keys前缀都有一个 '©', 而AVMetadataFormat.h 用'@' 表示,所以转换一下.
if (cstring[0] == '\xA9') {
cstring[0] = '@';
}
return [NSString stringWithCString:(char *) cstring
encoding:NSUTF8StringEncoding];
}
else {
return @"<<unknown>>";
}
}
除了通过键和键空间获取资源的元数据之外,iOS 8之后添加了用identifier获取元数据的方法. 标识符将键和键空间统一成单独的字符串,以一个更简单的模型来获取资源的元数据,具体参考AVMetadataItem.h, 这里使用键和键空间是为了方便兼容以前的系统.
1.3元数据的解析
通过上面的方法获得元数据以及keys属性的内容转换之后,接下来到了最难的部分,就是理解key对应value中的数据.当value是一个简单字符串时,比如歌手或唱片名称或年份,这样容易理解的是不需要转换的.但是有很多复杂key的value需要转换解析:
-
Artwork:
元数据Artwork对应的value会以多种不同的格式返回,比如封面和海报等,它保存在一个NSData中,我们要先定位
// 解析
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
NSImage *image = nil;
if ([item.value isKindOfClass:[NSData class]]) {
image = [[NSImage alloc] initWithData:item.dataValue];
}
else if ([item.value isKindOfClass:[NSDictionary class]]) { // 如果对象是MP3格式.value就可能是个字典.
NSDictionary *dict = (NSDictionary *)item.value;
image = [[NSImage alloc] initWithData:dict[@"data"]];
}
return image;
}
// 恢复原格式
- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
withMetadataItem:(AVMetadataItem *)item {
AVMutableMetadataItem *metadataItem = [item mutableCopy];
NSImage *image = (NSImage *)value;
metadataItem.value = image.TIFFRepresentation;
return metadataItem;
}
- 注释:
当处理对象是MPEG-4或QuickTime时,可以直接获取对应字符串,如果是mp3格式:
// 解析
- (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;
}
-
音轨数据信息:
音轨通常包含一首歌在整个唱片中的编号位置信息.
// 解析
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
NSNumber *number = nil;
NSNumber *count = nil;
if ([item.value isKindOfClass:[NSString class]]) { // MP3音频信息以"xx/xx"字符串格式返回.例如一个包含10首歌曲的唱片中第8首就是8/10;
NSArray *components =
[item.stringValue componentsSeparatedByString:@"/"];
number = @([components[0] integerValue]);
count = @([components[1] integerValue]);
}
else if ([item.value isKindOfClass:[NSData class]]) { // 对于MPEG-4格式,则复杂点.
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 dictionary];
[dict setObject:number ?: [NSNull null] forKey:THMetadataKeyTrackNumber]; // 得到的音轨编号
[dict setObject:count ?: [NSNull null] forKey:THMetadataKeyTrackCount];// 得到的音轨计数
return dict;
}
// 恢复
- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
withMetadataItem:(AVMetadataItem *)item {
AVMutableMetadataItem *metadataItem = [item mutableCopy];
NSDictionary *trackData = (NSDictionary *)value;
NSNumber *trackNumber = trackData[THMetadataKeyTrackNumber];
NSNumber *trackCount = trackData[THMetadataKeyTrackCount];
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;
}
- 风格信息:
乡村,爵士,蓝调等等.
// 转换
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
THGenre *genre = nil;
if ([item.value isKindOfClass:[NSString class]]) { // 1
if ([item.keySpace isEqualToString:AVMetadataKeySpaceID3]) {
// ID3v2.4 stores the genre as an index value
if (item.numberValue) { // 2
NSUInteger genreIndex = [item.numberValue unsignedIntValue];
genre = [THGenre id3GenreWithIndex:genreIndex];
} else {
genre = [THGenre id3GenreWithName:item.stringValue]; // 3
}
} else {
genre = [THGenre videoGenreWithName:item.stringValue]; // 4
}
}
else if ([item.value isKindOfClass:[NSData class]]) { // 5
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]]) { // 6
metadataItem.value = genre.name;
}
else if ([item.value isKindOfClass:[NSData class]]) { // 7
NSData *data = item.dataValue;
if (data.length == 2) {
uint16_t value = CFSwapInt16HostToBig(genre.index + 1); // 8
size_t length = sizeof(value);
metadataItem.value = [NSData dataWithBytes:&value length:length];
}
}
return metadataItem;
}
1.4 导出修改后的元数据
通过上面的解析转换方法,我们就可以进行元数据信息的读取与修改,修改完当然需要保存了.不过中间还有一个问题: 由于AVAsset是一个不可变类,所以我们不能直接修改AVAsset,而是使用AVAssetExportSession
类导出一个新的资源副本.
-
AVAssetExportSession配置
AVAssetExportSession是用于将AVAsset内容根据预设的导出条件进行转码,并写入磁盘中,用它可以实现将一种格式转换成另一种格式,修订资源内容,修改资源的音视频行为.也包含了写入新的元数据.
所以创建AVAssetExportSession实例要先提供资源和预设条件; 导出预设用于确定导出内容的质量,大小等属性. 创建完成后还需要指定一个outputURL写入地址,并且给outputFileType一个格式.代码如下:
- (void)saveWithCompletionHandler:(THCompletionHandler)handler {
// 先用AVAssetExportPresetPassthrough预设值创建一个AVAssetExportSession
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) { // 4
NSURL *sourceURL = self.url;
NSFileManager *manager = [NSFileManager defaultManager];
[manager removeItemAtURL:sourceURL error:nil];
[manager moveItemAtURL:outputURL toURL:sourceURL error:nil];
}
if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(success);
});
}
}];
}
AVAssetExportPresetPassthrough预设值允许修改预设值,但是不能用于添加元数据,如果要添加元数据,需要使用转码预设值.