《AV Foundation 开发秘籍》读书笔记(三)

第三章 资源和元数据

1. 资源简介

AV Foundation 把所有的代码设计围绕资源(assert)进行,AVAssert 是 AV Foundation 设计的核心。

AVAssert 不需要考虑的两个重要范畴:第一、它提供了对基本媒体格式的层抽象,这意味着无论是处理 MPEG-4 视频还是 MP3 音频,对你而言面对的只有资源这个概念。第二、不用我们管理资源的位置信息,不管是本地 URL 还是远程服务器上的一个音频流、视频流的 URL。

AVAssert 本身并不是媒体资源,但是它可以作为时基媒体的容器,它由一个或多个带有描述自身元数据的媒体组成。我们使用 AVAssetTrack 类代表保存在资源中的统一类型媒体,并对每个资源建立相应的模型。AVAssetTrack 最常见的形态就是音频和视频流,但是它还可以表示文本、副标题或隐藏字幕等媒体类型。

2. 资源创建

为一个媒体资源创建 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];

3. 异步载入

AVAsset 具有多种方法和属性,可以提供有关资源的信息,比如时长、创建日期和元数据等。AVAsset 使用了一种高效的设计方法,延迟载入资源属性,直到请求时才载入,这样就可以快速创建资源。不过属性的访问是同步发生的,如果正在请求的属性没有预先载入,程序就会阻塞,所以开发者应该使用异步来查询资源的属性。

NSURL *url = [[NSBundle mainBundle] URLForResource:@"人渣的本愿真人版01" 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];

4. 媒体元数据

4.1 元数据格式

我们在 Apple 环境下遇到的媒体类型主要有四种:

  • QuickTime( .mov )
  • MPEG-4 video( .mp4 和 .m4v )
  • MPEG-4 audio( .m4a )
  • MPEG-Layer III audio( .mp3 )

QuickTime( .mov )

QuickTime 是由苹果公司开发的一种功能强大、跨平台的媒体架构。了解 QuickTime 格式的一个好办法就是在十六进制编译器中打开一个 .mov 格式的文件,常见的十六进制编译器有 Hex Fiend 或 Synalyze It! Pro。更好的方法是借助 Apple Developer Center 中找到 Atom Inspector 工具,它可以将 atom 结构以 NSOutlineView 方式呈现,所以可以对 atom 之间的继承关系等信息有比较清晰的了解。

QuickTime

上图为 QuickTime 结构的简化示意图,该文件最小限度的包含了三个高级 atom,分别是用于描述文件类型和兼容类型的 ftyp,包含实际音视频媒体的 mdat 以及非常重要的 moov atom,它对媒体资源的所有相关细节做了完整描述,包括可呈现的元数据。

MPEG-4 video( .mp4 和 .m4v )和 MPEG-4 audio( .m4a )

mp4 直接派生于 QuickTime 文件格式,这就意味着它们的结构是类似的。实际上,我们经常会发现,能够解析一种文件类型的工具也可以处理其他文件类型,就像 QuickTime 文件一样,mp4 文件也由 atom 数据结构组成。

MPEG-4 video

mp4 是对 MPEG-4 媒体的标准扩展,m4v、m4a、m4p、m4b 这些变体都使用 MPEG-4 容器格式,但是包含了附加的拓展功能。m4v 文件是带有针对 FairPlay 加密和 AC3-audio 扩展的 MPEG-4 视频格式,如果不涉及这些 mp4 和 m4v 仅仅是扩展名不同而已。m4a 针对音频,让使用者知道该文件只带有音频资源。m4p 是苹果较旧的 iTunes 音频格式,使用其 FairPlay 扩展。m4b 用于有声读物,通常包含章节标签和书签功能。

MPEG-Layer III audio( .mp3 )

mp3 文件和上面两种有显著区别,mp3 文件不使用容器格式,而使用编码音频数据,包含的可选元数据的结构块通常位于文件开头。mp3 文件使用一种称为 ID3v2 格式来保存关于音频内容的描述信息,包含的数据有歌曲演唱者、所属唱片和音乐风格等。

MPEG-Layer III audio

AV Foundation 支持读取 ID3v2 标签的所有版本,但是不支持写入,所以 AV Foundation 无法支持对 mp3 进行编码。

4.2 获取元数据

AVAsset 和 AVAssetTrack 都可以实现查询相关元数据的功能。一般使用 AVAsset 提供的元数据,当涉及获取曲目一级元数据等情况时会使用 AVAssestTrack。读取具体资源元数据的接口由 AVMetadataItem 的类提供。

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 : 夏洛特烦恼 电影原声带
    }
}];

上面我们使用的是键和键空间来获取元数据,iOS 8 以后还引进了标识符获取元数据的方法:

// 歌曲名称
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

总结

找不到相关资料,根据我的理解,availableMetadataFormats、metadata、commonMetadata 三者的关系如下

  • availableMetadataFormats:asset.availableMetadataFormats 获得所有 keys,遍历 keys 根据 [asset metadataForFormat:key] 方法获取所有元数据 AVMetadataItem

  • metadata:通过 asset.metadata 方法直接可以得到所有元数据,据测试和 availableMetadataFormats 获得到的元数据相同

  • 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 数据。

5. 封装框架

根据 Learning-AV-Foundation Chapter 3 自行封装了一个元数据获取、编辑的框架,下载地址:https://github.com/Mayan29/MYAVFoundation

推荐阅读更多精彩内容