美文网首页
AVFoundation开发秘籍笔记:第3章 资源和元数据

AVFoundation开发秘籍笔记:第3章 资源和元数据

作者: AlanGe | 来源:发表于2020-08-08 11:20 被阅读0次

3.1 理解资源的含义

AVAsset是一个抽象 类和不可变类,定义了媒体资源混合呈现的方式,将媒体资源的静态属性模块化成一个整体,比如它们的标题、时长和元数据等。

AVAsset不需要考虑媒体资源所具有的两个重要范畴。
第一个:它提供了对基本媒体格式的层抽象,这意味着无论是处理QuickTime影片、MPEG-4视频还是MP3音频,对你和对框架其余部分而言,面对的只有资源这个概念。
第二个:AVAsset隐藏了 资源的位置信息。

AVAsset本身并不是媒体资源,但是它可以作为时基媒体的容器。它由一个或多个带有描述自身元数据的媒体组成。如图3-1所示。


资源的曲目可通过其tracks属性进行访问。对该属性的请求会返回一个NSArray,该数组中的元素就是专辑包含的所有曲目。此外,AVAsset还可 以通过标识符、媒体类型或媒体特征等信息找到相应的曲目。

3.2 创建资源

当你为一个现有媒体资源创建AVAsset对象时,可以通过URL对它进行初始化来实现。一般来说是一个本地文件URL,但也可能是远程资源的URL。

NSURL *assetURL = // url
AVAsset *asset = [AVAsset assetWithURL: assetURL];

AVAsset是一个抽象类,意味着它不能直接被实例化。当使用它的assetWithURL:方法创建实例时,实际上是创建了它子类的一个实例,子类名为AVURLAsset。有时我们会直接使用这个类,因为它允许通过传递选项字典来精细调整资源的创建方式。

NSURL *assetURL = // url
NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey:@YES};
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:assetURL options:options];

3.2.1 iOs Assets库

用户使用系统自带的Camera程序或第三方视频捕捉程序捕捉的视频,它们通常保存在用户的照片库中。iOS提供的Assets库框架可以实现从照片库中读写的功能。下例从用户资源库中的视频创建一个AVAsset:

ALAssetsLibrary *library = [ [ALAssetsLibrary alloc] init];
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
    // Filter down to only videos
    [group setAssetsFilter:[ALAssetsFilter allVideos]];
    // Grab the first video returned
    [group enumerateAssetsAtIndexes:[NSIndexSet indexSetWithIndex:0] options:0 usingBlock:^(ALAsset *alAsset, NSUInteger index, B00L *innerStop) {
        if (alAsset) {
            id representation = [alAsset defaultRepresentation];
            NSURL *url = [representation url];
            AVAsset *asset = [AVAsset assetWithURL:url];
            // Asset created. Perform some AV Foundation magic.
        }
    }] ;
} failureBlock:^(NSError *error) {
    NSLog (@"Error: &@",[error localizedDescription]);
}];

3.2.2 iOs iPod库

我们获取媒体的一个常见位置是用户的iPod库。MediaPlayer框架提供 了API,实现在这个库中查询和获取条目。当需要的条目找到后,可以获取其URL并使用这个URL初始化一个资源,如下例所示:

MPMediaPropertyPredicate *artistPredicate = [MPMediaPropertyPredicate predicateWithValue:@"Foo Fighters" forProperty:MPMediaItemPropertyArtist];
MPMediaPropertyPredicate *albumPredicate = [MPMediaPropertyPredicate predicateWithValue:@"In Your Honor" forProperty:MPMediaItemPropertyAlbumTitle];
MPMediaPropertyPredicate * songPredicate = [MPMediaPropertyPredicate predicateWithValue:@"Best of You" forProperty:MPMediaItemPropertyTitle];

MPMediaQuery *query = [ [MPMediaQuery alloc] init];

[query addFilterPredicate:artistPredicate];
[query addFilterPredicate:albumPredicate];
[query addFilterPredicate:songPredicate];

NSArray *results = [query items];

if (results.count> 0) {
    MPMediaItem *item = results[0];
    NSURL *assetURL = [item valueForProperty:MPMediaItemPropertyAssetURL];
    AVAsset *asset = [AVAssetassetWi thURL:assetURL] ;

    // Asset created. Perform some AV Foundation magic.
}

MediaPlayer框架提供了一个名为MPMediaPropertyPredicate的类,它用于构建帮助用户在iPod库中查找具体内容所用的查询语句。上述示例代码在Foo Fighter的In YourHonor唱片中查找Best ofYou这首歌。在一条查询语句成功执行后,会捕捉到这个媒体条目的资源URL属性,并使用这个属性创建一个AVAsset。

3.2.3 Mac iTunes库

在OS X上,iTunes是用户的中心媒体资源库。要识别这个库中的资源,开发者通常要对iTunes音乐目录中的iTunes Music Library.xml文件进行解析,从而得到相关数据。虽然这确实是一个可行方案,不过从Mac OS X 10.8和iTunes 11.0开始,我们有了更简单的方法,这要归功于iTunesLibrary框架。让我们看一下如何对库中资源进行查询。

ITLibrary *library = [ITLibrary libraryWithAPIVersion:@"1.0" error:nil];
NSArray *items = self.library.allMediaItems;
NSString *query = @"artist.name == 'Robert Johnson' AND"
                   "album.title == 'King of the Delta Blues Singers' AND"
                   "title = 'Cross Road Blues'";
NSPredicate *predicate = [NSPredicate predicateWithFormat:query];
NSArray *songs = [items filteredArrayUsingPredicate:predicate] ;
if (songs.count> 0) {
    ITLibMediaItem *item = songs[0];
    AVAsset *asset = [AVAssetassetWi thURL:item.location];
    // Asset created. Perform some AV Foundation magic.
}

iTunesLibrary框架并没有像MediaPlayer框架那样给出具体的查询API。不过开发者可使用标准的Cocoa NSPredicate类来构建一个复杂查询,从而查找所需的条目。当筛选出所需的媒体条目集合后,可使用ITLibMedialtem获取location属性,这样就能得到一个适于创建AVAsset对象的URL。

3.3 异步载入

AVAsset具有多种有用的方法和属性,可以提供有关资源的信息,比如时长、创建日期和元数据等。AVAsset还包含一些用于获取和使用曲目集合的方法。不过有一点很重要,就是当创建时,资源就是对基础媒体文件的处理。AVAsset使用一种高效的设计方法, 即延迟载入资源的属性,直到请求时才载入。这样就可以快速地创建资源,而不用考虑因为立即载入相关 媒体或元数据所带来的问题,不过有一点很重要,就是属性的访问总是同步发生的。如果正在请求的属性没有预先载入,程序就会阻塞,直到其可以做出适当的响应,显然这种情况一定会带来问题。比如,请求资源的duration可能是一个潜在的昂贵操作。如果开发者在使用MP3文件时没有在头文件中设置TLEN标签,这个标签用于定义duration值,则整个音频曲目都需要进行解析来准确确定它的duration值。假设这个请求发生在主线程,那么等待响应就会阻塞主线程,直到相关操作完成为止。在最好的情况下,可能会感觉应用程序变得迟钝,用户界面没有响应。在iOS上,最糟情况下,可能会出现卡顿,导致系统监视器介入,并终止应用程序的运行。如果出现这种情况,没理由不让用户给子程序"1星"评价。要解决这一问题,开发者应该使用异步的方式来查询资源的属性。

AVAsset和AVAssetTrack都采用了AVAsynchronousKeyValueLoading协议。该协议通过下面给出的这些方法实现了异步查询属性的功能:

- (AVKeyValueStatus)statusOfValueForKey:(NSString *)key error:(NSError *)outError;
- (void)loadValuesAsynchronouslyForKeys:(NSArray *)keys completionHandler:(void (^)(void))handler;

可使用statusOfValueForKey:error:方法查询一个给定属性的状态,该方法会返回一个枚举类型的AVKeyValueStatus值,用于表示当前所请求的属性的状态。如果状态不是AVKeyValueStatusLoaded,意味着此时请求该属性可能导致程序卡顿。要异步载入一个给定的属性,可以调用loadValuesAsynchronouslyForKeys: completionHandler:方法,为其提供-个具有一个或多个key的数组(资源的属性名)和一个completionHandler块, 当资源处于回应请求的状态时,就会调用这个completionHandler块,如下例所示:

