Bootstrap

(三)AVFoundation 之 资源和元数据学习笔记

参看文章 写的很用心

AVFoundation 之资源和元数据

一、资源介绍

AVFoundation 所有的代码设计都围绕资源 (AVAsset) 进行, AVAssetAVFoundation 设计的核心.

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、 获取元数据

AVAssetAVAssetTrack 都可以实现查询相关元数据的功能.
一般使用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 数据。

;