第三章 資源和中繼資料
1. 資源簡介
AV Foundation 把所有的代碼設計圍繞資源(assert)進行,AVAssert 是 AV Foundation 設計的核心。
AVAssert 不需要考慮的兩個重要範疇:第一、它提供了對基本媒體格式的層抽象,這意味着無論是處理 MPEG-4 視訊還是 MP3 音頻,對你而言面對的隻有資源這個概念。第二、不用我們管理資源的位置資訊,不管是本地 URL 還是遠端伺服器上的一個音頻流、視訊流的 URL。
AVAssert 本身并不是媒體資源,但是它可以作為時基媒體的容器,它由一個或多個帶有描述自身中繼資料的媒體組成。我們使用 AVAssetTrack 類代表儲存在資源中的統一類型媒體,并對每個資源建立相應的模型。AVAssetTrack 最常見的形态就是音頻和視訊流,但是它還可以表示文本、副标題或隐藏字幕等媒體類型。
2. 資源建立
為一個媒體資源建立 AVAsset 對象時,可以通過 URL 對它進行初始化來實作,可以是本地 URL,也可以是遠端資源的 URL
NSURL *url = [NSURL URLWithString:@""];
AVAsset *asset = [AVAsset assetWithURL:url];
AVAsset 是一個抽象類,意味着不能直接被執行個體化。當建立執行個體時,實際上是建立了它子類的一個執行個體,子類名為 AVURLAsset。有時我們會直接使用這個類,因為它允許通過傳遞選項字典來精細調整資源的建立方式。比如,如果建立一個用在音頻或視訊編輯場景中的資源,希望傳遞一個選項來告訴程式提供更精确的時長和計時資訊:
NSURL *url = [NSURL URLWithString:@""];
NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey : @YES};
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:options];
3. 異步載入
AVAsset 具有多種方法和屬性,可以提供有關資源的資訊,比如時長、建立日期和中繼資料等。AVAsset 使用了一種高效的設計方法,延遲載入資源屬性,直到請求時才載入,這樣就可以快速建立資源。不過屬性的通路是同步發生的,如果正在請求的屬性沒有預先載入,程式就會阻塞,是以開發者應該使用異步來查詢資源的屬性。
NSURL *url = [[NSBundle mainBundle] URLForResource:@"人渣的本願真人版01" withExtension:@"mp4"];
AVAsset *asset = [AVAsset assetWithURL:url];
[asset loadValuesAsynchronouslyForKeys:@[@"duration"] completionHandler:^{
NSError *error;
AVKeyValueStatus status = [asset statusOfValueForKey:@"duration" error:&error];
switch (status) {
case AVKeyValueStatusLoaded: // 加載完成
{
dispatch_async(dispatch_get_main_queue(), ^{
CMTimeShow(asset.duration);
});
break;
}
case AVKeyValueStatusFailed:
break;
case AVKeyValueStatusCancelled:
break;
default:
break;
}
}];
// 取消加載
[asset cancelLoading];
4. 媒體中繼資料
4.1 中繼資料格式
我們在 Apple 環境下遇到的媒體類型主要有四種:
- QuickTime( .mov )
- MPEG-4 video( .mp4 和 .m4v )
- MPEG-4 audio( .m4a )
- MPEG-Layer III audio( .mp3 )
QuickTime( .mov )
QuickTime 是由蘋果公司開發的一種功能強大、跨平台的媒體架構。了解 QuickTime 格式的一個好辦法就是在十六進制編譯器中打開一個 .mov 格式的檔案,常見的十六進制編譯器有 Hex Fiend 或 Synalyze It! Pro。更好的方法是借助 Apple Developer Center 中找到 Atom Inspector 工具,它可以将 atom 結構以 NSOutlineView 方式呈現,是以可以對 atom 之間的繼承關系等資訊有比較清晰的了解。
上圖為 QuickTime 結構的簡化示意圖,該檔案最小限度的包含了三個進階 atom,分别是用于描述檔案類型和相容類型的 ftyp,包含實際音視訊媒體的 mdat 以及非常重要的 moov atom,它對媒體資源的所有相關細節做了完整描述,包括可呈現的中繼資料。
MPEG-4 video( .mp4 和 .m4v )和 MPEG-4 audio( .m4a )
mp4 直接派生于 QuickTime 檔案格式,這就意味着它們的結構是類似的。實際上,我們經常會發現,能夠解析一種檔案類型的工具也可以處理其他檔案類型,就像 QuickTime 檔案一樣,mp4 檔案也由 atom 資料結構組成。
mp4 是對 MPEG-4 媒體的标準擴充,m4v、m4a、m4p、m4b 這些變體都使用 MPEG-4 容器格式,但是包含了附加的拓展功能。m4v 檔案是帶有針對 FairPlay 加密和 AC3-audio 擴充的 MPEG-4 視訊格式,如果不涉及這些 mp4 和 m4v 僅僅是擴充名不同而已。m4a 針對音頻,讓使用者知道該檔案隻帶有音頻資源。m4p 是蘋果較舊的 iTunes 音頻格式,使用其 FairPlay 擴充。m4b 用于有聲讀物,通常包含章節标簽和書簽功能。
MPEG-Layer III audio( .mp3 )
mp3 檔案和上面兩種有顯著差別,mp3 檔案不使用容器格式,而使用編碼音頻資料,包含的可選中繼資料的結構塊通常位于檔案開頭。mp3 檔案使用一種稱為 ID3v2 格式來儲存關于音頻内容的描述資訊,包含的資料有歌曲演唱者、所屬唱片和音樂風格等。
AV Foundation 支援讀取 ID3v2 标簽的所有版本,但是不支援寫入,是以 AV Foundation 無法支援對 mp3 進行編碼。
4.2 擷取中繼資料
AVAsset 和 AVAssetTrack 都可以實作查詢相關中繼資料的功能。一般使用 AVAsset 提供的中繼資料,當涉及擷取曲目一級中繼資料等情況時會使用 AVAssestTrack。讀取具體資源中繼資料的接口由 AVMetadataItem 的類提供。
NSURL *audioURL = [[NSBundle mainBundle] URLForResource:@"一次就好" withExtension:@"mp3"];
AVAsset *asset = [AVAsset assetWithURL:audioURL];
[asset loadValuesAsynchronouslyForKeys:@[@"availableMetadataFormats"] completionHandler:^{
AVKeyValueStatus commonStatus = [asset statusOfValueForKey:@"availableMetadataFormats" error:nil];
if (commonStatus == AVKeyValueStatusLoaded) {
NSMutableArray *metaData = [NSMutableArray array];
for (NSString *format in asset.availableMetadataFormats) {
[metaData addObjectsFromArray:[asset metadataForFormat:format]];
}
// 歌曲名稱
AVMetadataItem *titleItem = [AVMetadataItem metadataItemsFromArray:metaData withKey:AVMetadataCommonKeyTitle keySpace:AVMetadataKeySpaceCommon].firstObject;
// 演唱者
AVMetadataItem *artistItem = [AVMetadataItem metadataItemsFromArray:metaData withKey:AVMetadataCommonKeyArtist keySpace:AVMetadataKeySpaceCommon].firstObject;
// 專輯名稱
AVMetadataItem *albumItem = [AVMetadataItem metadataItemsFromArray:metaData withKey:AVMetadataCommonKeyAlbumName keySpace:AVMetadataKeySpaceCommon].firstObject;
NSLog(@"%@ : %@", titleItem.key, titleItem.value); // TIT2 : 一次就好
NSLog(@"%@ : %@", artistItem.key, artistItem.value); // TPE1 : 沈騰
NSLog(@"%@ : %@", albumItem.key, albumItem.value); // TALB : 夏洛特煩惱 電影原聲帶
}
}];
上面我們使用的是鍵和鍵空間來擷取中繼資料,iOS 8 以後還引進了辨別符擷取中繼資料的方法:
// 歌曲名稱
AVMetadataItem *titleItem = [AVMetadataItem metadataItemsFromArray:metaData filteredByIdentifier:AVMetadataCommonIdentifierTitle].firstObject;
// 演唱者
AVMetadataItem *artistItem = [AVMetadataItem metadataItemsFromArray:metaData filteredByIdentifier:AVMetadataCommonIdentifierArtist].firstObject;
// 專輯名稱
AVMetadataItem *albumItem = [AVMetadataItem metadataItemsFromArray:metaData filteredByIdentifier:AVMetadataCommonIdentifierAlbumName].firstObject;
注意
有的時候直接列印 key 為一串數字,是以我們建立一個 AVMetadataItem 的分類,将數字轉換成字元串
NSLog(@"%@ : %@", titleItem.keyString, titleItem.value);
NSLog(@"%@ : %@", artistItem.keyString, artistItem.value);
NSLog(@"%@ : %@", albumItem.keyString, albumItem.value);
#import "AVMetadataItem+Extend.h"
@implementation AVMetadataItem (Extend)
- (NSString *)keyString
{
// 如果 key 是一個字元串,則原樣傳回
if ([self.key isKindOfClass:[NSString class]]) {
return (NSString *)self.key;
}
else if ([self.key isKindOfClass:[NSNumber class]]) {
UInt32 keyValue = [(NSNumber *)self.key unsignedIntValue];
// 大部分情況下,key 是一個 4 字元代碼,比如 ©gen 或 TRAK,不過對于 mp3 檔案,鍵值隻有 3 個字元的長度
size_t length = sizeof(UInt32);
if ((keyValue >> ) == ) --length;
if ((keyValue >> ) == ) --length;
if ((keyValue >> ) == ) --length;
if ((keyValue >> ) == ) --length;
long address = (unsigned long)&keyValue;
address += (sizeof(UInt32) - length);
// 由于數字是 big endian 格式,是以使用 CFSwapInt32BigToHost() 函數将其轉換為符合主 CPU 順序的 little endian 格式
keyValue = CFSwapInt32BigToHost(keyValue);
// 建立一個字元數組,并使用 strncpy 函數将字元位元組填充到該數組中
char cstring[length];
strncpy(cstring, (char *) address, length);
cstring[length] = '\0';
// 大量 QuickTime 使用者資料和 iTunes key 的字首都帶有一個 © 符号
// 不過 AVMetadataFormat.h 中定義 key 所使用的字首符号為 @
// 是以為了進行 key 常量字元串比較,需要先将 © 替換為 @
if (cstring[] == '\xA9') {
cstring[] = '@';
}
return [NSString stringWithCString:(char *) cstring
encoding:NSUTF8StringEncoding];
}
else {
return @"<<unknown>>";
}
}
@end
總結
找不到相關資料,根據我的了解,availableMetadataFormats、metadata、commonMetadata 三者的關系如下
- availableMetadataFormats:
獲得所有 keys,周遊 keys 根據asset.availableMetadataFormats
方法擷取所有中繼資料 AVMetadataItem[asset metadataForFormat:key]
- metadata:通過
方法直接可以得到所有中繼資料,據測試和 availableMetadataFormats 獲得到的中繼資料相同asset.metadata
- commonMetadata:通過
方法直接可以得到常用的中繼資料,但是擷取的不全asset.commonMetadata
是以我認為最簡單擷取中繼資料方法如下,如果有錯誤,日後更正:
NSURL *audioURL = [[NSBundle mainBundle] URLForResource:@"一次就好" withExtension:@"mp3"];
AVAsset *asset = [AVAsset assetWithURL:audioURL];
[asset loadValuesAsynchronouslyForKeys:@[@"metadata"] completionHandler:^{
AVKeyValueStatus commonStatus = [asset statusOfValueForKey:@"metadata" error:nil];
if (commonStatus == AVKeyValueStatusLoaded) {
// 歌曲名稱
AVMetadataItem *titleItem = [AVMetadataItem metadataItemsFromArray:asset.metadata filteredByIdentifier:AVMetadataCommonIdentifierTitle].firstObject;
// 演唱者
AVMetadataItem *artistItem = [AVMetadataItem metadataItemsFromArray:asset.metadata filteredByIdentifier:AVMetadataCommonIdentifierArtist].firstObject;
// 專輯名稱
AVMetadataItem *albumItem = [AVMetadataItem metadataItemsFromArray:asset.metadata filteredByIdentifier:AVMetadataCommonIdentifierAlbumName].firstObject;
NSLog(@"%@ : %@", titleItem.keyString, titleItem.value);
NSLog(@"%@ : %@", artistItem.keyString, artistItem.value);
NSLog(@"%@ : %@", albumItem.keyString, albumItem.value);
}
}];
4.3 編輯中繼資料
AVAssetExportSession 用于将 AVAsset 内容根據導出預設條件進行轉碼,并将導出資源寫到磁盤中。
AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:_asset presetName:AVAssetExportPresetPassthrough];
session.outputURL = [self tempURL];
session.outputFileType = [self fileType];
session.metadata = [self.metadata metadataItems];
[session exportAsynchronouslyWithCompletionHandler:^{
if (session.status == AVAssetExportSessionStatusCompleted) {
[[NSFileManager defaultManager] removeItemAtURL:_url error:nil];
[[NSFileManager defaultManager] moveItemAtURL:session.outputURL toURL:_url error:nil];
}
}];
注意:AVAssetExportPresetPassthrough 可以修改 MPEG-4 和 QuickTime 容器中存在的中繼資料資訊,不過它不能添加新的中繼資料。添加中繼資料唯一方法是使用轉碼預設值。此外,它不能修改 ID3 标簽,不支援寫入 MP3 資料。
5. 封裝架構
根據 Learning-AV-Foundation Chapter 3 自行封裝了一個中繼資料擷取、編輯的架構,下載下傳位址:https://github.com/Mayan29/MYAVFoundation