// Create URL for 'sunset.mov' in the application bundle
NSURL *assetURL = [[NSBundle mainBundle] URLForResource:@"sunset" withExtension:@"mov"];
AVAsset *asset = [AVAsset assetwithURL:assetURL];
// Asynchronously load the assets 'tracks' property
NSArray *keys = @[@"tracks"];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
    // Capture the status of the 'tracks' property
    NSError *error = nil;
    AVKeyValueStatus status = [asset statusOfValueForKey:@"tracks" error:&error];
    // Switch over the status to determine its state
    switch (status){
    case AVKeyValueStatusLoaded:
        // Continue Processing
        break;
    case AVKeyValueStatusFailed:
        // Handle failure with error
        break;
    case AVKeyValueStatusCancelled:
        // Handle explicit cancellation
        break;
    default:
        // Handle all other cases
    }
}];

该例为存储在应用程序bundle中的QuickTime电影创建了一个AVAsset,并异步载入了该对象的tracks属性。在completionHandler块中,我们希望通过调用资源的statusOfValueForKey:error:方法来确定请求属性的状态。将NSError指针传递给这个方法很重要,因为如果返回的状态为AVKeyValueStatusFailed,则说明资源包含错误信息。通常会切换状态值,并根据返回的状态采取适当操作。要注意的是这个completionHandler块可能会在任意一个队列中 被调用。在对用户界面做出相应更新之前,必须首先回到主队列中。

注意:
该例载入一个tracks 属性,其实可以在一个调用中请求多个属性。当请求多个属性时,需要注意以下两点:
(1)每次调用loadValuesAsynchronouslyForKeys:completionHandler:方 法时只会调用一次completionHandler块。调用该方法的次数并不是根据传递给这个方法的键的个数而定的。
(2)需要为每个请求的属性调用statusOfValueForKeyerror:方法,不能假设所有的属性都返回相同的状态值。

3.4 媒体元数据

当创建一个媒体应用程序时,了解该媒体的组织格式非常重要。简单地向用户呈现一串文件名也许在文件不多的时候还能接受,但是这个方法显然无法满足大规模文件的呈现。所以我们真正需要的是,找到一种方法对媒体进行描述,让用户可以简单地找到、识别和组织这些媒体。幸运的是,我们所使用的AV Foundation中的主要媒体格式都可以嵌入描述其内容的元数据。对元数据的使用具有一定的挑战性,因为每个媒体类型都具有唯一的格式,并且通常要求开发者对相应格式读写操作的底层技术有所了解。不过AV Foundation让这一切变得简单,因为它使开发者不需要考虑大多数特定格式的细节;在处理媒体元数据方面,AVFoundation提供了一套统一的方法。本章剩余的内容将深入研究AV Foundation元数据的相关细节问题,学习如何在应用程序中实现这些功能。在我们开始详解前,先来看一下元数据是如何以多种不同格式保存的。

元数据格式

虽然存在多种格式的媒体资源,但是我们在Apple环境下遇到的媒体类型主要有4种,分别是: QuickTime(mov)、 MPEG-4 video(mp4和Im4v)、MPEG 4 audio(m4a)和MPEG-Layer IIaudio(mp3)。虽然AV Foundation在处理这些文件中嵌入的元数据时都使用一个接口,但是理 解这些不同类型资源的元数据如何存储及存储位置仍然很有价值。这里只做概述,但它是未来深入研究的基础。

1. QuickTime

QuickTime是由苹果公司开发的一种功能强大、跨平台的媒体架构。该架构的一部分 是QuickTime File Format规范, 定义了.mov文件的内部结构。QuickTime文件由一种称 为atoms的数据结构组成。一般规则是这样的: 一个atom包含了描述媒体资源某一方面的数据,或者它可以包含其他atom,但不可以两者都包含。有些情况下,苹果公司自己的方法实现可能会违背这一规则。atom以- -种复杂的树状结构组合在一起,详细地对布局、音频样本格式、视频帧信息乃至需要呈现的元数据信息(如作者信息和版权信息)做了描述。

了解QuickTime格式的一个好办法就是在十六进制编辑器中打开一个.mov格式的文件,常见的十六进制编辑器有Hex Fiend或Synalyze It! Pro。 典型的十六进制工具会将一个真实QuickTime文件的数据呈现到开发者眼前,不过想象其中的结构和atom间的关系也不是那么容易。更好的方法是借助于Apple Developer Center中可以找到的Atom Inspector工具。这个工具将atom结构以NSOutlineView方式呈现,所以就可以对atom之间的继承关系等信息有比较清晰的了解,这个工具还提供了一个小型的十六进制查看器,可以从中查看到实际字节布局。

《超人总动员》是作者非常喜欢的一部由Pixar公 司出品的电影,图3-2给出了这部作品;QuickTime版本结构的简化示意图。该QuickTime文件最小限度地包含了三个高级atom,分别是用于描述文件类型和兼容类型的ftyp,包含实际音频和视频媒体的mdat以及非常重要的moov atom(读作moo-vee),它对媒体资源的所有相关细节做了完整描述,包括可呈现的元数据。


当处理QuickTime电影时会遇到两种类型的元数据。标准QuickTime 元数据由诸如Final Cut Pro X这样的工具编写,位于/moov/meta/ilst/中,它的键几乎都具有com.apple.quicktime前缀。其他类型的数据被认为是QuickTime用户数据,保存在/moov/udta/中。QuickTime用户数据可以包括播放器需要查找的标准数据,比如歌曲演唱者或版权信息,不过同时还可以包含任何对应用程序有帮助的信息。上述两种元数据类型在AVFoundation中都是可以读写的。

对不同atom间的关系和相关细节的理解不在我们的讨论范围内。实际上,苹果公司出具了一份400多页的名为QuickTime File Format Specification 的文档,该文档全面介绍了QuickTime文件格式的所有相关内容。虽然成为QuickTime领域的专家并不重要,但是我们还是建议大家对核心moov atom有所了解,因为这有助于你更好地掌握AV Foundation是如何使用这些数据的。

2. MPEG-4音频和视频

MPEG-4 Part 14是定义MP4文件格式的规范。MP4直接派生于QuickTime文件格式,这就意味着它与QuickTime文件的结构是类似的。实际上,我们经常会发现,能够解析一种文件类型的工具也可以处理其他文件类型(有着不同程度的成功率)。就像QuickTime文件-样,MP4文件也由称为atom的数据结构组成。技术上讲,MPEG-4规范将这些称为boxes,不过考虑到该格式的QuickTime血统,大部分开发者还是把其称为atom。图3-3再次给出了《超人总动员》的例子,不过这次是MP4格式。


MPEG- 4文件的元数据保存在/moov/udta/meta/ilst/中。虽然对于在atom中使用的键没有标准,但大部分工具都遵循苹果公司尚未发布的iTunes元数据规范中对键的定义。虽然没有正式发布,但是iTunes元数据格式的相关文档在互联网上已经广为人知了。我们可以在开源的mp4v2库7中找到一个对这种格式进行讲解的优秀文档。

在实践中,大家对于该文件的扩展名还存在一些困惑。.mp4是对MPEG-4媒体的标准扩展,但存在一些变化, 如.m4v、 .m4a、 .m4p和.m4b。这些变体都使用MPEG-4容器格式,但有些包含了附加的扩展功能。M4V文件是带有苹果公司针对FairPlay加密及AC3-audio扩展的MPEG-4视频格式。如果不涉及FairPlay加密和AC3-audio扩展,则MP4和M4V就仅仅是扩展名不同而已。M4A专门针对音频,使用变化的扩展名的目的是让使用者知道该文件只带有音频资源。M4P是苹果较旧的iTunes音频格式,使用其FairPlay扩展。M4B用于有声读物,并通常包含章节标签以及提供书签功能,让读者可以返回到指定位置开始阅读。

3. MP3

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

使用十六进制工具研究MP3格式的架构是一个好办法。不要担心,与上面小节中介绍的atom相比,ID3数据很容易理解。MP3文件的前10个字节带有嵌入的元数据,这10个字节定义了ID3块的头部。该头部的前三个字节始终为'49 44 33' (ID3),用于表示这是一个ID3v2标签,后面两个字节用于定义主版本信息,即2、3、4和版本号。剩余字节用于定义标志集合及ID3块的大小,如图3-4所示。


