3.1 理解資源的含義
AVAsset是一個(gè)抽象 類和不可變類,定義了媒體資源混合呈現(xiàn)的方式,將媒體資源的靜態(tài)屬性模塊化成一個(gè)整體匾南,比如它們的標(biāo)題啃匿、時(shí)長和元數(shù)據(jù)等。
AVAsset不需要考慮媒體資源所具有的兩個(gè)重要范疇蛆楞。
第一個(gè):它提供了對基本媒體格式的層抽象溯乒,這意味著無論是處理QuickTime影片、MPEG-4視頻還是MP3音頻豹爹,對你和對框架其余部分而言裆悄,面對的只有資源這個(gè)概念。
第二個(gè):AVAsset隱藏了 資源的位置信息臂聋。
AVAsset本身并不是媒體資源光稼,但是它可以作為時(shí)基媒體的容器。它由一個(gè)或多個(gè)帶有描述自身元數(shù)據(jù)的媒體組成孩等。如圖3-1所示艾君。
資源的曲目可通過其tracks屬性進(jìn)行訪問。對該屬性的請求會(huì)返回一個(gè)NSArray肄方,該數(shù)組中的元素就是專輯包含的所有曲目冰垄。此外,AVAsset還可 以通過標(biāo)識符权她、媒體類型或媒體特征等信息找到相應(yīng)的曲目虹茶。
3.2 創(chuàng)建資源
當(dāng)你為一個(gè)現(xiàn)有媒體資源創(chuàng)建AVAsset對象時(shí)逝薪,可以通過URL對它進(jìn)行初始化來實(shí)現(xiàn)。一般來說是一個(gè)本地文件URL蝴罪,但也可能是遠(yuǎn)程資源的URL董济。
NSURL *assetURL = // url
AVAsset *asset = [AVAsset assetWithURL: assetURL];
AVAsset是一個(gè)抽象類,意味著它不能直接被實(shí)例化洲炊。當(dāng)使用它的assetWithURL:方法創(chuàng)建實(shí)例時(shí)感局,實(shí)際上是創(chuàng)建了它子類的一個(gè)實(shí)例,子類名為AVURLAsset暂衡。有時(shí)我們會(huì)直接使用這個(gè)類询微,因?yàn)樗试S通過傳遞選項(xiàng)字典來精細(xì)調(diào)整資源的創(chuàng)建方式。
NSURL *assetURL = // url
NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey:@YES};
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:assetURL options:options];
3.2.1 iOs Assets庫
用戶使用系統(tǒng)自帶的Camera程序或第三方視頻捕捉程序捕捉的視頻狂巢,它們通常保存在用戶的照片庫中撑毛。iOS提供的Assets庫框架可以實(shí)現(xiàn)從照片庫中讀寫的功能。下例從用戶資源庫中的視頻創(chuàng)建一個(gè)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庫
我們獲取媒體的一個(gè)常見位置是用戶的iPod庫唧领。MediaPlayer框架提供 了API藻雌,實(shí)現(xiàn)在這個(gè)庫中查詢和獲取條目。當(dāng)需要的條目找到后斩个,可以獲取其URL并使用這個(gè)URL初始化一個(gè)資源胯杭,如下例所示:
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框架提供了一個(gè)名為MPMediaPropertyPredicate的類,它用于構(gòu)建幫助用戶在iPod庫中查找具體內(nèi)容所用的查詢語句受啥。上述示例代碼在Foo Fighter的In YourHonor唱片中查找Best ofYou這首歌做个。在一條查詢語句成功執(zhí)行后,會(huì)捕捉到這個(gè)媒體條目的資源URL屬性滚局,并使用這個(gè)屬性創(chuàng)建一個(gè)AVAsset居暖。
3.2.3 Mac iTunes庫
在OS X上,iTunes是用戶的中心媒體資源庫藤肢。要識別這個(gè)庫中的資源太闺,開發(fā)者通常要對iTunes音樂目錄中的iTunes Music Library.xml文件進(jìn)行解析,從而得到相關(guān)數(shù)據(jù)嘁圈。雖然這確實(shí)是一個(gè)可行方案省骂,不過從Mac OS X 10.8和iTunes 11.0開始,我們有了更簡單的方法最住,這要?dú)w功于iTunesLibrary框架冀宴。讓我們看一下如何對庫中資源進(jìn)行查詢。
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温学。不過開發(fā)者可使用標(biāo)準(zhǔn)的Cocoa NSPredicate類來構(gòu)建一個(gè)復(fù)雜查詢略贮,從而查找所需的條目。當(dāng)篩選出所需的媒體條目集合后,可使用ITLibMedialtem獲取location屬性逃延,這樣就能得到一個(gè)適于創(chuàng)建AVAsset對象的URL览妖。
3.3 異步載入
AVAsset具有多種有用的方法和屬性,可以提供有關(guān)資源的信息揽祥,比如時(shí)長讽膏、創(chuàng)建日期和元數(shù)據(jù)等。AVAsset還包含一些用于獲取和使用曲目集合的方法拄丰。不過有一點(diǎn)很重要府树,就是當(dāng)創(chuàng)建時(shí),資源就是對基礎(chǔ)媒體文件的處理料按。AVAsset使用一種高效的設(shè)計(jì)方法奄侠, 即延遲載入資源的屬性,直到請求時(shí)才載入载矿。這樣就可以快速地創(chuàng)建資源垄潮,而不用考慮因?yàn)榱⒓摧d入相關(guān) 媒體或元數(shù)據(jù)所帶來的問題,不過有一點(diǎn)很重要闷盔,就是屬性的訪問總是同步發(fā)生的弯洗。如果正在請求的屬性沒有預(yù)先載入,程序就會(huì)阻塞逢勾,直到其可以做出適當(dāng)?shù)捻憫?yīng)牡整,顯然這種情況一定會(huì)帶來問題。比如溺拱,請求資源的duration可能是一個(gè)潛在的昂貴操作逃贝。如果開發(fā)者在使用MP3文件時(shí)沒有在頭文件中設(shè)置TLEN標(biāo)簽,這個(gè)標(biāo)簽用于定義duration值盟迟,則整個(gè)音頻曲目都需要進(jìn)行解析來準(zhǔn)確確定它的duration值秋泳。假設(shè)這個(gè)請求發(fā)生在主線程潦闲,那么等待響應(yīng)就會(huì)阻塞主線程攒菠,直到相關(guān)操作完成為止。在最好的情況下歉闰,可能會(huì)感覺應(yīng)用程序變得遲鈍辖众,用戶界面沒有響應(yīng)。在iOS上和敬,最糟情況下凹炸,可能會(huì)出現(xiàn)卡頓,導(dǎo)致系統(tǒng)監(jiān)視器介入昼弟,并終止應(yīng)用程序的運(yùn)行啤它。如果出現(xiàn)這種情況,沒理由不讓用戶給子程序"1星"評價(jià)。要解決這一問題变骡,開發(fā)者應(yīng)該使用異步的方式來查詢資源的屬性离赫。
AVAsset和AVAssetTrack都采用了AVAsynchronousKeyValueLoading協(xié)議。該協(xié)議通過下面給出的這些方法實(shí)現(xiàn)了異步查詢屬性的功能:
- (AVKeyValueStatus)statusOfValueForKey:(NSString *)key error:(NSError *)outError;
- (void)loadValuesAsynchronouslyForKeys:(NSArray *)keys completionHandler:(void (^)(void))handler;
可使用statusOfValueForKey:error:方法查詢一個(gè)給定屬性的狀態(tài)塌碌,該方法會(huì)返回一個(gè)枚舉類型的AVKeyValueStatus值渊胸,用于表示當(dāng)前所請求的屬性的狀態(tài)。如果狀態(tài)不是AVKeyValueStatusLoaded,意味著此時(shí)請求該屬性可能導(dǎo)致程序卡頓台妆。要異步載入一個(gè)給定的屬性,可以調(diào)用loadValuesAsynchronouslyForKeys: completionHandler:方法,為其提供-個(gè)具有一個(gè)或多個(gè)key的數(shù)組(資源的屬性名)和一個(gè)completionHandler塊帕膜, 當(dāng)資源處于回應(yīng)請求的狀態(tài)時(shí)珊皿,就會(huì)調(diào)用這個(gè)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
}
}];
該例為存儲(chǔ)在應(yīng)用程序bundle中的QuickTime電影創(chuàng)建了一個(gè)AVAsset搂漠,并異步載入了該對象的tracks屬性迂卢。在completionHandler塊中,我們希望通過調(diào)用資源的statusOfValueForKey:error:方法來確定請求屬性的狀態(tài)桐汤。將NSError指針傳遞給這個(gè)方法很重要而克,因?yàn)槿绻祷氐臓顟B(tài)為AVKeyValueStatusFailed,則說明資源包含錯(cuò)誤信息。通常會(huì)切換狀態(tài)值怔毛,并根據(jù)返回的狀態(tài)采取適當(dāng)操作员萍。要注意的是這個(gè)completionHandler塊可能會(huì)在任意一個(gè)隊(duì)列中 被調(diào)用。在對用戶界面做出相應(yīng)更新之前拣度,必須首先回到主隊(duì)列中碎绎。
注意:
該例載入一個(gè)tracks 屬性,其實(shí)可以在一個(gè)調(diào)用中請求多個(gè)屬性抗果。當(dāng)請求多個(gè)屬性時(shí)筋帖,需要注意以下兩點(diǎn):
(1)每次調(diào)用loadValuesAsynchronouslyForKeys:completionHandler:方 法時(shí)只會(huì)調(diào)用一次completionHandler塊。調(diào)用該方法的次數(shù)并不是根據(jù)傳遞給這個(gè)方法的鍵的個(gè)數(shù)而定的冤馏。
(2)需要為每個(gè)請求的屬性調(diào)用statusOfValueForKeyerror:方法日麸,不能假設(shè)所有的屬性都返回相同的狀態(tài)值。
3.4 媒體元數(shù)據(jù)
當(dāng)創(chuàng)建一個(gè)媒體應(yīng)用程序時(shí)逮光,了解該媒體的組織格式非常重要代箭。簡單地向用戶呈現(xiàn)一串文件名也許在文件不多的時(shí)候還能接受,但是這個(gè)方法顯然無法滿足大規(guī)模文件的呈現(xiàn)涕刚。所以我們真正需要的是嗡综,找到一種方法對媒體進(jìn)行描述,讓用戶可以簡單地找到杜漠、識別和組織這些媒體极景。幸運(yùn)的是察净,我們所使用的AV Foundation中的主要媒體格式都可以嵌入描述其內(nèi)容的元數(shù)據(jù)。對元數(shù)據(jù)的使用具有一定的挑戰(zhàn)性盼樟,因?yàn)槊總€(gè)媒體類型都具有唯一的格式塞绿,并且通常要求開發(fā)者對相應(yīng)格式讀寫操作的底層技術(shù)有所了解。不過AV Foundation讓這一切變得簡單恤批,因?yàn)樗归_發(fā)者不需要考慮大多數(shù)特定格式的細(xì)節(jié);在處理媒體元數(shù)據(jù)方面异吻,AVFoundation提供了一套統(tǒng)一的方法。本章剩余的內(nèi)容將深入研究AV Foundation元數(shù)據(jù)的相關(guān)細(xì)節(jié)問題喜庞,學(xué)習(xí)如何在應(yīng)用程序中實(shí)現(xiàn)這些功能诀浪。在我們開始詳解前,先來看一下元數(shù)據(jù)是如何以多種不同格式保存的延都。
元數(shù)據(jù)格式
雖然存在多種格式的媒體資源雷猪,但是我們在Apple環(huán)境下遇到的媒體類型主要有4種,分別是: QuickTime(mov)晰房、 MPEG-4 video(mp4和Im4v)求摇、MPEG 4 audio(m4a)和MPEG-Layer IIaudio(mp3)。雖然AV Foundation在處理這些文件中嵌入的元數(shù)據(jù)時(shí)都使用一個(gè)接口殊者,但是理 解這些不同類型資源的元數(shù)據(jù)如何存儲(chǔ)及存儲(chǔ)位置仍然很有價(jià)值与境。這里只做概述,但它是未來深入研究的基礎(chǔ)猖吴。
1. QuickTime
QuickTime是由蘋果公司開發(fā)的一種功能強(qiáng)大摔刁、跨平臺的媒體架構(gòu)。該架構(gòu)的一部分 是QuickTime File Format規(guī)范海蔽, 定義了.mov文件的內(nèi)部結(jié)構(gòu)共屈。QuickTime文件由一種稱 為atoms的數(shù)據(jù)結(jié)構(gòu)組成。一般規(guī)則是這樣的: 一個(gè)atom包含了描述媒體資源某一方面的數(shù)據(jù)党窜,或者它可以包含其他atom,但不可以兩者都包含拗引。有些情況下,蘋果公司自己的方法實(shí)現(xiàn)可能會(huì)違背這一規(guī)則幌衣。atom以- -種復(fù)雜的樹狀結(jié)構(gòu)組合在一起矾削,詳細(xì)地對布局、音頻樣本格式泼掠、視頻幀信息乃至需要呈現(xiàn)的元數(shù)據(jù)信息(如作者信息和版權(quán)信息)做了描述怔软。
了解QuickTime格式的一個(gè)好辦法就是在十六進(jìn)制編輯器中打開一個(gè).mov格式的文件垦细,常見的十六進(jìn)制編輯器有Hex Fiend或Synalyze It! Pro择镇。 典型的十六進(jìn)制工具會(huì)將一個(gè)真實(shí)QuickTime文件的數(shù)據(jù)呈現(xiàn)到開發(fā)者眼前,不過想象其中的結(jié)構(gòu)和atom間的關(guān)系也不是那么容易括改。更好的方法是借助于Apple Developer Center中可以找到的Atom Inspector工具腻豌。這個(gè)工具將atom結(jié)構(gòu)以NSOutlineView方式呈現(xiàn),所以就可以對atom之間的繼承關(guān)系等信息有比較清晰的了解,這個(gè)工具還提供了一個(gè)小型的十六進(jìn)制查看器吝梅,可以從中查看到實(shí)際字節(jié)布局虱疏。
《超人總動(dòng)員》是作者非常喜歡的一部由Pixar公 司出品的電影,圖3-2給出了這部作品;QuickTime版本結(jié)構(gòu)的簡化示意圖苏携。該QuickTime文件最小限度地包含了三個(gè)高級atom,分別是用于描述文件類型和兼容類型的ftyp,包含實(shí)際音頻和視頻媒體的mdat以及非常重要的moov atom(讀作moo-vee),它對媒體資源的所有相關(guān)細(xì)節(jié)做了完整描述做瞪,包括可呈現(xiàn)的元數(shù)據(jù)。
當(dāng)處理QuickTime電影時(shí)會(huì)遇到兩種類型的元數(shù)據(jù)右冻。標(biāo)準(zhǔn)QuickTime 元數(shù)據(jù)由諸如Final Cut Pro X這樣的工具編寫装蓬,位于/moov/meta/ilst/中,它的鍵幾乎都具有com.apple.quicktime前綴纱扭。其他類型的數(shù)據(jù)被認(rèn)為是QuickTime用戶數(shù)據(jù)牍帚,保存在/moov/udta/中。QuickTime用戶數(shù)據(jù)可以包括播放器需要查找的標(biāo)準(zhǔn)數(shù)據(jù)乳蛾,比如歌曲演唱者或版權(quán)信息暗赶,不過同時(shí)還可以包含任何對應(yīng)用程序有幫助的信息。上述兩種元數(shù)據(jù)類型在AVFoundation中都是可以讀寫的肃叶。
對不同atom間的關(guān)系和相關(guān)細(xì)節(jié)的理解不在我們的討論范圍內(nèi)蹂随。實(shí)際上,蘋果公司出具了一份400多頁的名為QuickTime File Format Specification 的文檔因惭,該文檔全面介紹了QuickTime文件格式的所有相關(guān)內(nèi)容糙及。雖然成為QuickTime領(lǐng)域的專家并不重要,但是我們還是建議大家對核心moov atom有所了解筛欢,因?yàn)檫@有助于你更好地掌握AV Foundation是如何使用這些數(shù)據(jù)的浸锨。
2. MPEG-4音頻和視頻
MPEG-4 Part 14是定義MP4文件格式的規(guī)范。MP4直接派生于QuickTime文件格式版姑,這就意味著它與QuickTime文件的結(jié)構(gòu)是類似的柱搜。實(shí)際上,我們經(jīng)常會(huì)發(fā)現(xiàn)剥险,能夠解析一種文件類型的工具也可以處理其他文件類型(有著不同程度的成功率)聪蘸。就像QuickTime文件-樣,MP4文件也由稱為atom的數(shù)據(jù)結(jié)構(gòu)組成。技術(shù)上講表制,MPEG-4規(guī)范將這些稱為boxes,不過考慮到該格式的QuickTime血統(tǒng)健爬,大部分開發(fā)者還是把其稱為atom。圖3-3再次給出了《超人總動(dòng)員》的例子么介,不過這次是MP4格式娜遵。
MPEG- 4文件的元數(shù)據(jù)保存在/moov/udta/meta/ilst/中。雖然對于在atom中使用的鍵沒有標(biāo)準(zhǔn)壤短,但大部分工具都遵循蘋果公司尚未發(fā)布的iTunes元數(shù)據(jù)規(guī)范中對鍵的定義设拟。雖然沒有正式發(fā)布慨仿,但是iTunes元數(shù)據(jù)格式的相關(guān)文檔在互聯(lián)網(wǎng)上已經(jīng)廣為人知了。我們可以在開源的mp4v2庫7中找到一個(gè)對這種格式進(jìn)行講解的優(yōu)秀文檔纳胧。
在實(shí)踐中镰吆,大家對于該文件的擴(kuò)展名還存在一些困惑。.mp4是對MPEG-4媒體的標(biāo)準(zhǔn)擴(kuò)展跑慕,但存在一些變化万皿, 如.m4v、 .m4a核行、 .m4p和.m4b相寇。這些變體都使用MPEG-4容器格式,但有些包含了附加的擴(kuò)展功能钮科。M4V文件是帶有蘋果公司針對FairPlay加密及AC3-audio擴(kuò)展的MPEG-4視頻格式唤衫。如果不涉及FairPlay加密和AC3-audio擴(kuò)展,則MP4和M4V就僅僅是擴(kuò)展名不同而已绵脯。M4A專門針對音頻佳励,使用變化的擴(kuò)展名的目的是讓使用者知道該文件只帶有音頻資源。M4P是蘋果較舊的iTunes音頻格式蛆挫,使用其FairPlay擴(kuò)展赃承。M4B用于有聲讀物,并通常包含章節(jié)標(biāo)簽以及提供書簽功能悴侵,讓讀者可以返回到指定位置開始閱讀瞧剖。
3. MP3
MP3文件與上面介紹的兩種格式有顯著區(qū)別。MP3文件不使用容器格式可免,而使用編碼音頻數(shù)據(jù)抓于,包含的可選元數(shù)據(jù)的結(jié)構(gòu)塊通常位于文件開頭。MP3文件使用一種稱為ID3v2的格式來保存關(guān)于音頻內(nèi)容的描述信息浇借,包含的數(shù)據(jù)有歌曲演唱者捉撮、所屬唱片和音樂風(fēng)格等。
使用十六進(jìn)制工具研究MP3格式的架構(gòu)是一個(gè)好辦法妇垢。不要擔(dān)心巾遭,與上面小節(jié)中介紹的atom相比,ID3數(shù)據(jù)很容易理解闯估。MP3文件的前10個(gè)字節(jié)帶有嵌入的元數(shù)據(jù)灼舍,這10個(gè)字節(jié)定義了ID3塊的頭部。該頭部的前三個(gè)字節(jié)始終為'49 44 33' (ID3),用于表示這是一個(gè)ID3v2標(biāo)簽涨薪,后面兩個(gè)字節(jié)用于定義主版本信息骑素,即2、3尤辱、4和版本號砂豌。剩余字節(jié)用于定義標(biāo)志集合及ID3塊的大小,如圖3-4所示光督。
ID3塊中剩下的數(shù)據(jù)都是用于描述不同元數(shù)據(jù)鍵值的幀阳距。每一幀都有一個(gè)帶有實(shí)際標(biāo)簽名稱的10字節(jié)的頭,之后的4字節(jié)表示尺寸结借,再之后的兩個(gè)字節(jié)用來定義選項(xiàng)標(biāo)志筐摘。幀剩下的字節(jié)包含了實(shí)際的元數(shù)據(jù)值。如果值是文本類型船老,這是最常見的情況咖熟,tag中的第一個(gè)字節(jié)用來定義類型編碼。類型編碼通常被設(shè)置為0x00柳畔,代表ISO-8859-1, 不過這里也支持其他類型的編碼馍管。圖3-5給 出了John Coltrane經(jīng) 典曲目Giant Steps的MP3版本中的ID3結(jié)構(gòu)示意圖。
AV Foundation支持讀取ID3v2標(biāo)簽的所有版本薪韩,但不支持寫入确沸。MP3格式受到專利限制,所以AV Foundation無法支持對MP3或ID3數(shù)據(jù)進(jìn)行編碼俘陷。
注意:
AV Foundation支持所有ID3v2標(biāo)簽格式的讀取操作罗捎,但是ID3v2是要加星號的。ID3v2.2的布局和ID3v2.3及之后版本的布局不同拉盾。值得注意的是桨菜,有些標(biāo)簽是由3個(gè)字符組成而非4個(gè),比如一首歌曲的標(biāo)注信息捉偏,當(dāng)標(biāo)簽為ID3v2.2時(shí)倒得,是被保存在COM幀中,但當(dāng)同一首歌使用ID3v2.3標(biāo)簽或更新版本的標(biāo)簽時(shí)夭禽,歌曲的標(biāo)注信息會(huì)被保存在COMM幀中屎暇。框架定義的字符常量只適用于ID3v2.3及之后版本驻粟。不過我們會(huì)在之后的示例程序中告訴你如何仍使用ID3v2.2數(shù)據(jù)根悼。
3.5 使用元數(shù)據(jù)
AVAsset和AVAssetTrack都可以實(shí)現(xiàn)查詢相關(guān)元數(shù)據(jù)的功能。大部分情況下我們會(huì)使用AVAsset提供的元數(shù)據(jù)蜀撑,不過當(dāng)涉及獲取曲目一級元數(shù)據(jù)等情況時(shí)也會(huì)使用AVAssetTrack.讀取具體資源元數(shù)據(jù)的接口由名為AVMetadataItem的類提供挤巡。這個(gè)類提供了一個(gè)面向?qū)ο蟮慕涌冢岄_發(fā)者可以對存儲(chǔ)于QuickTime酷麦、MPEG-4 atom和ID3幀中的元數(shù)據(jù)進(jìn)行訪問矿卑。
AVAsset和AVAssetTrack提供了兩種方法可以獲取相關(guān)的元數(shù)據(jù)。要了解這兩個(gè)不同的方法的適用范圍沃饶,首先要知道鍵空間(key spaces)的含義母廷。AV Foundation使用鍵空間作為將相關(guān)鍵組合在一起的方法轻黑, 可以實(shí)現(xiàn)對AVMetadataItem實(shí)例集合的篩選。每個(gè)資源至少包含兩個(gè)鍵空間琴昆,供從中獲取元數(shù)據(jù)氓鄙,如圖3-6所示。
Common鍵空間用來定義所有支持的媒體類型的鍵业舍,包括諸如曲名抖拦、歌手和插圖信息等常見元素。這提供了一種對所有支持的媒體格式進(jìn)行一定級別的元數(shù)據(jù)標(biāo)準(zhǔn)化的過程舷暮。開發(fā)者可以通過查詢資源或曲目的commonMetadata屬性從Common鍵空間獲取元數(shù)據(jù)态罪,這個(gè)屬性會(huì)返回一個(gè)包含所有可用元數(shù)據(jù)的數(shù)組。
訪問指定格式的元數(shù)據(jù)需要在資源或曲目上調(diào)用metadataForFormat:方法下面。 這個(gè)方法包含一個(gè)用于定義元數(shù)據(jù)格式的NSString對象并返回一個(gè)包含所有相關(guān)元數(shù)據(jù)信息的NSArray复颈。AVMetadataFormath為不同的元數(shù)據(jù)格式提供對應(yīng)的字符串常量。與硬編碼某個(gè)具體的元數(shù)據(jù)格式字符串不同沥割,也可以查詢資源或曲目的availableMetadataFormats券膀,其會(huì)返回一個(gè)字符串?dāng)?shù)組,其中定義了資源中包含的所有元數(shù)據(jù)格式驯遇。開發(fā)者可以利用這個(gè)結(jié)果數(shù)組循環(huán)訪問所有的格式芹彬,并為每種格式調(diào)用metadataForFormat:方法。讓我們看一個(gè)例子:
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 查找元數(shù)據(jù)
當(dāng)我們得到一個(gè)包含元數(shù)據(jù)項(xiàng)的數(shù)組時(shí)叉庐,通常希望找到所需的具體元數(shù)據(jù)值舒帮。一個(gè)特別有效的做法是使用AVMetadataItem提供的便利方法,來獲取結(jié)果集合并對其進(jìn)行篩選陡叠。例如玩郊,如果開發(fā)者希望得到一個(gè)M4A音頻文件的演奏者和唱片的元數(shù)據(jù),需要按如下方法獲 取這些數(shù)據(jù):
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:方法來對集合進(jìn)行篩選枉阵,得出那些匹配鍵和鍵空間標(biāo)準(zhǔn)的對象译红。這個(gè)方法的返回值是一個(gè)NSArray, 但通常只包含單個(gè)AVMetadataItem實(shí)例。
3.5.2 使用AVMetadataltem
AVMetadataltem最基本的形式其實(shí)是一個(gè)封裝鍵值對的封裝器兴溜≌旌瘢可通過它查詢key或commonKey,查詢其是否存在于Common鍵空間中拙徽,最重要的是它對應(yīng)的value刨沦。value屬性 被定義成id<NSObject,NSCopying>形式膘怕,不過它可能是NSString想诅、NSNumber、NSData或其他一些情況,比如NSDictionary来破。如果開發(fā)者已經(jīng)提前知道value的類型篮灼,AVMetadatalItem還會(huì)提供三個(gè)類型強(qiáng)制屬性一stringValue、 numberValue和dataValue--這簡化了輸入相應(yīng)返回值的過程徘禁。
大部分開發(fā)者在第一次接觸AVMetadataltem時(shí)都會(huì)碰到的一個(gè)常見問題是诅诱,如何理解該對象的key屬性。commonKey被定義為一個(gè)字符串晌坤, 并且很容易估算它在AVMetadataFormat.h中所對應(yīng)的key逢艘,不過key屬性是以id<NSObject旦袋,NSCopying>值的形式定義的骤菠。雖然這個(gè)類型可以保存NSString,不過很少這么使用疤孕。比如商乎,如果通過下面的代碼循環(huán)訪問嵌入到庫中的M4A文件內(nèi)的元數(shù)據(jù),就會(huì)得到錯(cuò)誤的鍵值:
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);
}
執(zhí)行這段代碼將生成如下清單:
-1452383891: Have A Drink On Me
-1455336876: AC/DC
-1451789708: A. Young - M. Young - B. Johnson
-1453233054: Back In Black
-1453039239: 1980
雖然我們通過具體的結(jié)果可以看出元數(shù)據(jù)信息所描述的歌曲是Back in Black唱片中的AC/DC鹉戚,不過對于key屬性返回的整數(shù)形式無法知道它的含義。我們看到的是不同的key字符串所對應(yīng)的整數(shù)值专控。在對這些值進(jìn)行解釋前抹凳,需要將它們轉(zhuǎn)換為相應(yīng)的字符串。由于這是一類常見問題伦腐,因此可在AVMetadataItem上添加一個(gè)名為keyString的分類方法赢底,這樣就可以很容易地獲取NSString所對應(yīng)的內(nèi)容了。這個(gè)分類方法的實(shí)現(xiàn)如代碼清單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屬性已經(jīng)是一個(gè)字符串幸冻,則原樣返回,但這并不常見咳焚。
(2)請求key的無符號整型值洽损。返回值是一個(gè)稍后將提取出的表示4字符代碼的32位big endian數(shù)字。
(3)大部分情況下革半,value值 都是一個(gè)4字符代碼碑定,比如Ogen或TRAK,不過對于使用ID3v2.2標(biāo)簽的MP3文件,鍵值只有3個(gè)字符的長度又官。代碼會(huì)移動(dòng)每個(gè)字節(jié)來確定length值是否應(yīng)該截短不傅。
(4)由于數(shù)字是big endian格式,因此使用CFSwapInt32BigToHostO函數(shù)將其轉(zhuǎn)換為符合主CPU的順序的little endian格式赏胚,Intel和ARM處 理器都是這樣要求的访娶。
(5)創(chuàng)建一個(gè)字符數(shù)組,并使用strmcpy函數(shù)將字符字節(jié)填充到該數(shù)組中觉阅。
(6)大量QuickTime用戶數(shù)據(jù)和iTunes key的前綴都帶有一個(gè)[圖片上傳失敗...(image-91eb0f-1596720315944)] 符號崖疤。 不過AVMetadataFormat.h中定義key所使用的前綴符號為@秘车。所以為了進(jìn)行key常量字符串比較,需要先將[圖片上傳失敗...(image-6e567c-1596720315944)] 替換為@劫哼。
(7)最后叮趴,使用tringWithCString:encoding初始化器將 字符數(shù)組轉(zhuǎn)換為一個(gè)NSString。
如果我們導(dǎo)入這個(gè)分類并將前一示例修改為使用這個(gè)新方法权烧, 就會(huì)得到下面的結(jié)果眯亦,結(jié)果已不再那么難懂。
@nam: Have A Drink On Me
@ART: AC/DC
@wrt: A. Young - M. Young - B. Johnson
@alb: Back In Black
@day: 1980
注意:
除了通過鍵和鍵空間獲取資源的元數(shù)據(jù)之外般码,Mac OS X 10.10和iOS 8還引進(jìn)了使用標(biāo)識符(identifier)獲取元數(shù)據(jù)的方法妻率。標(biāo)識符將鍵和鍵空間統(tǒng)一成單獨(dú)的字符串,以一個(gè)更簡單的模型來獲取資源的元數(shù)據(jù)板祝。本章使用鍵和鍵空間的方法宫静,是因?yàn)檫@種方法可以兼容大部分OS版本,不過如果只針對Mac OSX 10.10或iOs 8進(jìn)行開發(fā)券时,完全可以考慮使用標(biāo)識符孤里。
現(xiàn)在我們基本理解AVMetadataltem,下面讓我們利用學(xué)到的知識編寫一個(gè)名為MetaManager的Mac元數(shù)據(jù)編輯器應(yīng)用程序橘洞。
3.6 創(chuàng)建 MetaManager應(yīng)用程序
MetaManager應(yīng)用程序提供了一個(gè)簡單界面捌袜,讓用戶可以查看并編輯元數(shù)據(jù)(如圖3-7所示)。該程序可以讓用戶查看AV Foundation支持的所有媒體類型的元數(shù)據(jù)信息炸枣,還可以對除了MP3之外的所有資源寫入元數(shù)據(jù)信息虏等。該應(yīng)用程序很好地展示了如何使用這些元數(shù)據(jù)類,同時(shí)也對剛剛接觸該框架的開發(fā)者經(jīng)常遇到的一些問題給出了很好的解決方案抛虏。用戶界面創(chuàng)建完畢后博其,你只需把注意力放在如何構(gòu)建基礎(chǔ)AV Foundation框架上∮睾铮可在Chapter3目錄中找到名為MetaManager starter的項(xiàng)目代碼慕淡。
雖然AV Foundation的元數(shù)據(jù)類可在一定 程度上抽象因基礎(chǔ)元數(shù)據(jù)格式不同而帶來的區(qū)別,不過要對多種媒體格式的元數(shù)據(jù)進(jìn)行統(tǒng)一管理還是很有挑戰(zhàn)的沸毁。要對基礎(chǔ)元數(shù)據(jù)提供一個(gè)統(tǒng)一界面峰髓,應(yīng)該采用的策略是將特定格式的元數(shù)據(jù)映射到一個(gè)標(biāo)準(zhǔn)化的鍵值集合中。這就為基礎(chǔ)元數(shù)據(jù)提供了一個(gè)簡單息尺、一致的用戶界面携兵,并使得管理這些元數(shù)據(jù)的邏輯方法可以集中在單個(gè)類中。讓我們首先實(shí)現(xiàn)第一個(gè)類THMedialtem搂誉。
3.6.1 THMedialtem
THMedialtem定義了應(yīng)用程序管理媒體最主要的接口徐紧。它封裝了基礎(chǔ)AVAsset實(shí)例并負(fù)責(zé)管理相關(guān)元數(shù)據(jù)的載入和解析。首先看一下這個(gè)類的接口,如代碼清單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實(shí)例是通過調(diào)用其initWithURL:方法創(chuàng)建的拂檩,傳遞一個(gè)磁盤中給定媒體文件的本地文件URL。應(yīng)用程序在程序bundle中包含需要的示例文件嘲碧,當(dāng)程序啟動(dòng)時(shí)將這些文件復(fù)制到~/Library/Application Support/MetaManager目錄中稻励。程序會(huì)為每個(gè)文件創(chuàng)建一個(gè)THMediaItem實(shí)例并填充媒體列表。
注意:
如果希望將媒體資源重置回默認(rèn)狀態(tài)愈涩,可以選擇Edit菜單并選擇Reset Media Item,即可將媒體重新設(shè)置成初始狀態(tài)望抽。
讓我們轉(zhuǎn)過來看一下具體的實(shí)現(xiàn)文件并開始實(shí)現(xiàn)這些方法,如代碼清單3-3所示履婉。
代碼清單3-3 THMedialtem 實(shí)現(xiàn)
#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:方法中設(shè)置類的內(nèi)部狀態(tài)煤篙,并基于傳入的URL創(chuàng)建一個(gè)AVAsset實(shí)例。應(yīng)用程序的表視圖中顯示的每個(gè)條目都是該類的一個(gè)實(shí)例谐鼎。
(2)為基礎(chǔ)資源確定文件類型舰蟆。雖然AV Foundation提供一些高級方法用于確定文件類型趣惠,但基于文件擴(kuò)展名來確定類型的方法已經(jīng)可以滿足需求了狸棍。當(dāng)實(shí)現(xiàn)程序的保存功能時(shí),我們會(huì)用到filetype屬性味悄。
(3)程序會(huì)基于媒體文件的類型來設(shè)置editable標(biāo)志草戈。雖然AV Foundation允許我們讀取ID3元數(shù)據(jù),但我們無法向其中寫入數(shù)據(jù),所以需要設(shè)置editable標(biāo)志侍瑟,使得程序可以正確設(shè)置Save按鈕的enabled狀態(tài)唐片。
(4)還需要設(shè)置-一個(gè)包含支持的所有元數(shù)據(jù)格式的數(shù)組。這么做是為了排除那些可能出現(xiàn)在AVMetadataKeySpaceQuickTimeUserData和AVMetadataKeySpaceISOUserData鍵空間中的外部重復(fù)值涨颜。
讓我們繼續(xù)研究prepareWithCompletionHandler:方法的實(shí)現(xiàn)费韭,如代碼清單3-4所示。
代碼清單3-4 prepareWithCompletionHandler:方法的實(shí)現(xiàn)
- (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)用戶每次在應(yīng)用的表視圖中選擇一個(gè)媒體項(xiàng)時(shí)庭瑰,都會(huì)調(diào)用prepareWithCompletion-Handler:方法距淫。只需將這個(gè)項(xiàng)準(zhǔn)備一次窒升, 所以如果已經(jīng)準(zhǔn)備了這個(gè)項(xiàng),則直接調(diào)用completion塊并跳出。
(2)創(chuàng)建一個(gè)新的THMetadata實(shí)例壁晒。這個(gè)我們即將實(shí)現(xiàn)的類會(huì)對保存在單獨(dú)AVMetadataltem實(shí)例中的值進(jìn)行管理。
(3)通過查看commonMetadata和availableMetadataFormats屬性的狀態(tài)來確定prepared狀態(tài)预吆。這兩個(gè)屬性一定要成功載入用于后續(xù)的進(jìn)程薄嫡。
(4)循環(huán)訪問所有由通用鍵空間返回的AVMetadataltem實(shí)例,并將每一個(gè)添加到THMetadata實(shí)例捡鱼。
(5)請求資源的availableMetadataFormats會(huì)返回一個(gè)數(shù)組八回,其中包含用于確定媒體元素中可用格式的字符串?dāng)?shù)組。如果supportedFormats集合中包含 了特定格式,就需要獲取相關(guān)的AVMetadataItem實(shí)例并將其添加到內(nèi)部元數(shù)據(jù)存儲(chǔ)中缠诅。
(6)最后調(diào)用completion handler塊使用戶界面可以將獲取的元數(shù)據(jù)呈現(xiàn)在屏幕上伟墙。繼續(xù)運(yùn)行該應(yīng)用程序。僅僅展示元數(shù)據(jù)的狀態(tài)還不夠滴铅,當(dāng)選擇單獨(dú)的媒體項(xiàng)時(shí)戳葵,可以看 到控制臺將鍵值信息輸出了。當(dāng)驗(yàn)證完這一行為后汉匙,就可以將注釋代碼清除了拱烁,或者將NSLog語句刪除掉。
下面將注意力轉(zhuǎn)到THMetadata類噩翠,重點(diǎn)研究單個(gè)AVMetadataItem實(shí)例中返回值相關(guān)的細(xì)節(jié)問題戏自。
3.6.2 THMetadata 的實(shí)現(xiàn)
THMetadata類實(shí)現(xiàn)了這個(gè)應(yīng)用程序中大部分重要的功能。它負(fù)責(zé)從相關(guān)元數(shù)據(jù)元素中提取值伤锚,并將它們保存供日后使用擅笔。在這個(gè)類中,我們?nèi)孕鑼τ成涞接脩艚缑嫔咸厥庾侄蔚乃胁煌I值做標(biāo)準(zhǔn)化操作屯援。讓我們先看一下代碼清單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
該接口定義了多種可在屏幕上呈現(xiàn)的數(shù)據(jù)值。每個(gè)屬性都對應(yīng)用戶接口中的一個(gè)特殊字段狞洋。之后程序定義了addMetadataItem:方法弯淘,向其內(nèi)部集合添加一個(gè)AVMetadataItem。最后定義了一個(gè)metadataltems方法用于獲取已經(jīng)更新的元數(shù)據(jù)吉懊,這樣用戶就可以對所發(fā)生的變更進(jìn)行保存庐橙。讓我們轉(zhuǎn)到具體的實(shí)現(xiàn)中(如代碼清單3-6所示)。
代碼清單3-6 THMetadata 方法的實(shí)現(xiàn)
#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)要完成對不同鍵空間的鍵和格式的標(biāo)準(zhǔn)化工作借嗽,需要?jiǎng)?chuàng)建一個(gè) 從指定格式鍵到標(biāo)準(zhǔn)化鍵的映射态鳖。標(biāo)準(zhǔn)化的鍵常量在THMetadataKeys.h中定義。代碼清單3-6給出了buildKeyMappings方法的縮略版本恶导。要了解所有鍵值的映射關(guān)系浆竭,一定要查看源代碼中程序的完整版本。
(2)創(chuàng)建一個(gè)NSMutableDictionary來保存 從AVMetadataItem值提取到的展示內(nèi)容甲锡。保存在這個(gè)字典對象中的值就是最后在屏幕上呈現(xiàn)的值兆蕉。
(3)創(chuàng)建一個(gè)THMetadataConverterFactory實(shí)例來聲明THMetadataConverter實(shí)例。 我們要?jiǎng)?chuàng)建的這個(gè)轉(zhuǎn)換對象用于在保存于AVMetadataltem中的數(shù)據(jù)和呈現(xiàn)在屏幕上的值之間進(jìn)行轉(zhuǎn)換缤沦。
接下來看一下FaddMetadataItem:方法的實(shí)現(xiàn)(如代碼清單3-7所示)虎韵。
代碼清單3-7 addMetadataltem:的實(shí)現(xiàn)
- (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)在這個(gè)方法中,我們首先為傳進(jìn)來的鍵找到標(biāo)準(zhǔn)化的鍵缸废。也就是將指定格式的鍵映射為標(biāo)準(zhǔn)化的鍵包蓝。如果返回得到標(biāo)準(zhǔn)化的值驶社,則繼續(xù)處理AVMetadataItem。
(2)之后在converterFactory中請求能夠處理當(dāng)前AVMetadataltem的THMetadataConverter對象测萎。通過轉(zhuǎn)換器獲取當(dāng)前元數(shù)據(jù)項(xiàng)對應(yīng)的展示值亡电,即將元數(shù)據(jù)的值通過提取和轉(zhuǎn)換使其成為能夠用于展示的格式。
(3)當(dāng)?shù)玫揭粋€(gè)正確的返回值之后硅瞧,需要判斷它的類型份乒。唱片和曲目編號是一種特殊情況,需要其他一些操作才 能提取它們的值腕唧。最終使用鍵值編碼方setValue:forKey來設(shè)置每個(gè)獨(dú)立的屬性值或辖。
(4)最后,將AVMetadataItem保存在metadata字典中以備日后使用枣接。
如果還對這個(gè)流程感到不是很理解也不必?fù)?dān)心颂暇,開始創(chuàng)建轉(zhuǎn)換器后,我們會(huì)將這些復(fù)雜的步驟分解成一個(gè)個(gè)小的部分來實(shí)現(xiàn)但惶。讓我們開始創(chuàng)建這些對象吧耳鸯。
3.6.3數(shù)據(jù)轉(zhuǎn)換器
在處理AVMetadataItem時(shí),其中最具有挑戰(zhàn)性及最難懂的部分就是理解保存在其value屬性中的數(shù)據(jù)膀曾。當(dāng)value是一個(gè)簡單字符串時(shí)县爬,比如藝術(shù)家名字或唱片標(biāo)題,或是數(shù)字類型妓肢,比如錄制年份或心律值(BPM)捌省,這樣的數(shù)據(jù)很容易理解并且不需要轉(zhuǎn)換苫纤。不過我們經(jīng)常會(huì)遇到一些看起來很混亂的值碉钠, 或者僅僅是一些晦澀的內(nèi)容, 這就意味著需要通過一些額外努力將這些值轉(zhuǎn)換為可以展示的格式卷拘『胺希可在THMetadata類中直接加入轉(zhuǎn)換邏輯,但是這樣的話代碼量會(huì)快速增加栗弟,導(dǎo)致不易維護(hù)污筷。所以正確的做法是將這些邏輯語句分離到幾個(gè)轉(zhuǎn)換類中,當(dāng)然這些轉(zhuǎn)換類都要遵循THMetadataConverter協(xié)議乍赫,如代碼清單3-8所示瓣蛀。
代碼清單3-8 THMetadataConverter 協(xié)議
#import <AVFoundation/AVFoundation.h>
@protocol THMetadataConverter <NSObject>
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item;
- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
withMetadataItem:(AVMetadataItem *)item;
@end
THMetadataConverter協(xié)議定義了兩個(gè)方法。其中displayValueFromMetadataItem:方法用于解析AVMetadataItem實(shí)例中的數(shù)據(jù)并將其轉(zhuǎn)換為可展示的格式雷厂。它的同級方法metadataltemFrom-DisplayValue:withMetadataltem:可將展示內(nèi)容轉(zhuǎn)換為之前的AVMetadataltem格式惋增,使其始終可以保存到基礎(chǔ)媒體中。轉(zhuǎn)換對象的類接口全部遵循代碼清單3-9所示的模式改鲫,所以接下來的代碼清單只給出類的實(shí)現(xiàn)部分诈皿。
代碼清單3-9 THMetadataConverter 接口模板
#import "THMetadataConverter .h"
@interface ClassName : NsObject <THMetadataConverter>
@end
3.6.4 簡單轉(zhuǎn)換
上述協(xié)議的默認(rèn)實(shí)現(xiàn)方法很簡單林束,僅提供了一個(gè)到AVMetadataItem值的通道。這個(gè)類主要用于轉(zhuǎn)換簡單的字符串和數(shù)字值稽亏。代碼清單3-10給出了這個(gè)轉(zhuǎn)換器的具體實(shí)現(xiàn)壶冒。
代碼清單3-10 THDefaultMetadataConverter 的實(shí)現(xiàn)
#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值轉(zhuǎn)換為對應(yīng)的展示內(nèi)容時(shí),需要返回項(xiàng)的value屬性截歉。這對那些value屬性為簡單字符串或數(shù)字值的元數(shù)據(jù)項(xiàng)是適用的胖腾。當(dāng)由展示內(nèi)容轉(zhuǎn)換回AVMetadataItem值時(shí),首先需要?jiǎng)?chuàng)建一個(gè)原始AVMetadataltem的可變副本瘪松,這將返回一個(gè)AVMutableMetadataltem值胸嘁。當(dāng)創(chuàng)建新的元數(shù)據(jù)或修改已存在的元數(shù)據(jù)時(shí),需要使用AVMetadataItem的可變子類凉逛。它具有與父類一樣的基本接口性宏,但它重新將其中的一些只讀屬性定義為讀寫屬性。通過復(fù)制原始AVMetadataItem所創(chuàng)建的AVMutableMetadataItem是一個(gè)新實(shí)例状飞,實(shí)例中所有重要的屬性都和原始項(xiàng)的值一樣毫胜,只需要將它的value屬性 設(shè)置為傳入方法的值即可。
這只是我們所說的簡單轉(zhuǎn)換诬辈,在后面馬.上會(huì)接觸到很多復(fù)雜的場景酵使。下 面讓我們看一下如何轉(zhuǎn)換元數(shù)據(jù)項(xiàng)的Artwork。
3.6.5 轉(zhuǎn)換Artwork
媒體Artwork元數(shù)據(jù)會(huì)以多種不同的格式返回焙糟,比如唱片的封面和電影的海報(bào)等口渔。不過最終Artwork數(shù)據(jù)的字節(jié)都保存在一個(gè)NSData實(shí)例中,但是定位NSData實(shí)例有時(shí)候需要完成一些額外工作穿撮。代碼清單3-11給出了THArtworkMetadataConverter的實(shí)現(xiàn)缺脉。
代碼清單3-11THArtworkMetadataConverter的實(shí)現(xiàn)
#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返回一個(gè)NSData類型的值,我們就需要根據(jù)保存在該對象中的字節(jié)創(chuàng)建一個(gè)新的NSImage. NSImage 和Ullmage都可以提供初始化器悦穿,使我們可從NSData直接創(chuàng)建一個(gè)圖片實(shí)例攻礼。
(2)如果選中的對象是MP3,就需要對它進(jìn)行深入地挖掘以得到圖片字節(jié)栗柒。這種情況下的value屬性可能是一個(gè)NSDictionary礁扮,所以我們適當(dāng)?shù)剞D(zhuǎn)換值并檢索對應(yīng)的data鍵,這樣就可以得到保存圖片字節(jié)的NSData瞬沦。
(3)將展示內(nèi)容轉(zhuǎn)換回AVMutableMetadataltem實(shí)例的實(shí)現(xiàn)過程相當(dāng)簡單太伊,這是由于AVFoundation無法寫入ID3元數(shù)據(jù)」渥辏可認(rèn)為這個(gè)值就是以NSData格式保存的并向NSImage請求TIFFRepresentation僚焦。如果需要存儲(chǔ)PNG或JPG格式的圖片數(shù)據(jù),可以使用NSBitmapImageRep绣的、Ullmage方法或Quart框架將該數(shù)據(jù)轉(zhuǎn)換為所需的類型叠赐。
3.6.6 轉(zhuǎn)換注釋
獲取媒體項(xiàng)注釋的方法相當(dāng)直接欲账。如果處理的對象是MPEG-4或QuickTime內(nèi)容,可以獲取AVMetadataltem的stringValue屬性芭概。MP3同樣需要一些額外 方法來獲取注釋數(shù)據(jù)赛不,如代碼清單3-12所示。
代碼清單3-12 THCommentMetadataConverter 的實(shí)現(xiàn)
#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的注釋保存在一個(gè)定 義ID3 COMM幀的NSDictionary中(如果處理的是ID3v2.2,則為COM)惹苗。所有類型的值都保存在這個(gè)幀中殿较。比如,iTunes在這個(gè)幀中保存音頻標(biāo)準(zhǔn)化和無縫播放設(shè)置等桩蓉,意味著當(dāng)請求ID3元數(shù)據(jù)時(shí)需要接收多個(gè)COMM幀淋纲。包含實(shí)際注釋內(nèi)容的特定COMM幀被存儲(chǔ)在一個(gè)帶有空字 符串標(biāo)識符的幀中。找到需要的條目后院究,通過請求text鍵來檢索注釋洽瞬。
(3)將展示內(nèi)容轉(zhuǎn)換回AVMetadataItem很簡單,因?yàn)檫@個(gè)過程不涉及ID3數(shù)據(jù)业汰,所以只需復(fù)制原始AVMetadataltem并設(shè)置value屬性為傳入方法的值即可伙窃。
3.6.7 轉(zhuǎn)換音軌數(shù)據(jù)
音軌通常包含一首歌曲在整個(gè)唱片中的編號位置信息(比如:"12首歌曲中的第4首"這樣的信息)。MP3文件以一種簡單易懂的方式保存此信息样漆,但是M4A就有些復(fù)雜了为障,并且需要一些處理才能生成用于展示的內(nèi)容。代碼清單3-13給出了THTrackMetadataConverter類的實(shí)現(xiàn)放祟。
代碼清單3-13 THTrackMetadataConverter的實(shí)現(xiàn)
#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”字符串的格式返回鳍怨。比如在一個(gè)包含10首歌曲的唱片中的第8首歌曲,得到的返回字符串值應(yīng)該為“8/10”舞竿。 這種情況下京景,我們需要使用NSString的componentsSeparatedByString:方法將值分開,得到單獨(dú)值并將它們轉(zhuǎn)換為相應(yīng)的數(shù)值對象骗奖,將值打包為一個(gè)NSNumber實(shí)例。
(2)對于M4A文件醒串,這個(gè)值有些神秘执桌。如果在控制臺打印輸出元數(shù)據(jù)條目的值,所得到的結(jié)果是NSData芜赌,使用諸如0000008000a0000>的值仰挣。這是4個(gè)16位big endian數(shù)字?jǐn)?shù)組的十六進(jìn)制表現(xiàn)形式。數(shù)組中的第2個(gè)和第3個(gè)元素分別包含音軌編號和音軌計(jì)數(shù)值缠沈。
(3)如果音軌編號不等于0,使用CFSwapInt16BigToHost0函數(shù)進(jìn)行endian變換將其轉(zhuǎn)換為lttle endian格式膘壶,并將這個(gè)值打包為一個(gè)NSNumber實(shí)例错蝴。
(4)同樣,如果音軌計(jì)數(shù)值不等于0颓芭,則獲取該值并執(zhí)行endian轉(zhuǎn)換顷锰,最后將結(jié)果值打包為一個(gè)NSNumber實(shí)例。
(5)將得到的編號和計(jì)數(shù)值保存在一個(gè)NSDictionary實(shí)例中亡问,將它返回給調(diào)用者官紫。需要檢查每個(gè)值是否為nil,如果有的值為nil,則使用NSNull實(shí)例替換。
(6)將展示內(nèi)容轉(zhuǎn)換回AVMetadataItem所需的格式意味著需要將之前的步驟反過來州藕,即把在displayValueFromMetadataltem:方法中提取這些值的過程反過來束世。需要?jiǎng)?chuàng)建一個(gè)包含4個(gè)uint16_t值的數(shù)組來保存音軌編號和計(jì)數(shù)值。
(7)如果得到一個(gè)有效的音軌編號床玻,將字節(jié)轉(zhuǎn)換為big endian格式并將其保存在數(shù)組的第二個(gè)位置毁涉。
(8)如果得到一個(gè)有效的音軌總數(shù), 將字節(jié)轉(zhuǎn)換為big endian格式并將其保存在數(shù)組的第三個(gè)位置锈死。
(9)最后薪丁,將這些values數(shù)組打包為一個(gè)NSData實(shí)例,并將其設(shè)置為元數(shù)據(jù)項(xiàng)的值∠诰現(xiàn)在我們知道了處理音軌計(jì)數(shù)信息并不是那么簡單严嗜,不過現(xiàn)在我們已經(jīng)知道其中的奧妙了!
3.6.8 轉(zhuǎn)換唱片數(shù)據(jù)
唱片計(jì)數(shù)信息用于表示一首歌曲所在的CD是所有唱片中的第幾張唱片洲敢。大部分情況下這個(gè)值都是1/1漫玄,即只有一張唱片,不過如果這首歌曲所屬的唱片屬于一個(gè)唱片集合的一部分压彭,則這個(gè)值可能就會(huì)發(fā)生變化睦优。比如,如果你擁有Led Zeppelin的Complete StudioRecordings(很值得擁有的唱片)壮不,則Led Zeppelin IV這張唱片的唱片值就應(yīng)該是4/10.好消息是我們已經(jīng)知道如何獲取這個(gè)數(shù)據(jù)汗盘,因?yàn)樗c上一節(jié)中的音軌編號和音軌計(jì)數(shù)數(shù)據(jù)的保存方法幾乎一樣,所以它的類實(shí)現(xiàn)也十分相似询一。代碼清單3-14給出了THDiscMetadataConverter類的實(shí)現(xiàn)隐孽。
代碼清單3-14 THDiscMetadataConverter 的實(shí)現(xiàn)
#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個(gè)唱片集合中的第4張唱片健蕊,那么得到的字符串值就為4/10菱阵。這時(shí)我們使用NSString的components-SeparatedByString:方法將值分開,并將它們轉(zhuǎn)換為數(shù)值類型缩功,打包成一個(gè)NSNumber實(shí)例晴及。
(2) iTunes M4A文件的唱片信息保存在一個(gè)NSData中,NSData包 含3個(gè)16位big endian數(shù)字嫡锌。數(shù)組中的第2個(gè)和第3個(gè)元素分別保存唱片編號和唱片計(jì)數(shù)值虑稼。
(3)如果唱片編號不等于0琳钉,則獲取該值并使用CFSwapInt1 6BigToHost()函數(shù)執(zhí)行endian轉(zhuǎn)換,轉(zhuǎn)換為little endian蛛倦,并打包成一個(gè)NSNumber歌懒。
(4)同樣,如果唱片計(jì)數(shù)值不等于0胰蝠,則獲取該值并在字節(jié)上執(zhí)行endian轉(zhuǎn)換歼培,將得到的值打包為一個(gè)NSNumber。
(5)將所得到的編號和計(jì)數(shù)值保存在一個(gè)NSDictionary中茸塞, 將它返回到調(diào)用者躲庄。需要檢查每個(gè)值是否為nil,如果有的值為nil,則使用NSNull實(shí)例替換钾虐。
(6)將展示內(nèi)容轉(zhuǎn)換回AVMetadataltem所需的格式意味著需要將之前的步驟反過來噪窘,即把在displayValueFromMetadataItem:方法中提取這些值的過程反過來。需要?jiǎng)?chuàng)建一個(gè)包含3個(gè)uint16_t值的數(shù)組來保存唱片編號和唱片計(jì)數(shù)值效扫。
(7)如果得到一個(gè)有效的唱片編號倔监,就將字節(jié)轉(zhuǎn)換為big endian格式并將其保存在數(shù)組的第二個(gè)位置。
(8)如果得到一個(gè)有效的唱片計(jì)數(shù)值菌仁,將字節(jié)轉(zhuǎn)換為big endian格式并將其保存在數(shù)組的第三個(gè)位置浩习。
(9)最后,將這些values數(shù)組打包為一個(gè)NSData實(shí)例济丘,并將其設(shè)置為元數(shù)據(jù)項(xiàng)的值谱秽。
3.6.9 轉(zhuǎn)換風(fēng)格數(shù)據(jù)
處理風(fēng)格(genre)數(shù)據(jù)具有一定挑戰(zhàn)。 困難并非來自于數(shù)據(jù)本身的復(fù)雜性摹迷,而是風(fēng)格具有多種不同的格式疟赊。比如預(yù)定義風(fēng)格、用戶風(fēng)格峡碉、風(fēng)格ID近哟、音樂風(fēng)格、電影風(fēng)格等鲫寄。另一個(gè) 挑戰(zhàn)在于風(fēng)格信息的保存不止一種方法吉执,即使對于文件類型也同樣如此。我們看一下相關(guān)的基礎(chǔ)知識塔拳。
對數(shù)字音頻使用的標(biāo)準(zhǔn)風(fēng)格分類最初來自于MP3鼠证。ID3規(guī)范定義了80個(gè)默認(rèn)的風(fēng)格類型及另外46個(gè)WinAmp擴(kuò)展,一共126 個(gè)風(fēng)格靠抑。你可能還會(huì)找到各種工具所支持的特有風(fēng)格類型,不過這些都不屬于正式格式适掰。
由于在這一點(diǎn)上MP3的主導(dǎo)地位明顯颂碧,iTunes沒有 創(chuàng)建一套自己的音樂風(fēng)格劃分荠列,而是基本遵循了ID3的風(fēng)格分類,不過做了點(diǎn)小變化载城。iTunes音 樂風(fēng)格的標(biāo)號比相應(yīng)的ID3標(biāo)識符大1肌似。比如表3-1給出了ID3集合中的前5種風(fēng)格以及相應(yīng)的iTunes值。
雖然iTunes使用了ID3集合中的預(yù)定義音樂風(fēng)格類型诉瓦,不過iTunes對電視川队、電影和有聲讀物等定義了自己的風(fēng)格集〔窃瑁可在Apple' s Genre IDs Appendix3中找到完整的iTunes風(fēng)格列表固额。
要簡化風(fēng)格數(shù)據(jù)的處理(節(jié)省代碼的輸入),示例程序包含一個(gè)名為THGenre的類煞聪,它定義了標(biāo)準(zhǔn)的音樂風(fēng)格和電視節(jié)目的風(fēng)格斗躏。可以對應(yīng)用程序中的音頻及視頻內(nèi)容使用這些風(fēng)格類型昔脯。
讓我們學(xué)習(xí)一下有關(guān)風(fēng)格的具體細(xì)節(jié)啄糙。代碼清單3-15給出了THGenreMetadata- Converter類的實(shí)現(xiàn)。
代碼清單3-15 THGenreMetadataConverter 的實(shí)現(xiàn)
#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)有些格式將風(fēng)格數(shù)據(jù)保存為一個(gè)NSString云稚。有時(shí)該字符串的內(nèi)容就是實(shí)際風(fēng)格的名字隧饼,有時(shí)是風(fēng)格在預(yù)定義風(fēng)格列表中對應(yīng)的索引值。
(2)如果元數(shù)據(jù)項(xiàng)來自于ID 3鍵空間并且這個(gè)值可以強(qiáng)制轉(zhuǎn)換為NSNumber静陈,在使用ID3v2.4的情況下燕雁,可以取到該值的無符號整型值并用它作為索引來查找正確的THGenre實(shí)例。
(3) ID3的另一個(gè)變化是使用風(fēng)格實(shí)際的名稱進(jìn)行保存窿给,比如Jazz贵白、Rock、Country及類似的情況崩泡。如果值為風(fēng)格的名稱禁荒,可以使用這個(gè)名稱找到相應(yīng)的THGenre實(shí)例。
(4)對于其他以NSString類型保存的風(fēng)格信息角撞,可以將它們視為QuickTime電影或MPEG-4視頻文件呛伴,這兩種情況都直接保存風(fēng)格的實(shí)際名稱≮怂可以從THGenre類查找到相應(yīng)的視頻風(fēng)格热康。
(5)當(dāng)使用其中一個(gè)預(yù)定義風(fēng)格時(shí),iTunes M4A音頻會(huì)返回一個(gè)保存在NSData中的16位bigendian數(shù)字劣领。取到該值并將其轉(zhuǎn)換為little endian格式姐军, 之后調(diào)用iTunesGenreWithIndex:方法返回相應(yīng)的風(fēng)格實(shí)例。
(6)當(dāng)轉(zhuǎn)換回AVMetadataltem格式時(shí),如果原始AVMetadataItem中保存的值是一個(gè)字符串奕锌,可以簡單地將輸入值賦給條目的value屬性著觉。
(7)如果原始AVMetadataltem保存的值是一個(gè)NSData,通過將字節(jié)轉(zhuǎn)換回big endian格式,并將結(jié)果包裝為一個(gè)NSData值惊暴,將展示的內(nèi)容轉(zhuǎn)換回相應(yīng)的格式饼丘。另外注意THGenre的索引屬性基于0,因此為調(diào)整回iTunes索引辽话,需要加1肄鸽。
恭喜,有關(guān)轉(zhuǎn)換的方法我們?nèi)繉W(xué)習(xí)完了∮推。現(xiàn)在可以運(yùn)行程序典徘,從列表中選擇一個(gè)條目并觀察它在用戶界面中展示的內(nèi)容。我們還剩下一個(gè)功能需要實(shí)現(xiàn)村砂,就是讓用戶對這些變更進(jìn)行保存烂斋。讓我們回到上面的THMetadata類并完成其實(shí)現(xiàn)。
3.6.10 完成 THMetadata
我們還需要實(shí)現(xiàn)THMetadata的metadataltems方法础废。這個(gè)方法會(huì)獲取所有當(dāng)前保存的展示內(nèi)容汛骂,并使用我們上面幾節(jié)中創(chuàng)建的轉(zhuǎn)換類將它們轉(zhuǎn)換回AVMetadataltem格式。代碼清單3-16給出了這個(gè)方法的實(shí)現(xiàn)评腺。
代碼清單3-16 metadataltems 方法的實(shí)現(xiàn)
- (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)創(chuàng)建一個(gè)NSMutableArray實(shí)例帘瞭, 保存在本方法創(chuàng)建的元數(shù)據(jù)集合中。
(2)音軌編號/計(jì)數(shù)及唱片編碼/計(jì)數(shù)都需要額外的處理來將它們的值轉(zhuǎn)換回AVMetadatatem格式蒿讥,所以具體處理邏輯應(yīng)該分解為不同方法蝶念,如應(yīng)用程序中的編號3所示。
(3)將成對的number和count封裝到一個(gè)NSDictionary,并將原始AVMetadataltem傳遞到轉(zhuǎn)換方法中芋绸。
(4)從轉(zhuǎn)換器獲取元數(shù)據(jù)項(xiàng)媒殉,如果返回一個(gè)有效的元數(shù)據(jù)實(shí)例,則將它添加到items數(shù)組中摔敛。
(5)創(chuàng)建一個(gè)包含不同顯示值的內(nèi)部metadata字典的副本廷蓉,將音軌和唱片編號鍵從數(shù)組中刪除,因?yàn)檫@些都是已經(jīng)處理過的马昙。遍歷剩下的鍵值來創(chuàng)建相應(yīng)的元數(shù)據(jù)項(xiàng)桃犬。
(6)使用鍵/值編碼方法valueForKey:查找當(dāng)前鍵對應(yīng)的值。
(7)最后從當(dāng)前轉(zhuǎn)換方法中檢索AVMetadataltem實(shí)例行楞,如果返回一個(gè)有效實(shí)例攒暇,則將它添加到items集合中。
THMetadata類現(xiàn)在就全部完成了子房,我們也基本上完成了MetaManager應(yīng)用程序形用。唯一剩下的任務(wù)就是實(shí)現(xiàn)THMedialtem上的saveWithCompletionHandler:方法就轧。下面就開始實(shí)現(xiàn)它吧。
3.7 保存元數(shù)據(jù)
我們已經(jīng)創(chuàng)建了THMetadata類及相關(guān)的轉(zhuǎn)換對象尾序,現(xiàn)在就可以讀取元數(shù)據(jù)并將用戶輸入的內(nèi)容轉(zhuǎn)換回AVMetadataItem實(shí)例钓丰。不過還有一個(gè)重要的問題沒有解決躯砰,由于AVAsset是一個(gè)不可變類每币,我們?nèi)绾尾拍軕?yīng)用這些元數(shù)據(jù)的改變呢?答案是我們不直接修改AVAsset,而是使用一個(gè)名為AVAssetExportSession的類來導(dǎo)出一個(gè)新的資源副本以及元數(shù)據(jù)改動(dòng)琢歇。在具體學(xué)習(xí)這個(gè)方法的實(shí)現(xiàn)之前兰怠,先看一下如何配置和使用AVAssetExportSession。
應(yīng)用AVAssetExportSession
AVAssetExportSession用于將AVAsset內(nèi)容根據(jù)導(dǎo)出預(yù)設(shè)條件進(jìn)行轉(zhuǎn)碼李茫,并將導(dǎo)出資源寫到磁盤中揭保。AVAssetExportSession提供了多個(gè)功能來實(shí)現(xiàn)將一種格式轉(zhuǎn)換為另一種格式、修訂資源的內(nèi)容魄宏、修改資源的音頻和視頻行為秸侣,當(dāng)然還有我們最感興趣的功能,即寫入新的元數(shù)據(jù)宠互。
創(chuàng)建一個(gè)AVAssetExportSession實(shí)例需要提供資源和導(dǎo)出預(yù)設(shè)味榛。導(dǎo)出預(yù)設(shè)用于確定導(dǎo)出內(nèi)容的質(zhì)量、大小等屬性予跌。創(chuàng)建一個(gè)導(dǎo)出會(huì)話后搏色,并且需要指定一個(gè)outputURL用于聲明導(dǎo)出內(nèi)容將要寫入的地址,并且需要給出一個(gè)outputFileType值來表示將要寫入的導(dǎo)出格式券册。最后調(diào)用exportAsynchronouslyWithCompletionHandler:方法開始導(dǎo)出過程频轿。
在此并不對AVAssetExportSession的一些抽象概念進(jìn)行講述,而是直接來看saveWithCompletetionHandler:方法的實(shí)現(xiàn)烁焙。代碼清單3-17給出了這個(gè)方法的具體實(shí)現(xiàn)航邢。
代碼清單3-17 THMedialtem 的saveWithCompletionHandler:的實(shí)現(xiàn)
- (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預(yù)設(shè)值創(chuàng)建一個(gè)AVAssetExportSession。AVAssetExportSession具有多個(gè)預(yù)設(shè)選項(xiàng)骄蝇,不過AVAssetExportPresetPassthrough預(yù)設(shè)值可以讓我們在不需要重新對媒體編碼的前提下實(shí)現(xiàn)寫入元數(shù)據(jù)的功能膳殷。在將實(shí)際媒體內(nèi)容進(jìn)行轉(zhuǎn)碼時(shí),"passthrough"導(dǎo)出過程的時(shí)間很短乞榨。
(2)為臨時(shí)文件創(chuàng)建一個(gè)寫入磁盤的URL秽之,該URL基 于當(dāng)前url屬性值,只不過在文件名后面附上temp字樣吃既。
(3)取得THMetadata實(shí)例的metadatalItems屬性考榨,這一步會(huì)返回一個(gè)包含用戶接口值狀態(tài)的AVMutableMetadataItem實(shí)例數(shù)組。
(4)在導(dǎo)出會(huì)話的completionhandler中鹦倚,首先要判斷導(dǎo)出狀態(tài)河质。如果導(dǎo)出狀態(tài)為AVAssetExportSessionCompleted,意味著導(dǎo)出成功,需要?jiǎng)h除舊資源掀鹅,改用新導(dǎo)出的版本散休。本例中我們做的有些快而忽略了對于文件操作的錯(cuò)誤處理程序。在實(shí)際的產(chǎn)品中需要對各種可能出現(xiàn)的問題進(jìn)行處理乐尊。
(5)最后調(diào)用私有的reset方法來重置媒體項(xiàng)的prepared狀態(tài)并重新初始化基礎(chǔ)資源戚丸。
MetaManager應(yīng)用程序終于全部完成了!現(xiàn)在可以運(yùn)行該程序并對已有的元數(shù)據(jù)項(xiàng)進(jìn)行修改,并且還可以將這些改動(dòng)保存到磁盤中扔嵌。
注意:
AVAssetExportPresetPassthrough預(yù)設(shè)值在一些特定場景中是有用的限府,并且對于演示類應(yīng)用程序非常適合。不過要注意它有一定的限制痢缎, 它確實(shí)允許修改MPEG-4和QuickTime容器中存在的元數(shù)據(jù)信息胁勺,不過它不可以添加新的元數(shù)據(jù)。添加元數(shù)據(jù)的唯一方 法是使用轉(zhuǎn)碼預(yù)設(shè)值独旷。此外署穗,它不能用于修改ID3 標(biāo)簽∏锻荩框架不支持寫入MP3數(shù)據(jù)案疲,這也是為什么在演示程序中MP3文件是只讀類型的原因。
3.8 小結(jié)
本章我們已經(jīng)介紹了很多基礎(chǔ)知識!本章的內(nèi)容很多咱台,不過最重要的是我們?yōu)橹髢?nèi)容的學(xué)習(xí)做好了充分準(zhǔn)備络拌。主要學(xué)習(xí)了AVAsset和AVAssetTrack類并理解了使用AVAsynchronous-KeyValueLoading協(xié)議異步獲取屬性的重要性。深入研究AV Foundation基于AVMetadataItem和AVMutableMetadataItem類的元數(shù)據(jù)功能回溺,并討論了更底層的數(shù)據(jù)操作方法春贸。最后,我們第一次接觸AVAssetExportSession類 并將它很好地用在MetaManager應(yīng)用程序中遗遵。這絕對不是我們最后一-次使用這些類萍恕, 并且隨著我們學(xué)習(xí)的深入還會(huì)涉及這些類在其他方面的應(yīng)用。
現(xiàn)在我們已經(jīng)為這趟AV Foundation學(xué)習(xí)之旅做好了充分準(zhǔn)備车要。
3.9 挑戰(zhàn)
本章對如何讀取或?qū)懭胱畛R姷脑獢?shù)據(jù)進(jìn)行了詳細(xì)介紹允粤,但這還遠(yuǎn)遠(yuǎn)不夠。復(fù)制媒體庫中的一首歌曲或一部電影并在iTunes中對它進(jìn)行完整的標(biāo)注翼岁。使用十六進(jìn)制編輯器查看數(shù)據(jù)是如何保存及保存在什么位置类垫。當(dāng)你開始接觸更高級的AV Foundation用例時(shí),掌握不同容器格式中的數(shù)據(jù)存儲(chǔ)方式會(huì)大有幫助琅坡。