参看文章 写的很用心
AVFoundation 之资源和元数据
一、资源介绍
AVFoundation 所有的代码设计都围绕资源 (AVAsset) 进行, AVAsset 是 AVFoundation 设计的核心.
-
AVAsset 不需要考虑的2个重要范畴:
-
(1) 、它提供了对基本媒体格式的层抽象,这意味着无论是处理MPEG-4 视频还是MP3音频, 对你而言面对的只有资源这个概念 .
-
(2) 、不用我们管理资源的位置信息, 不管是本地的URL 还是远程服务器上的一个音频流、视频流的URL
AVAsset本身并不是媒体资源,但是它可以作为实际媒体的容器,它由一个或多个带有描述自身元数据的媒体组成. 我们使用AVAssetTrack 类代表保存在资源中的统一类型媒体,并对每个资源建立相应的模型. AVAssetTrack 最常见的形态就是音频和视频流,但是它还可以 表示文本、副标题或者隐藏字幕等媒体类型.
二、资源创建
为一个媒体资源创建AVAsset对象,可以通过URL对它进行初始化来实现,可以是本地的URL,也可以是远程的URL
NSURL *url = [NSURL URLWithString:@""];
AVAsset *asset = [AVAsset assetWithURL:url];
AVAsset 是一个抽象类,意味着不能被直接实例化. 当创建实例时,实际上是创建了它子类的一个实例对象, 子类名为AVURLAsset. 有时我们会直接只用这个类,因为它允许通过传递选项字典来精细调整资源的创建方式. 比如创建一个用在音频或视频编辑场景中的资源,希望传递一个选项来告诉程序提供更精确的时长和计时信息.
NSURL *url = [NSURL URLWithString:@""];
NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey : @YES};
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:options];
三、异步载入
AVAsset 具有多种方法和属性,可以提供有关资源的信息,比如: 专辑名、作者、时长、创建日期、元数据等. (这些信息都存储在AVMetadataItem里面的, 比如有标题的AVMetadataItem, 有作者的AVMetadataItem 等等) AVAsset 使用了一种高效的设计方法,延迟载入资源属性,直到请求时才加载,这样可以快速创建资源. 不过属性的访问是同步的,如果正在请求的属性没有预先载入,程序就会阻塞,所以开发者应该使用异步的方式来查询资源的属性.
NSURL *url = [[NSBundle mainBundle] URLForResource:@"abc" withExtension:@"mp4"];
AVAsset *asset = [AVAsset assetWithURL:url];
[asset loadValuesAsynchronouslyForKeys:@[@"duration"] completionHandler:^{
NSError *error;
AVKeyValueStatus status = [asset statusOfValueForKey:@"duration" error:&error];
switch (status) {
case AVKeyValueStatusLoaded: // 加载完成
{
dispatch_async(dispatch_get_main_queue(), ^{
CMTimeShow(asset.duration);
});
break;
}
case AVKeyValueStatusFailed:
break;
case AVKeyValueStatusCancelled:
break;
default:
break;
}
}];
取消加载
// 取消加载
[asset cancelLoading];
补充说明:
如果想访问资源的多个属性时, 虽然loadValuesAsynchronouslyForKeys:只会调用一次,但是每个属性的状态不一定一致,所以要分开判断.
四、 媒体元数据
4.1、 元数据格式
我们在Apple 环境遇到的媒体数据主要有4种类型:
- Quick Time (.mov)
- MPEG-4 video (.mp4 和 .m4v)
- MPEG-4 audio (.m4a)
- MPEG-Layer III audio (.mp3)
QuickTime (.mov)
QuickTime 是由苹果开发的一种功能强大、跨平台的媒体架构,了解QuickTime 格式的一个好的方法就是在十六进制的编译器中打开一个 .mov 格式的文件, 常见的十六进制编译器有Hex Fiend 或Synaly It! Pro . 更好的方法是借助 Apple Developer Center 中找到 Atom Inspector工具, 它可以将 atom 结构以 NSOutlineView 方式呈现, 所以可以对atom 之间的继承关系等信息有比较清晰的了解.
上图为 QuickTime 结构的简化示意图, 该文件最小限度的包含了三个高级Atom: 分别用于描述文件的类型和兼容类型的ftyp, 包含实际音视频媒体的mdat 以及非常重要的moov atom, 它对媒体资源的所有相关细节做了完整的描述,包括可呈现的元数据.
MPEG-4 video (.mp4 和 .m4v ) 和 MPEG-4 audio (.m4a)
MP4 直接派生于QuickTime 文件格式, 这就意味着它的结构是类似的.实际上,我们经常会发现,能够解析一种文件类型的工具也可以处理其它文件类型,就像QuickTime 文件一样,MP4文件也是由atom 数据结构组成.
MP4 是对MPEG-4 媒体的标准扩展, m4a、m4b 、m4p、m4v 这些变体都使用 MPEG-4容器格式,但是包含了附加的拓展功能.
- m4v 文件是带有针对 FairPlay 加密和 AC3-audio 扩展的MPEG-4 视频格式.如果不涉及这些mp4 和 m4v 仅仅是扩展名不同而已.
- m4a 针对音频, 让使用者知道该文件只带有音频资源.
- m4p 是苹果较旧的Itunes 音频格式,使用其FairPlay 扩展.
- m4b 用于有声读物,通常包含章节标签和书签功能
MPEG-Layer III audio (.mp3)
mp3 文件和上面两种有显著的区别, mp3 文件不使用容器格式, 而使用编码音频数据,包含的可选元数据的结构块通常位于文件的开头. MP3 文件使用一种称为 id3v2 格式来保存关于音频内容的描述信息. 包含的信息有歌曲演唱者、 唱片信息、 音乐风格等.
AVFoundation 支持读取id3v2 标签的所有版本, 但是不支持写入, 所以AVFoundation 无法支持对mp3 进行保存.
4.2、 获取元数据
AVAsset 和 AVAssetTrack 都可以实现查询相关元数据的功能.
一般使用AVAsset 提供的元数据当涉及获取曲目一级元数据等情况时会使用AVAssetTrack
NSURL *audioURL = [[NSBundle mainBundle] URLForResource:@"一次就好" withExtension:@"mp3"];
AVAsset *asset = [AVAsset assetWithURL:audioURL];
[asset loadValuesAsynchronouslyForKeys:@[@"availableMetadataFormats"] completionHandler:^{
AVKeyValueStatus commonStatus = [asset statusOfValueForKey:@"availableMetadataFormats" error:nil];
if (commonStatus == AVKeyValueStatusLoaded) {
NSMutableArray *metaData = [NSMutableArray array];
for (NSString *format in asset.availableMetadataFormats) {
[metaData addObjectsFromArray:[asset metadataForFormat:format]];
}
// 歌曲名称
AVMetadataItem *titleItem = [AVMetadataItem metadataItemsFromArray:metaData withKey:AVMetadataCommonKeyTitle keySpace:AVMetadataKeySpaceCommon].firstObject;
// 演唱者
AVMetadataItem *artistItem = [AVMetadataItem metadataItemsFromArray:metaData withKey:AVMetadataCommonKeyArtist keySpace:AVMetadataKeySpaceCommon].firstObject;
// 专辑名称
AVMetadataItem *albumItem = [AVMetadataItem metadataItemsFromArray:metaData withKey:AVMetadataCommonKeyAlbumName keySpace:AVMetadataKeySpaceCommon].firstObject;
NSLog(@"%@ : %@", titleItem.key, titleItem.value); // TIT2 : 一次就好
NSLog(@"%@ : %@", artistItem.key, artistItem.value); // TPE1 : 沈腾
NSLog(@"%@ : %@", albumItem.key, albumItem.value); // TALB : 夏洛特烦恼 电影原声带
}
}];
上面我们使用的是键和键空间来获取元数据,iOS8 之后还引进了标识符获取元数据的方法:
// 歌曲名称
AVMetadataItem *titleItem = [AVMetadataItem metadataItemsFromArray:metaData filteredByIdentifier:AVMetadataCommonIdentifierTitle].firstObject;
// 演唱者
AVMetadataItem *artistItem = [AVMetadataItem metadataItemsFromArray:metaData filteredByIdentifier:AVMetadataCommonIdentifierArtist].firstObject;
// 专辑名称
AVMetadataItem *albumItem = [AVMetadataItem metadataItemsFromArray:metaData filteredByIdentifier:AVMetadataCommonIdentifierAlbumName].firstObject;
注意:
有时候直接打印 Key 为一串数字, 所以我们创建一个AVMetadataItem的分类,将数字装换成字符串.
NSLog(@"%@ : %@", titleItem.keyString, titleItem.value);
NSLog(@"%@ : %@", artistItem.keyString, artistItem.value);
NSLog(@"%@ : %@", albumItem.keyString, albumItem.value);
#import "AVMetadataItem+Extend.h"
@implementation AVMetadataItem (Extend)
- (NSString *)keyString
{
// 如果 key 是一个字符串,则原样返回
if ([self.key isKindOfClass:[NSString class]]) {
return (NSString *)self.key;
}
else if ([self.key isKindOfClass:[NSNumber class]]) {
UInt32 keyValue = [(NSNumber *)self.key unsignedIntValue];
// 大部分情况下,key 是一个 4 字符代码,比如 ©gen 或 TRAK,不过对于 mp3 文件,键值只有 3 个字符的长度
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);
// 由于数字是 big endian 格式,因此使用 CFSwapInt32BigToHost() 函数将其转换为符合主 CPU 顺序的 little endian 格式
keyValue = CFSwapInt32BigToHost(keyValue);
// 创建一个字符数组,并使用 strncpy 函数将字符字节填充到该数组中
char cstring[length];
strncpy(cstring, (char *) address, length);
cstring[length] = '\0';
// 大量 QuickTime 用户数据和 iTunes key 的前缀都带有一个 © 符号
// 不过 AVMetadataFormat.h 中定义 key 所使用的前缀符号为 @
// 所以为了进行 key 常量字符串比较,需要先将 © 替换为 @
if (cstring[0] == '\xA9') {
cstring[0] = '@';
}
return [NSString stringWithCString:(char *) cstring
encoding:NSUTF8StringEncoding];
}
else {
return @"<<unknown>>";
}
}
@end
总结
找不 到相关的资料, 根据我的理解, availableaMetadataFormats 、metadata、commonMetadata 三者之间的关系如下:
-
availableMetadataFormats:
NSMutableArray<AVMetadataItem *> *metadataItems = [NSMutableArray array]; // 获取所有的<AVMetadataFormat> metadataFormats NSArray<AVMetadataFormat> *metadataFormats =asset.availableMetadataFormats; //遍历所有的<AVMetadataFormat> metadataFormats for (AVMetadataFormat format in metadataFormats){ // 根据 AVMetadataFormat 获取对应的AVMetadataItems NSArray<AVMetadataItem *> *items = [asset metadataForFormat:format]; metadataItems addObjectsFromArray:items]; }
-
metadata: 通过asset.metadata 方法可以直接得到所有的元数据,据测和 availableMetadataFormats 获取到的数据相同.
NSArray<AVMetadataItem *> *metadataItems = [asset metadata];
-
commonMetadata:通过 asset.commonMetadata 方法直接可以得到常用的元数据,但是获取的不全
所以我认为最简单获取元数据方法如下
NSURL *audioURL = [[NSBundle mainBundle] URLForResource:@"一次就好" withExtension:@"mp3"];
AVAsset *asset = [AVAsset assetWithURL:audioURL];
[asset loadValuesAsynchronouslyForKeys:@[@"metadata"] completionHandler:^{
AVKeyValueStatus commonStatus = [asset statusOfValueForKey:@"metadata" error:nil];
if (commonStatus == AVKeyValueStatusLoaded) {
// 歌曲名称
AVMetadataItem *titleItem = [AVMetadataItem metadataItemsFromArray:asset.metadata filteredByIdentifier:AVMetadataCommonIdentifierTitle].firstObject;
// 演唱者
AVMetadataItem *artistItem = [AVMetadataItem metadataItemsFromArray:asset.metadata filteredByIdentifier:AVMetadataCommonIdentifierArtist].firstObject;
// 专辑名称
AVMetadataItem *albumItem = [AVMetadataItem metadataItemsFromArray:asset.metadata filteredByIdentifier:AVMetadataCommonIdentifierAlbumName].firstObject;
NSLog(@"%@ : %@", titleItem.keyString, titleItem.value);
NSLog(@"%@ : %@", artistItem.keyString, artistItem.value);
NSLog(@"%@ : %@", albumItem.keyString, albumItem.value);
}
}];
4.3、 编辑元数据
AVAssetExportSession 用于将 AVAsset 内容根据导出预设条件进行转码,并将导出资源写到磁盘中。
AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:_asset presetName:AVAssetExportPresetPassthrough];
session.outputURL = [self tempURL];
session.outputFileType = [self fileType];
session.metadata = [self.metadata metadataItems];
[session exportAsynchronouslyWithCompletionHandler:^{
if (session.status == AVAssetExportSessionStatusCompleted) {
[[NSFileManager defaultManager] removeItemAtURL:_url error:nil];
[[NSFileManager defaultManager] moveItemAtURL:session.outputURL toURL:_url error:nil];
}
}];
注意:
AVAssetExportPresetPassthrough 可以修改 MPEG-4 和 QuickTime 容器中存在的元数据信息,不过它不能添加新的元数据。添加元数据唯一方法是使用转码预设值。此外,它不能修改 ID3 标签,不支持写入 MP3 数据。