ID3块中剩下的数据都是用于描述不同元数据键值的帧。每一帧都有一个带有实际标签名称的10字节的头,之后的4字节表示尺寸,再之后的两个字节用来定义选项标志。帧剩下的字节包含了实际的元数据值。如果值是文本类型,这是最常见的情况,tag中的第一个字节用来定义类型编码。类型编码通常被设置为0x00,代表ISO-8859-1, 不过这里也支持其他类型的编码。图3-5给 出了John Coltrane经 典曲目Giant Steps的MP3版本中的ID3结构示意图。

AV Foundation支持读取ID3v2标签的所有版本,但不支持写入。MP3格式受到专利限制,所以AV Foundation无法支持对MP3或ID3数据进行编码。


注意:
AV Foundation支持所有ID3v2标签格式的读取操作,但是ID3v2是要加星号的。ID3v2.2的布局和ID3v2.3及之后版本的布局不同。值得注意的是,有些标签是由3个字符组成而非4个,比如一首歌曲的标注信息,当标签为ID3v2.2时,是被保存在COM帧中,但当同一首歌使用ID3v2.3标签或更新版本的标签时,歌曲的标注信息会被保存在COMM帧中。框架定义的字符常量只适用于ID3v2.3及之后版本。不过我们会在之后的示例程序中告诉你如何仍使用ID3v2.2数据。

3.5 使用元数据

AVAsset和AVAssetTrack都可以实现查询相关元数据的功能。大部分情况下我们会使用AVAsset提供的元数据,不过当涉及获取曲目一级元数据等情况时也会使用AVAssetTrack.读取具体资源元数据的接口由名为AVMetadataItem的类提供。这个类提供了一个面向对象的接口,让开发者可以对存储于QuickTime、MPEG-4 atom和ID3帧中的元数据进行访问。

AVAsset和AVAssetTrack提供了两种方法可以获取相关的元数据。要了解这两个不同的方法的适用范围,首先要知道键空间(key spaces)的含义。AV Foundation使用键空间作为将相关键组合在一起的方法, 可以实现对AVMetadataItem实例集合的筛选。每个资源至少包含两个键空间,供从中获取元数据,如图3-6所示。


Common键空间用来定义所有支持的媒体类型的键,包括诸如曲名、歌手和插图信息等常见元素。这提供了一种对所有支持的媒体格式进行一定级别的元数据标准化的过程。开发者可以通过查询资源或曲目的commonMetadata属性从Common键空间获取元数据,这个属性会返回一个包含所有可用元数据的数组。

访问指定格式的元数据需要在资源或曲目上调用metadataForFormat:方法。 这个方法包含一个用于定义元数据格式的NSString对象并返回一个包含所有相关元数据信息的NSArray。AVMetadataFormath为不同的元数据格式提供对应的字符串常量。与硬编码某个具体的元数据格式字符串不同,也可以查询资源或曲目的availableMetadataFormats,其会返回一个字符串数组,其中定义了资源中包含的所有元数据格式。开发者可以利用这个结果数组循环访问所有的格式,并为每种格式调用metadataForFormat:方法。让我们看一个例子:

NSURL *url = // url
AVAsset *asset = [AVAsset assetWithURL:url]; :
NSArray *keys = @ [@"availableMetadataFormats"];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^ {
    NSMutableArray *metadata = [NSMutableArray array];
    // Collect all metadata for the available formats
    for (NSString *format in asset.availableMetadataFormats) {
        [metadata addobjectsFromArray:[asset metadataForFormat:format]];
    }
    // Process AVMetadataI tems
}];

3.5.1 查找元数据

当我们得到一个包含元数据项的数组时,通常希望找到所需的具体元数据值。一个特别有效的做法是使用AVMetadataItem提供的便利方法,来获取结果集合并对其进行筛选。例如,如果开发者希望得到一个M4A音频文件的演奏者和唱片的元数据,需要按如下方法获 取这些数据:

NSArray *metadata = // Collection of AVMetadataItems
NSString *keySpace = AVMetadataKeySpaceiTunes;
NSString *artistKey = AVMetadataiTunesMetada taKeyArtist;
NSString *albumKey = AVMetadataiTunesMetadataKeyAlbum;
NSArray *artistMetadata = [AVMetadataItem metadataItemsFromArray:metadata
                                                        withKey:artistKey
                                                       keySpace:keySpace];
NSArray *albumMetadata = [AVMetadataItem metadataItemsFromArray:metadata
                                                       withKey:albumKey
                                                      keySpace:keySpace];
AVMetadataItem *artistItem, *albumItem;
if (artistMetadata.count> 0) {
    artistItem = artistMetadata[0];
}
if (albumMetadata.count> 0) {
    albumItem = albumMetadata[0];
}

例子中使用了AVMetadataltem的metadataltemsFromArray:withKey;keySpace:方法来对集合进行筛选,得出那些匹配键和键空间标准的对象。这个方法的返回值是一个NSArray, 但通常只包含单个AVMetadataItem实例。

3.5.2 使用AVMetadataltem

AVMetadataltem最基本的形式其实是一个封装键值对的封装器。可通过它查询key或commonKey,查询其是否存在于Common键空间中,最重要的是它对应的value。value属性 被定义成id<NSObject,NSCopying>形式,不过它可能是NSString、NSNumber、NSData或其他一些情况,比如NSDictionary。如果开发者已经提前知道value的类型,AVMetadatalItem还会提供三个类型强制属性一stringValue、 numberValue和dataValue--这简化了输入相应返回值的过程。

大部分开发者在第一次接触AVMetadataltem时都会碰到的一个常见问题是,如何理解该对象的key属性。commonKey被定义为一个字符串, 并且很容易估算它在AVMetadataFormat.h中所对应的key,不过key属性是以id<NSObject,NSCopying>值的形式定义的。虽然这个类型可以保存NSString,不过很少这么使用。比如,如果通过下面的代码循环访问嵌入到库中的M4A文件内的元数据,就会得到错误的键值:

NSURL *url = // Song URL
AVAsset *asset = // Asset that has its properties loaded
NSArray *metadata = [assetmeta dataForFormat:AVMetadataFormatiTunesMetadata];
for (AVMetadataItem *item in metadata)
    NSLog(@"&@: 8@", item.key, item.value);
}

执行这段代码将生成如下清单:

-1452383891: Have A Drink On Me
-1455336876: AC/DC
-1451789708: A. Young - M. Young - B. Johnson
-1453233054: Back In Black
-1453039239: 1980

虽然我们通过具体的结果可以看出元数据信息所描述的歌曲是Back in Black唱片中的AC/DC,不过对于key属性返回的整数形式无法知道它的含义。我们看到的是不同的key字符串所对应的整数值。在对这些值进行解释前,需要将它们转换为相应的字符串。由于这是一类常见问题,因此可在AVMetadataItem上添加一个名为keyString的分类方法,这样就可以很容易地获取NSString所对应的内容了。这个分类方法的实现如代码清单3-1所示。

代码清单3-1 AVMetadataltem keyString分类方法

@implementation AVMetadataItem (THAdditions)

- (NSString *)keyString {
    if ([self.key isKindOfClass:[NSString class]]) {                        // 1
        return (NSString *)self.key;
    }
    else if ([self.key isKindOfClass:[NSNumber class]]) {

        UInt32 keyValue = [(NSNumber *) self.key unsignedIntValue];         // 2
        
        // Most, but not all, keys are 4 characters ID3v2.2 keys are
        // only be 3 characters long.  Adjust the length if necessary.
        
        size_t length = sizeof(UInt32);                                     // 3
        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 are stored in big-endian format, swap
        keyValue = CFSwapInt32BigToHost(keyValue);                          // 4

        char cstring[length];                                               // 5
        strncpy(cstring, (char *) address, length);
        cstring[length] = '\0';

        // Replace '©' with '@' to match constants in AVMetadataFormat.h
        if (cstring[0] == '\xA9') {                                         // 6
            cstring[0] = '@';
        }

        return [NSString stringWithCString:(char *) cstring                 // 7
                                  encoding:NSUTF8StringEncoding];

    }
    else {
        return @"<<unknown>>";
    }
}

@end

(1)如果key属性已经是一个字符串,则原样返回,但这并不常见。
(2)请求key的无符号整型值。返回值是一个稍后将提取出的表示4字符代码的32位big endian数字。
(3)大部分情况下,value值 都是一个4字符代码,比如Ogen或TRAK,不过对于使用ID3v2.2标签的MP3文件,键值只有3个字符的长度。代码会移动每个字节来确定length值是否应该截短。
(4)由于数字是big endian格式,因此使用CFSwapInt32BigToHostO函数将其转换为符合主CPU的顺序的little endian格式,Intel和ARM处 理器都是这样要求的。
(5)创建一个字符数组,并使用strmcpy函数将字符字节填充到该数组中。
(6)大量QuickTime用户数据和iTunes key的前缀都带有一个[图片上传失败...(image-91eb0f-1596720315944)] 符号。 不过AVMetadataFormat.h中定义key所使用的前缀符号为@。所以为了进行key常量字符串比较,需要先将[图片上传失败...(image-6e567c-1596720315944)] 替换为@。
(7)最后,使用tringWithCString:encoding初始化器将 字符数组转换为一个NSString。

如果我们导入这个分类并将前一示例修改为使用这个新方法, 就会得到下面的结果,结果已不再那么难懂。

@nam: Have A Drink On Me
@ART: AC/DC
@wrt: A. Young - M. Young - B. Johnson
@alb: Back In Black
@day: 1980

注意:
除了通过键和键空间获取资源的元数据之外,Mac OS X 10.10和iOS 8还引进了使用标识符(identifier)获取元数据的方法。标识符将键和键空间统一成单独的字符串,以一个更简单的模型来获取资源的元数据。本章使用键和键空间的方法,是因为这种方法可以兼容大部分OS版本,不过如果只针对Mac OSX 10.10或iOs 8进行开发,完全可以考虑使用标识符。

现在我们基本理解AVMetadataltem,下面让我们利用学到的知识编写一个名为MetaManager的Mac元数据编辑器应用程序。

3.6 创建 MetaManager应用程序

MetaManager应用程序提供了一个简单界面,让用户可以查看并编辑元数据(如图3-7所示)。该程序可以让用户查看AV Foundation支持的所有媒体类型的元数据信息,还可以对除了MP3之外的所有资源写入元数据信息。该应用程序很好地展示了如何使用这些元数据类,同时也对刚刚接触该框架的开发者经常遇到的一些问题给出了很好的解决方案。用户界面创建完毕后,你只需把注意力放在如何构建基础AV Foundation框架上。可在Chapter3目录中找到名为MetaManager starter的项目代码。


虽然AV Foundation的元数据类可在一定 程度上抽象因基础元数据格式不同而带来的区别,不过要对多种媒体格式的元数据进行统一管理还是很有挑战的。要对基础元数据提供一个统一界面,应该采用的策略是将特定格式的元数据映射到一个标准化的键值集合中。这就为基础元数据提供了一个简单、一致的用户界面,并使得管理这些元数据的逻辑方法可以集中在单个类中。让我们首先实现第一个类THMedialtem。

3.6.1 THMedialtem

THMedialtem定义了应用程序管理媒体最主要的接口。它封装了基础AVAsset实例并负责管理相关元数据的载入和解析。首先看一下这个类的接口,如代码清单3-2所示。

代码清单3-2 THMediaItem 接口

#import <AVFoundation/AVFoundation.h>
#import "THMetadata.h"

typedef void(^THCompletionHandler)(BOOL complete);

@interface THMediaItem : NSObject

@property (strong, readonly) NSString *filename;
@property (strong, readonly) NSString *filetype;
@property (strong, readonly) THMetadata *metadata;
@property (readonly, getter = isEditable) BOOL editable;

- (id)initWithURL:(NSURL *)url;

- (void)prepareWithCompletionHandler:(THCompletionHandler)handler;

- (void)saveWithCompletionHandler:(THCompletionHandler)handler;

@end

THMedialtem实例是通过调用其initWithURL:方法创建的,传递一个磁盘中给定媒体文件的本地文件URL。应用程序在程序bundle中包含需要的示例文件,当程序启动时将这些文件复制到~/Library/Application Support/MetaManager目录中。程序会为每个文件创建一个THMediaItem实例并填充媒体列表。

注意:

如果希望将媒体资源重置回默认状态,可以选择Edit菜单并选择Reset Media Item,即可将媒体重新设置成初始状态。

让我们转过来看一下具体的实现文件并开始实现这些方法,如代码清单3-3所示。

代码清单3-3 THMedialtem 实现

#import "THMediaItem.h"

#import "AVMetadataItem+THAdditions.h"
#import "NSFileManager+DirectoryLocations.h"

#define COMMON_META_KEY     @"commonMetadata"
#define AVAILABLE_META_KEY  @"availableMetadataFormats"

@interface THMediaItem ()
@property (strong) NSURL *url;
@property (strong) AVAsset *asset;
@property (strong) THMetadata *metadata;
@property (strong) NSArray *acceptedFormats;
@property BOOL prepared;
@end

@implementation THMediaItem

- (id)initWithURL:(NSURL *)url {
    self = [super init];
    if (self) {
        _url = url;                                                         // 1
        _asset = [AVAsset assetWithURL:url];
        _filename = [url lastPathComponent];
        _filetype = [self fileTypeForURL:url];                              // 2
        _editable = ![_filetype isEqualToString:AVFileTypeMPEGLayer3];      // 3
        _acceptedFormats = @[                                               // 4
            AVMetadataFormatQuickTimeMetadata,
            AVMetadataFormatiTunesMetadata,
            AVMetadataFormatID3Metadata
        ];
    }
    return self;
}

- (NSString *)fileTypeForURL:(NSURL *)url {
    NSString *ext = [[self.url lastPathComponent] pathExtension];
    NSString *type = nil;
    if ([ext isEqualToString:@"m4a"]) {
        type = AVFileTypeAppleM4A;
    } else if ([ext isEqualToString:@"m4v"]) {
        type = AVFileTypeAppleM4V;
    } else if ([ext isEqualToString:@"mov"]) {
        type = AVFileTypeQuickTimeMovie;
    } else if ([ext isEqualToString:@"mp4"]) {
        type = AVFileTypeMPEG4;
    } else {
        type = AVFileTypeMPEGLayer3;
    }
    return type;
}

(1)在initWithURL:方法中设置类的内部状态,并基于传入的URL创建一个AVAsset实例。应用程序的表视图中显示的每个条目都是该类的一个实例。
(2)为基础资源确定文件类型。虽然AV Foundation提供一些高级方法用于确定文件类型,但基于文件扩展名来确定类型的方法已经可以满足需求了。当实现程序的保存功能时,我们会用到filetype属性。
(3)程序会基于媒体文件的类型来设置editable标志。虽然AV Foundation允许我们读取ID3元数据,但我们无法向其中写入数据,所以需要设置editable标志,使得程序可以正确设置Save按钮的enabled状态。
(4)还需要设置-一个包含支持的所有元数据格式的数组。这么做是为了排除那些可能出现在AVMetadataKeySpaceQuickTimeUserData和AVMetadataKeySpaceISOUserData键空间中的外部重复值。

让我们继续研究prepareWithCompletionHandler:方法的实现,如代码清单3-4所示。

代码清单3-4 prepareWithCompletionHandler:方法的实现

- (void)prepareWithCompletionHandler:(THCompletionHandler)completionHandler {
    if (self.prepared) {                                                    // 1
        completionHandler(self.prepared);
        return;
    }
    self.metadata = [[THMetadata alloc] init];                              // 2
    NSArray *keys = @[COMMON_META_KEY, AVAILABLE_META_KEY];
    [self.asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
        AVKeyValueStatus commonStatus =
            [self.asset statusOfValueForKey:COMMON_META_KEY error:nil];
        AVKeyValueStatus formatsStatus =
            [self.asset statusOfValueForKey:AVAILABLE_META_KEY error:nil];
        self.prepared = (commonStatus == AVKeyValueStatusLoaded) &&         // 3
                        (formatsStatus == AVKeyValueStatusLoaded);
        if (self.prepared) {
            for (AVMetadataItem *item in self.asset.commonMetadata) {       // 4
                //NSLog(@"%@: %@", item.keyString, item.value);
                [self.metadata addMetadataItem:item withKey:item.commonKey];
            }
            for (id format in self.asset.availableMetadataFormats) {        // 5
                if ([self.acceptedFormats containsObject:format]) {
                    NSArray *items = [self.asset metadataForFormat:format];
                    for (AVMetadataItem *item in items) {
                        //NSLog(@"%@: %@", item.keyString, item.value);
                        [self.metadata addMetadataItem:item
                                               withKey:item.keyString];
                    }
                }
            }
        }
        completionHandler(self.prepared);                                 // 6
    }];
}

(1)用户每次在应用的表视图中选择一个媒体项时,都会调用prepareWithCompletion-Handler:方法。只需将这个项准备一次, 所以如果已经准备了这个项,则直接调用completion块并跳出。
(2)创建一个新的THMetadata实例。这个我们即将实现的类会对保存在单独AVMetadataltem实例中的值进行管理。
(3)通过查看commonMetadata和availableMetadataFormats属性的状态来确定prepared状态。这两个属性一定要成功载入用于后续的进程。
(4)循环访问所有由通用键空间返回的AVMetadataltem实例,并将每一个添加到THMetadata实例。
(5)请求资源的availableMetadataFormats会返回一个数组,其中包含用于确定媒体元素中可用格式的字符串数组。如果supportedFormats集合中包含 了特定格式,就需要获取相关的AVMetadataItem实例并将其添加到内部元数据存储中。
(6)最后调用completion handler块使用户界面可以将获取的元数据呈现在屏幕上。继续运行该应用程序。仅仅展示元数据的状态还不够,当选择单独的媒体项时,可以看 到控制台将键值信息输出了。当验证完这一行为后,就可以将注释代码清除了,或者将NSLog语句删除掉。

下面将注意力转到THMetadata类,重点研究单个AVMetadataItem实例中返回值相关的细节问题。

3.6.2 THMetadata 的实现

THMetadata类实现了这个应用程序中大部分重要的功能。它负责从相关元数据元素中提取值,并将它们保存供日后使用。在这个类中,我们仍需对映射到用户界面上特殊字段的所有不同键值做标准化操作。让我们先看一下代码清单3-5所示的THMetadataltem接口。

代码清单3-5 THMetadataltem 接口

#import <AVFoundation/AVFoundation.h>

@class THGenre;

@interface THMetadata : 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) NSImage *artwork;
@property (strong) THGenre *genre;

@property NSString *year;
@property NSNumber *bpm;
@property NSNumber *trackNumber;
@property NSNumber *trackCount;
@property NSNumber *discNumber;
@property NSNumber *discCount;

- (void)addMetadataItem:(AVMetadataItem *)item withKey:(id)key;

- (NSArray *)metadataItems;

@end

该接口定义了多种可在屏幕上呈现的数据值。每个属性都对应用户接口中的一个特殊字段。之后程序定义了addMetadataItem:方法,向其内部集合添加一个AVMetadataItem。最后定义了一个metadataltems方法用于获取已经更新的元数据,这样用户就可以对所发生的变更进行保存。让我们转到具体的实现中(如代码清单3-6所示)。

代码清单3-6 THMetadata 方法的实现

#import "THMetadata.h"
#import "THMetadataConverterFactory.h"
#import "THMetadataKeys.h"

@interface THMetadata ()
@property (strong) NSDictionary *keyMapping;
@property (strong) NSMutableDictionary *metadata;
@property (strong) THMetadataConverterFactory *converterFactory;
@end

@implementation THMetadata

- (id)init {
    self = [super init];
    if (self) {
        _keyMapping = [self buildKeyMapping];                               // 1
        _metadata = [NSMutableDictionary dictionary];                       // 2
        _converterFactory = [[THMetadataConverterFactory alloc] init];      // 3
    }
    return self;
}

- (NSDictionary *)buildKeyMapping {

    return @{
        // Name Mapping
        AVMetadataCommonKeyTitle : THMetadataKeyName,

        // Artist Mapping
        AVMetadataCommonKeyArtist : THMetadataKeyArtist,
        AVMetadataQuickTimeMetadataKeyProducer : THMetadataKeyArtist,

        // Album Artist Mapping
        AVMetadataID3MetadataKeyBand : THMetadataKeyAlbumArtist,
        AVMetadataiTunesMetadataKeyAlbumArtist : THMetadataKeyAlbumArtist,
        @"TP2" : THMetadataKeyAlbumArtist,

        // Album Mapping
        AVMetadataCommonKeyAlbumName : THMetadataKeyAlbum,

        // Artwork Mapping
        AVMetadataCommonKeyArtwork : THMetadataKeyArtwork,

        // Year Mapping
        AVMetadataCommonKeyCreationDate : THMetadataKeyYear,
        AVMetadataID3MetadataKeyYear : THMetadataKeyYear,
        @"TYE" : THMetadataKeyYear,
        AVMetadataQuickTimeMetadataKeyYear : THMetadataKeyYear,
        AVMetadataID3MetadataKeyRecordingTime : THMetadataKeyYear,

        // BPM Mapping
        AVMetadataiTunesMetadataKeyBeatsPerMin : THMetadataKeyBPM,
        AVMetadataID3MetadataKeyBeatsPerMinute : THMetadataKeyBPM,
        @"TBP" : THMetadataKeyBPM,

        // Grouping Mapping
        AVMetadataiTunesMetadataKeyGrouping : THMetadataKeyGrouping,
        @"@grp" : THMetadataKeyGrouping,
        AVMetadataCommonKeySubject : THMetadataKeyGrouping,

        // Track Number Mapping
        AVMetadataiTunesMetadataKeyTrackNumber : THMetadataKeyTrackNumber,
        AVMetadataID3MetadataKeyTrackNumber : THMetadataKeyTrackNumber,
        @"TRK" : THMetadataKeyTrackNumber,

        // Composer Mapping
        AVMetadataQuickTimeMetadataKeyDirector : THMetadataKeyComposer,
        AVMetadataiTunesMetadataKeyComposer : THMetadataKeyComposer,
        AVMetadataCommonKeyCreator : THMetadataKeyComposer,

        // Disc Number Mapping
        AVMetadataiTunesMetadataKeyDiscNumber : THMetadataKeyDiscNumber,
        AVMetadataID3MetadataKeyPartOfASet : THMetadataKeyDiscNumber,
        @"TPA" : THMetadataKeyDiscNumber,

        // Comments Mapping
        @"ldes" : THMetadataKeyComments,
        AVMetadataCommonKeyDescription : THMetadataKeyComments,
        AVMetadataiTunesMetadataKeyUserComment : THMetadataKeyComments,
        AVMetadataID3MetadataKeyComments : THMetadataKeyComments,
        @"COM" : THMetadataKeyComments,

        // Genre Mapping
        AVMetadataQuickTimeMetadataKeyGenre : THMetadataKeyGenre,
        AVMetadataiTunesMetadataKeyUserGenre : THMetadataKeyGenre,
        AVMetadataCommonKeyType : THMetadataKeyGenre
    };
}

(1)要完成对不同键空间的键和格式的标准化工作,需要创建一个 从指定格式键到标准化键的映射。标准化的键常量在THMetadataKeys.h中定义。代码清单3-6给出了buildKeyMappings方法的缩略版本。要了解所有键值的映射关系,一定要查看源代码中程序的完整版本。
(2)创建一个NSMutableDictionary来保存 从AVMetadataItem值提取到的展示内容。保存在这个字典对象中的值就是最后在屏幕上呈现的值。
(3)创建一个THMetadataConverterFactory实例来声明THMetadataConverter实例。 我们要创建的这个转换对象用于在保存于AVMetadataltem中的数据和呈现在屏幕上的值之间进行转换。

接下来看一下FaddMetadataItem:方法的实现(如代码清单3-7所示)。

代码清单3-7 addMetadataltem:的实现

- (void)addMetadataItem:(AVMetadataItem *)item withKey:(id)key {

    NSString *normalizedKey = self.keyMapping[key];                         // 1

    if (normalizedKey) {

        id <THMetadataConverter> converter =                                // 2
            [self.converterFactory converterForKey:normalizedKey];

        // Extract the value from the AVMetadataItem instance and
        // convert it into a format suitable for presentation.
        id value = [converter displayValueFromMetadataItem:item];

        // Track and Disc numbers/counts are stored as NSDictionary
        if ([value isKindOfClass:[NSDictionary class]]) {                   // 3
            NSDictionary *data = (NSDictionary *) value;
            for (NSString *currentKey in data) {
                if (![data[currentKey] isKindOfClass:[NSNull class]]) {
                    [self setValue:data[currentKey] forKey:currentKey];
                }
            }
        } else {
            [self setValue:value forKey:normalizedKey];
        }

        // Store the AVMetadataItem away for later use
        self.metadata[normalizedKey] = item;                                // 4
    }
}

(1)在这个方法中,我们首先为传进来的键找到标准化的键。也就是将指定格式的键映射为标准化的键。如果返回得到标准化的值,则继续处理AVMetadataItem。
(2)之后在converterFactory中请求能够处理当前AVMetadataltem的THMetadataConverter对象。通过转换器获取当前元数据项对应的展示值,即将元数据的值通过提取和转换使其成为能够用于展示的格式。
(3)当得到一个正确的返回值之后,需要判断它的类型。唱片和曲目编号是一种特殊情况,需要其他一些操作才 能提取它们的值。最终使用键值编码方setValue:forKey来设置每个独立的属性值。
(4)最后,将AVMetadataItem保存在metadata字典中以备日后使用。

如果还对这个流程感到不是很理解也不必担心,开始创建转换器后,我们会将这些复杂的步骤分解成一个个小的部分来实现。让我们开始创建这些对象吧。

3.6.3数据转换器

在处理AVMetadataItem时,其中最具有挑战性及最难懂的部分就是理解保存在其value属性中的数据。当value是一个简单字符串时,比如艺术家名字或唱片标题,或是数字类型,比如录制年份或心律值(BPM),这样的数据很容易理解并且不需要转换。不过我们经常会遇到一些看起来很混乱的值, 或者仅仅是一些晦涩的内容, 这就意味着需要通过一些额外努力将这些值转换为可以展示的格式。可在THMetadata类中直接加入转换逻辑,但是这样的话代码量会快速增加,导致不易维护。所以正确的做法是将这些逻辑语句分离到几个转换类中,当然这些转换类都要遵循THMetadataConverter协议,如代码清单3-8所示。

代码清单3-8 THMetadataConverter 协议

#import <AVFoundation/AVFoundation.h>

@protocol THMetadataConverter <NSObject>

- (id)displayValueFromMetadataItem:(AVMetadataItem *)item;

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item;
@end

THMetadataConverter协议定义了两个方法。其中displayValueFromMetadataItem:方法用于解析AVMetadataItem实例中的数据并将其转换为可展示的格式。它的同级方法metadataltemFrom-DisplayValue:withMetadataltem:可将展示内容转换为之前的AVMetadataltem格式,使其始终可以保存到基础媒体中。转换对象的类接口全部遵循代码清单3-9所示的模式,所以接下来的代码清单只给出类的实现部分。

代码清单3-9 THMetadataConverter 接口模板

#import "THMetadataConverter .h"

@interface ClassName : NsObject <THMetadataConverter>

@end

3.6.4 简单转换

上述协议的默认实现方法很简单,仅提供了一个到AVMetadataItem值的通道。这个类主要用于转换简单的字符串和数字值。代码清单3-10给出了这个转换器的具体实现。

代码清单3-10 THDefaultMetadataConverter 的实现

#import "THDefaultMetadataConverter.h"

@implementation THDefaultMetadataConverter

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

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

@end

将AVMetadataltem值转换为对应的展示内容时,需要返回项的value属性。这对那些value属性为简单字符串或数字值的元数据项是适用的。当由展示内容转换回AVMetadataItem值时,首先需要创建一个原始AVMetadataltem的可变副本,这将返回一个AVMutableMetadataltem值。当创建新的元数据或修改已存在的元数据时,需要使用AVMetadataItem的可变子类。它具有与父类一样的基本接口,但它重新将其中的一些只读属性定义为读写属性。通过复制原始AVMetadataItem所创建的AVMutableMetadataItem是一个新实例,实例中所有重要的属性都和原始项的值一样,只需要将它的value属性 设置为传入方法的值即可。

这只是我们所说的简单转换,在后面马.上会接触到很多复杂的场景。下 面让我们看一下如何转换元数据项的Artwork。

3.6.5 转换Artwork

媒体Artwork元数据会以多种不同的格式返回,比如唱片的封面和电影的海报等。不过最终Artwork数据的字节都保存在一个NSData实例中,但是定位NSData实例有时候需要完成一些额外工作。代码清单3-11给出了THArtworkMetadataConverter的实现。

代码清单3-11THArtworkMetadataConverter的实现

#import "THArtworkMetadataConverter.h"

@implementation THArtworkMetadataConverter

- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    NSImage *image = nil;
    if ([item.value isKindOfClass:[NSData class]]) {                        // 1
        image = [[NSImage alloc] initWithData:item.dataValue];
    }
    else if ([item.value isKindOfClass:[NSDictionary class]]) {             // 2
        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;                          // 3
    
    return metadataItem;
}

@end

(1)如果AVMetadataItem返回一个NSData类型的值,我们就需要根据保存在该对象中的字节创建一个新的NSImage. NSImage 和Ullmage都可以提供初始化器,使我们可从NSData直接创建一个图片实例。
(2)如果选中的对象是MP3,就需要对它进行深入地挖掘以得到图片字节。这种情况下的value属性可能是一个NSDictionary,所以我们适当地转换值并检索对应的data键,这样就可以得到保存图片字节的NSData。
(3)将展示内容转换回AVMutableMetadataltem实例的实现过程相当简单,这是由于AVFoundation无法写入ID3元数据。可认为这个值就是以NSData格式保存的并向NSImage请求TIFFRepresentation。如果需要存储PNG或JPG格式的图片数据,可以使用NSBitmapImageRep、Ullmage方法或Quart框架将该数据转换为所需的类型。

3.6.6 转换注释

获取媒体项注释的方法相当直接。如果处理的对象是MPEG-4或QuickTime内容,可以获取AVMetadataltem的stringValue属性。MP3同样需要一些额外 方法来获取注释数据,如代码清单3-12所示。

代码清单3-12 THCommentMetadataConverter 的实现

#import "THCommentMetadataConverter.h"

@implementation THCommentMetadataConverter

- (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)如果条目的value属性为NSString,可以获取它的stringValue属性。这种情况适用于MPEG-4和QuickTime媒体。
(2) MP3的注释保存在一个定 义ID3 COMM帧的NSDictionary中(如果处理的是ID3v2.2,则为COM)。所有类型的值都保存在这个帧中。比如,iTunes在这个帧中保存音频标准化和无缝播放设置等,意味着当请求ID3元数据时需要接收多个COMM帧。包含实际注释内容的特定COMM帧被存储在一个带有空字 符串标识符的帧中。找到需要的条目后,通过请求text键来检索注释。
(3)将展示内容转换回AVMetadataItem很简单,因为这个过程不涉及ID3数据,所以只需复制原始AVMetadataltem并设置value属性为传入方法的值即可。

3.6.7 转换音轨数据

音轨通常包含一首歌曲在整个唱片中的编号位置信息(比如:"12首歌曲中的第4首"这样的信息)。MP3文件以一种简单易懂的方式保存此信息,但是M4A就有些复杂了,并且需要一些处理才能生成用于展示的内容。代码清单3-13给出了THTrackMetadataConverter类的实现。

代码清单3-13 THTrackMetadataConverter的实现

#import "THTrackMetadataConverter.h"
#import "THMetadataKeys.h"

@implementation THTrackMetadataConverter

- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
    
    NSNumber *number = nil;
    NSNumber *count = nil;
    
    if ([item.value isKindOfClass:[NSString class]]) {                      // 1
        NSArray *components =
            [item.stringValue componentsSeparatedByString:@"/"];
        number = @([components[0] integerValue]);
        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: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;
}

@end

(1) MP3音轨信息以“xx/xx”字符串的格式返回。比如在一个包含10首歌曲的唱片中的第8首歌曲,得到的返回字符串值应该为“8/10”。 这种情况下,我们需要使用NSString的componentsSeparatedByString:方法将值分开,得到单独值并将它们转换为相应的数值对象,将值打包为一个NSNumber实例。
(2)对于M4A文件,这个值有些神秘。如果在控制台打印输出元数据条目的值,所得到的结果是NSData,使用诸如0000008000a0000>的值。这是4个16位big endian数字数组的十六进制表现形式。数组中的第2个和第3个元素分别包含音轨编号和音轨计数值。
(3)如果音轨编号不等于0,使用CFSwapInt16BigToHost0函数进行endian变换将其转换为lttle endian格式,并将这个值打包为一个NSNumber实例。
(4)同样,如果音轨计数值不等于0,则获取该值并执行endian转换,最后将结果值打包为一个NSNumber实例。
(5)将得到的编号和计数值保存在一个NSDictionary实例中,将它返回给调用者。需要检查每个值是否为nil,如果有的值为nil,则使用NSNull实例替换。
(6)将展示内容转换回AVMetadataItem所需的格式意味着需要将之前的步骤反过来,即把在displayValueFromMetadataltem:方法中提取这些值的过程反过来。需要创建一个包含4个uint16_t值的数组来保存音轨编号和计数值。
(7)如果得到一个有效的音轨编号,将字节转换为big endian格式并将其保存在数组的第二个位置。
(8)如果得到一个有效的音轨总数, 将字节转换为big endian格式并将其保存在数组的第三个位置。
(9)最后,将这些values数组打包为一个NSData实例,并将其设置为元数据项的值。现在我们知道了处理音轨计数信息并不是那么简单,不过现在我们已经知道其中的奧妙了!

3.6.8 转换唱片数据

唱片计数信息用于表示一首歌曲所在的CD是所有唱片中的第几张唱片。大部分情况下这个值都是1/1,即只有一张唱片,不过如果这首歌曲所属的唱片属于一个唱片集合的一部分,则这个值可能就会发生变化。比如,如果你拥有Led Zeppelin的Complete StudioRecordings(很值得拥有的唱片),则Led Zeppelin IV这张唱片的唱片值就应该是4/10.好消息是我们已经知道如何获取这个数据,因为它与上一节中的音轨编号和音轨计数数据的保存方法几乎一样,所以它的类实现也十分相似。代码清单3-14给出了THDiscMetadataConverter类的实现。

代码清单3-14 THDiscMetadataConverter 的实现

#import "THDiscMetadataConverter.h"
#import "THMetadataKeys.h"

@implementation THDiscMetadataConverter

- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {

    NSNumber *number = nil;
    NSNumber *count = nil;
    
    if ([item.value isKindOfClass:[NSString class]]) {                      // 1
        NSArray *components =
            [item.stringValue componentsSeparatedByString:@"/"];
        number = @([components[0] integerValue]);
        count = @([components[1] integerValue]);
    }
    else if ([item.value isKindOfClass:[NSData class]]) {                   // 2
        NSData *data = item.dataValue;
        if (data.length == 6) {
            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:THMetadataKeyDiscNumber];
    [dict setObject:count ?: [NSNull null] forKey:THMetadataKeyDiscCount];
    
    return dict;
}

- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item {

    AVMutableMetadataItem *metadataItem = [item mutableCopy];

    NSDictionary *discData = (NSDictionary *)value;
    NSNumber *discNumber = discData[THMetadataKeyDiscNumber];
    NSNumber *discCount = discData[THMetadataKeyDiscCount];

    uint16_t values[3] = {0};                                                 // 6
    
    if (discNumber && ![discNumber isKindOfClass:[NSNull class]]) {
        values[1] = CFSwapInt16HostToBig([discNumber unsignedIntValue]);    // 7
    }
    
    if (discCount && ![discCount isKindOfClass:[NSNull class]]) {
        values[2] = CFSwapInt16HostToBig([discCount unsignedIntValue]);     // 8
    }
    
    size_t length = sizeof(values);
    metadataItem.value = [NSData dataWithBytes:values length:length];       // 9

    return metadataItem;
}

@end

(1) MP3的唱片信息以“xx/xx”格式的字符串返回。如果一首歌曲所属唱片是10个唱片集合中的第4张唱片,那么得到的字符串值就为4/10。这时我们使用NSString的components-SeparatedByString:方法将值分开,并将它们转换为数值类型,打包成一个NSNumber实例。
(2) iTunes M4A文件的唱片信息保存在一个NSData中,NSData包 含3个16位big endian数字。数组中的第2个和第3个元素分别保存唱片编号和唱片计数值。
(3)如果唱片编号不等于0,则获取该值并使用CFSwapInt1 6BigToHost()函数执行endian转换,转换为little endian,并打包成一个NSNumber。
(4)同样,如果唱片计数值不等于0,则获取该值并在字节上执行endian转换,将得到的值打包为一个NSNumber。
(5)将所得到的编号和计数值保存在一个NSDictionary中, 将它返回到调用者。需要检查每个值是否为nil,如果有的值为nil,则使用NSNull实例替换。
(6)将展示内容转换回AVMetadataltem所需的格式意味着需要将之前的步骤反过来,即把在displayValueFromMetadataItem:方法中提取这些值的过程反过来。需要创建一个包含3个uint16_t值的数组来保存唱片编号和唱片计数值。
(7)如果得到一个有效的唱片编号,就将字节转换为big endian格式并将其保存在数组的第二个位置。
(8)如果得到一个有效的唱片计数值,将字节转换为big endian格式并将其保存在数组的第三个位置。
(9)最后,将这些values数组打包为一个NSData实例,并将其设置为元数据项的值。

3.6.9 转换风格数据

处理风格(genre)数据具有一定挑战。 困难并非来自于数据本身的复杂性,而是风格具有多种不同的格式。比如预定义风格、用户风格、风格ID、音乐风格、电影风格等。另一个 挑战在于风格信息的保存不止一种方法,即使对于文件类型也同样如此。我们看一下相关的基础知识。

对数字音频使用的标准风格分类最初来自于MP3。ID3规范定义了80个默认的风格类型及另外46个WinAmp扩展,一共126 个风格。你可能还会找到各种工具所支持的特有风格类型,不过这些都不属于正式格式。

由于在这一点上MP3的主导地位明显,iTunes没有 创建一套自己的音乐风格划分,而是基本遵循了ID3的风格分类,不过做了点小变化。iTunes音 乐风格的标号比相应的ID3标识符大1。比如表3-1给出了ID3集合中的前5种风格以及相应的iTunes值。


虽然iTunes使用了ID3集合中的预定义音乐风格类型,不过iTunes对电视、电影和有声读物等定义了自己的风格集。可在Apple' s Genre IDs Appendix3中找到完整的iTunes风格列表。

要简化风格数据的处理(节省代码的输入),示例程序包含一个名为THGenre的类,它定义了标准的音乐风格和电视节目的风格。可以对应用程序中的音频及视频内容使用这些风格类型。

让我们学习一下有关风格的具体细节。代码清单3-15给出了THGenreMetadata- Converter类的实现。

代码清单3-15 THGenreMetadataConverter 的实现

#import "THGenreMetadataConverter.h"
#import "THGenre.h"

@implementation THGenreMetadataConverter

- (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;
}

@end

(1)有些格式将风格数据保存为一个NSString。有时该字符串的内容就是实际风格的名字,有时是风格在预定义风格列表中对应的索引值。
(2)如果元数据项来自于ID 3键空间并且这个值可以强制转换为NSNumber,在使用ID3v2.4的情况下,可以取到该值的无符号整型值并用它作为索引来查找正确的THGenre实例。
(3) ID3的另一个变化是使用风格实际的名称进行保存,比如Jazz、Rock、Country及类似的情况。如果值为风格的名称,可以使用这个名称找到相应的THGenre实例。
(4)对于其他以NSString类型保存的风格信息,可以将它们视为QuickTime电影或MPEG-4视频文件,这两种情况都直接保存风格的实际名称。可以从THGenre类查找到相应的视频风格。
(5)当使用其中一个预定义风格时,iTunes M4A音频会返回一个保存在NSData中的16位bigendian数字。取到该值并将其转换为little endian格式, 之后调用iTunesGenreWithIndex:方法返回相应的风格实例。
(6)当转换回AVMetadataltem格式时,如果原始AVMetadataItem中保存的值是一个字符串,可以简单地将输入值赋给条目的value属性。
(7)如果原始AVMetadataltem保存的值是一个NSData,通过将字节转换回big endian格式,并将结果包装为一个NSData值,将展示的内容转换回相应的格式。另外注意THGenre的索引属性基于0,因此为调整回iTunes索引,需要加1。

恭喜,有关转换的方法我们全部学习完了。现在可以运行程序,从列表中选择一个条目并观察它在用户界面中展示的内容。我们还剩下一个功能需要实现,就是让用户对这些变更进行保存。让我们回到上面的THMetadata类并完成其实现。

3.6.10 完成 THMetadata

我们还需要实现THMetadata的metadataltems方法。这个方法会获取所有当前保存的展示内容,并使用我们上面几节中创建的转换类将它们转换回AVMetadataltem格式。代码清单3-16给出了这个方法的实现。

代码清单3-16 metadataltems 方法的实现

- (NSArray *)metadataItems {

    NSMutableArray *items = [NSMutableArray array];                         // 1

    // Add track number/count if applicable
    [self addMetadataItemForNumber:self.trackNumber                         // 2
                             count:self.trackCount
                         numberKey:THMetadataKeyTrackNumber
                          countKey:THMetadataKeyTrackCount
                           toArray:items];

    // Add disc number/count if applicable
    [self addMetadataItemForNumber:self.discNumber
                             count:self.discCount
                         numberKey:THMetadataKeyDiscNumber
                          countKey:THMetadataKeyDiscCount
                           toArray:items];

    NSMutableDictionary *metaDict = [self.metadata mutableCopy];            // 5
    [metaDict removeObjectForKey:THMetadataKeyTrackNumber];
    [metaDict removeObjectForKey:THMetadataKeyDiscNumber];

    for (NSString *key in metaDict) {

        id <THMetadataConverter> converter =
            [self.converterFactory converterForKey:key];

        id value = [self valueForKey:key];                                  // 6

        AVMetadataItem *item =                                              // 7
            [converter metadataItemFromDisplayValue:value
                                   withMetadataItem:metaDict[key]];
        if (item) {
            [items addObject:item];
        }
    }

    return items;
}

- (void)addMetadataItemForNumber:(NSNumber *)number
                           count:(NSNumber *)count
                       numberKey:(NSString *)numberKey
                        countKey:(NSString *)countKey
                         toArray:(NSMutableArray *)items {

    id <THMetadataConverter> converter =
        [self.converterFactory converterForKey:numberKey];
    
    NSDictionary *data = @{numberKey : number ?: [NSNull null],             // 3
                           countKey : count ?: [NSNull null]};
    
    AVMetadataItem *sourceItem = self.metadata[numberKey];
    
    AVMetadataItem *item =                                                  // 4
        [converter metadataItemFromDisplayValue:data
                               withMetadataItem:sourceItem];
    if (item) {
        [items addObject:item];
    }
}

(1)创建一个NSMutableArray实例, 保存在本方法创建的元数据集合中。
(2)音轨编号/计数及唱片编码/计数都需要额外的处理来将它们的值转换回AVMetadatatem格式,所以具体处理逻辑应该分解为不同方法,如应用程序中的编号3所示。
(3)将成对的number和count封装到一个NSDictionary,并将原始AVMetadataltem传递到转换方法中。
(4)从转换器获取元数据项,如果返回一个有效的元数据实例,则将它添加到items数组中。
(5)创建一个包含不同显示值的内部metadata字典的副本,将音轨和唱片编号键从数组中删除,因为这些都是已经处理过的。遍历剩下的键值来创建相应的元数据项。
(6)使用键/值编码方法valueForKey:查找当前键对应的值。
(7)最后从当前转换方法中检索AVMetadataltem实例,如果返回一个有效实例,则将它添加到items集合中。

THMetadata类现在就全部完成了,我们也基本上完成了MetaManager应用程序。唯一剩下的任务就是实现THMedialtem上的saveWithCompletionHandler:方法。下面就开始实现它吧。

3.7 保存元数据

我们已经创建了THMetadata类及相关的转换对象,现在就可以读取元数据并将用户输入的内容转换回AVMetadataItem实例。不过还有一个重要的问题没有解决,由于AVAsset是一个不可变类,我们如何才能应用这些元数据的改变呢?答案是我们不直接修改AVAsset,而是使用一个名为AVAssetExportSession的类来导出一个新的资源副本以及元数据改动。在具体学习这个方法的实现之前,先看一下如何配置和使用AVAssetExportSession。

应用AVAssetExportSession

AVAssetExportSession用于将AVAsset内容根据导出预设条件进行转码,并将导出资源写到磁盘中。AVAssetExportSession提供了多个功能来实现将一种格式转换为另一种格式、修订资源的内容、修改资源的音频和视频行为,当然还有我们最感兴趣的功能,即写入新的元数据。

创建一个AVAssetExportSession实例需要提供资源和导出预设。导出预设用于确定导出内容的质量、大小等属性。创建一个导出会话后,并且需要指定一个outputURL用于声明导出内容将要写入的地址,并且需要给出一个outputFileType值来表示将要写入的导出格式。最后调用exportAsynchronouslyWithCompletionHandler:方法开始导出过程。

在此并不对AVAssetExportSession的一些抽象概念进行讲述,而是直接来看saveWithCompletetionHandler:方法的实现。代码清单3-17给出了这个方法的具体实现。

代码清单3-17 THMedialtem 的saveWithCompletionHandler:的实现

- (void)saveWithCompletionHandler:(THCompletionHandler)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);
            });
        }
    }];
}

- (NSURL *)tempURL {
    NSString *tempDir = NSTemporaryDirectory();
    NSString *ext = [[self.url lastPathComponent] pathExtension];
    NSString *tempName = [NSString stringWithFormat:@"temp.%@", ext];
    NSString *tempPath = [tempDir stringByAppendingPathComponent:tempName];
    return [NSURL fileURLWithPath:tempPath];
}

- (void)reset {
    _prepared = NO;
    _asset = [AVAsset assetWithURL:self.url];
}

(1) 首先使用AVAssetExportPresetPassthrough预设值创建一个AVAssetExportSession。AVAssetExportSession具有多个预设选项,不过AVAssetExportPresetPassthrough预设值可以让我们在不需要重新对媒体编码的前提下实现写入元数据的功能。在将实际媒体内容进行转码时,"passthrough"导出过程的时间很短。

(2)为临时文件创建一个写入磁盘的URL,该URL基 于当前url属性值,只不过在文件名后面附上temp字样。

(3)取得THMetadata实例的metadatalItems属性,这一步会返回一个包含用户接口值状态的AVMutableMetadataItem实例数组。

(4)在导出会话的completionhandler中,首先要判断导出状态。如果导出状态为AVAssetExportSessionCompleted,意味着导出成功,需要删除旧资源,改用新导出的版本。本例中我们做的有些快而忽略了对于文件操作的错误处理程序。在实际的产品中需要对各种可能出现的问题进行处理。

(5)最后调用私有的reset方法来重置媒体项的prepared状态并重新初始化基础资源。

MetaManager应用程序终于全部完成了!现在可以运行该程序并对已有的元数据项进行修改,并且还可以将这些改动保存到磁盘中。

注意:
AVAssetExportPresetPassthrough预设值在一些特定场景中是有用的,并且对于演示类应用程序非常适合。不过要注意它有一定的限制, 它确实允许修改MPEG-4和QuickTime容器中存在的元数据信息,不过它不可以添加新的元数据。添加元数据的唯一方 法是使用转码预设值。此外,它不能用于修改ID3 标签。框架不支持写入MP3数据,这也是为什么在演示程序中MP3文件是只读类型的原因。

3.8 小结

本章我们已经介绍了很多基础知识!本章的内容很多,不过最重要的是我们为之后内容的学习做好了充分准备。主要学习了AVAsset和AVAssetTrack类并理解了使用AVAsynchronous-KeyValueLoading协议异步获取属性的重要性。深入研究AV Foundation基于AVMetadataItem和AVMutableMetadataItem类的元数据功能,并讨论了更底层的数据操作方法。最后,我们第一次接触AVAssetExportSession类 并将它很好地用在MetaManager应用程序中。这绝对不是我们最后一-次使用这些类, 并且随着我们学习的深入还会涉及这些类在其他方面的应用。

现在我们已经为这趟AV Foundation学习之旅做好了充分准备。

3.9 挑战

本章对如何读取或写入最常见的元数据进行了详细介绍,但这还远远不够。复制媒体库中的一首歌曲或一部电影并在iTunes中对它进行完整的标注。使用十六进制编辑器查看数据是如何保存及保存在什么位置。当你开始接触更高级的AV Foundation用例时,掌握不同容器格式中的数据存储方式会大有帮助。

相关文章

网友评论

      本文标题:AVFoundation开发秘籍笔记:第3章 资源和元数据

      本文链接:https://www.haomeiwen.com/subject/llntdktx.html