通過使用資源對象,并通過構(gòu)建它的元數(shù)據(jù)編輯來了解AVFoundation的特性.
1. 媒體資源AVAsset
AVFoundation 中最重要的就是AVAsset
這個(gè)抽象類,它定義了媒體資源混合呈現(xiàn)的方式,將媒體資源的靜態(tài)屬性模塊化一個(gè)整體.比如標(biāo)題,時(shí)長,元數(shù)據(jù)等.
我們使用AVAssetTrack
可以從AVAsset資源容器中拿到軌道信息和上面的內(nèi)容。
AVAsset
主要是抽象化了基本媒體資源的格式 , 跟資源文件一對一映射, 因此我們就不需考慮不種格式,而使用單一統(tǒng)一的方式處理.
- 創(chuàng)建資源
例: 從照片庫中視頻文件創(chuàng)建一個(gè)AVAsset資源
PHPhotoLibrary *libary = [PHPhotoLibrary sharedPhotoLibrary];
[libary performChanges:^{
// 獲取視頻類資源.可以用類似 NSArray 的接口來訪問結(jié)果內(nèi)的集合。它會按需動(dòng)態(tài)加載內(nèi)容并且緩存最近請求的內(nèi)容
PHFetchResult *result = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeVideo options:nil];
// 獲取第一個(gè)視頻.
[result enumerateObjectsAtIndexes:[NSIndexSet indexSetWithIndex:0] options:0 usingBlock:^(PHAsset * _Nonnull phAsset, NSUInteger idx, BOOL * _Nonnull stop) {
if (phAsset) {
__weak __typeof(self) weakSelf = self;
// PHAsset轉(zhuǎn)換 AVAsset
[[PHImageManager defaultManager] requestAVAssetForVideo:phAsset options:nil resultHandler:^(AVAsset * avAsset, AVAudioMix * audioMix, NSDictionary * info) {
// dispatch_async(dispatch_get_main_queue(), ^{ // 按需要添加
[weakSelf doSomethingWithAVAsset:avAsset];
//});
}];
}
}];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
if (!success) {
NSLog(@"PHPhotoLibrary error:%@",error);
}
}];
AVAsset資源通常使用
assetWithURL:
方法來獲取,但是iOS使用新的了Photos照片庫,沒有了url的定義,所以這里用PHImageManager來進(jìn)行轉(zhuǎn)換.
-
訪問資源屬性:AVAsset使用一種高效的設(shè)計(jì)模式,即延遲加載資源的屬性,只有當(dāng)使用時(shí)候才加載.這樣可以快速創(chuàng)建資源 . 但有時(shí)資源屬性的訪問是同步發(fā)生的,而正在請求的屬性沒有預(yù)先載入時(shí),就會造成程序阻塞. 所以**要使用異步方式查詢資源的屬性. **
AVAsset和AVAssetTrack通過AVAsynchronousKeyValueLoading協(xié)議
實(shí)現(xiàn)異步查詢屬性功能.
- (void)doSomethingWithAVAsset:(AVAsset*) asset {
NSArray *keys = @[@"tracks"];
// 異步加載資源的tracks屬性
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
// 先捕獲 tracks 屬性狀態(tài),再據(jù)此做處理
NSError *error = nil;
AVKeyValueStatus status = [asset statusOfValueForKey:@"tracks" error:&error];
switch (status) {
case AVKeyValueStatusLoaded:
break;
case AVKeyValueStatusFailed:
break;
default:
;
}
}];
}
如果想訪問資源的多個(gè)屬性時(shí), 雖然
loadValuesAsynchronouslyForKeys:
只會調(diào)用一次,但是每個(gè)屬性的狀態(tài)不一定一致,所以要分開判斷.
1.1 元數(shù)據(jù)
每個(gè)媒體資源類型都具有唯一的格式,因此開發(fā)者通常需要對相應(yīng)格式的底層讀寫操作有所了解. 而AVFoundation 框架中的媒體格式都嵌入到了描述其內(nèi)容的元數(shù)據(jù)中. 所以我們可以使用一套統(tǒng)一的方法來直接處理元數(shù)據(jù) .
**元數(shù)據(jù)格式: ** Apple環(huán)境下媒體類型主要是: QuickTime(mov)都办、MPEG-4 video(mp4,mpv) 和MPEG-4 audio(m4a)、MPEG-Layer III audio (mp3);
- QuickTime
QuickTime定義了.mov文件的內(nèi)部結(jié)構(gòu). 通常一部QuickTime電影會包含兩種類型元數(shù)據(jù):標(biāo)準(zhǔn)元數(shù)據(jù)(/moov/meta/ilst/中)和用戶元數(shù)據(jù)(/moov/udta/中).顧名思義: 用戶元數(shù)據(jù)就是包括演唱者,版權(quán)信息以及任何對應(yīng)用程序有幫助的額外信息. - MPEG-4 音頻和視頻
MP4文件直接派生于QuickTime文件格式,所以很多解析QuickTime文件的工具也能解析MPEG-4.它的元數(shù)據(jù)保存在/moov/udta/meta/ilst中
該類型文件還有一些變化的擴(kuò)展名,如.m4v,.m4a,.m4p和.m4b. 它們都使用MPEG-4容器格式,但包含了一些附加的擴(kuò)展功能;m4v是帶有蘋果公司針對FairPlay加密和AC3-audio擴(kuò)展的格式.m4a專門針對音頻,告訴使用者此文件只含音頻資源.m4b用于有聲讀物,通常包含章節(jié)標(biāo)簽痊末、書簽等功能.
- MP3
MP3文件與上面兩種有顯著區(qū)別,MP3不適用容器格式,而使用編碼音頻數(shù)據(jù),使用ID3v2格式來保存音頻的描述信息,包含演唱者,唱片公司等信息.AVFoundation只支持MP3的解碼讀取.
1.2 獲取元數(shù)據(jù)
AVAsset和AVAssetTrack都可以實(shí)現(xiàn)查詢相關(guān)元數(shù)據(jù)的功能,通過AVMetadataItem
類的接口來訪問元數(shù)據(jù).大部分情況我們使用AVAsset,除非你要獲取低層級元數(shù)據(jù)的信息. 另外AVFoundation使用鍵空間(keys space)將相關(guān)鍵組合在一起.以方便實(shí)現(xiàn)對AVMetadataItem
實(shí)例集合分類篩選.每個(gè)資源至少包含兩個(gè)鍵空間: commonMetadata
和availableMetadataFormats
. 前者用來定義所有支持的媒體類型的鍵,包括:曲名,歌手,插圖等常見元素. 后者用來包含用來定義元數(shù)據(jù)格式的NSString對象和相關(guān)元數(shù)據(jù)信息的NSArray.
#define COMMON_META_KEY @"commonMetadata"
#define AVAILABLE_META_KEY @"availableMetadataFormats"
// 兩種鍵空間
NSArray *keys = @[COMMON_META_KEY, AVAILABLE_META_KEY];
// ios支持的元數(shù)據(jù)格式
NSArray *acceptedFormats = @[
AVMetadataFormatQuickTimeMetadata,
AVMetadataFormatiTunesMetadata,
AVMetadataFormatID3Metadata
];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
// 1. 判斷資源屬性加載狀態(tài).
AVKeyValueStatus commonStatus =
[self.asset statusOfValueForKey:COMMON_META_KEY error:nil];
AVKeyValueStatus formatsStatus =
[self.asset statusOfValueForKey:AVAILABLE_META_KEY error:nil];
self.prepared = (commonStatus == AVKeyValueStatusLoaded) &&
(formatsStatus == AVKeyValueStatusLoaded);
// 2. 獲取各鍵空間的元數(shù)據(jù)
if (self.prepared) {
for (AVMetadataItem *item in self.asset.commonMetadata) {
//NSLog(@"%@: %@", item.keyString, item.value);
}
for (NSString *format in self.asset.availableMetadataFormats) { // 查詢此資源中所包含的所有元數(shù)據(jù)格式
if ([acceptedFormats containsObject:format]) { // 是否支持此格式.
NSArray *items = [self.asset metadataForFormat:format]; // 訪問對應(yīng)格式元數(shù)據(jù).
for (AVMetadataItem *item in items) {
//NSLog(@"%@: %@", item.keyString, item.value);
}
}
}
}
}];
}
AVMetadataItem 最基本形式是一個(gè)鍵值對的容器.可以通過它查詢key或commonKey來訪問元數(shù)據(jù). 但是它的key屬性是以id<NSObject,NSCopying>
值的形式定義的,雖然可以保存NSString,但通過上面的打印可以知道Key只是無意義的整數(shù).所以我們最好添加一個(gè)AVMetadataItem分類方法,用來轉(zhuǎn)換獲取key的內(nèi)容.代碼如下:
@interface AVMetadataItem (THAdditions)
@property (readonly) NSString *keyString;
// .m
- (NSString *)keyString {
if ([self.key isKindOfClass:[NSString class]]) {
return (NSString *)self.key;
} else if ([self.key isKindOfClass:[NSNumber class]]) {
UInt32 keyValue = [(NSNumber *) self.key unsignedIntValue];
//大部分keys 有 4 字符長度,而 ID3v2.2 格式的keys 只有3個(gè)字符,下面代碼表示移動(dòng)每個(gè)字節(jié)來確定length長度是要截短.
size_t length = sizeof(UInt32);
if ((keyValue >> 24) == 0) --length;
if ((keyValue >> 16) == 0) --length;
if ((keyValue >> 8) == 0) --length;
if ((keyValue >> 0) == 0) --length;
long address = (unsigned long)&keyValue;
address += (sizeof(UInt32) - length);
// keys 是以big-endian(高位優(yōu)先)格式存儲的.需要轉(zhuǎn)換成符合主流CPU順序的little-endian格式.
keyValue = CFSwapInt32BigToHost(keyValue);
// 創(chuàng)建一個(gè)字符數(shù)組,以keys字符字節(jié)填充
char cstring[length];
strncpy(cstring, (char *) address, length);
cstring[length] = '\0';
// 大部分QuickTime和iTunes keys前綴都有一個(gè) '?', 而AVMetadataFormat.h 用'@' 表示,所以轉(zhuǎn)換一下.
if (cstring[0] == '\xA9') {
cstring[0] = '@';
}
return [NSString stringWithCString:(char *) cstring
encoding:NSUTF8StringEncoding];
}
else {
return @"<<unknown>>";
}
}
除了通過鍵和鍵空間獲取資源的元數(shù)據(jù)之外,iOS 8之后添加了用identifier獲取元數(shù)據(jù)的方法. 標(biāo)識符將鍵和鍵空間統(tǒng)一成單獨(dú)的字符串,以一個(gè)更簡單的模型來獲取資源的元數(shù)據(jù),具體參考AVMetadataItem.h, 這里使用鍵和鍵空間是為了方便兼容以前的系統(tǒng).
1.3元數(shù)據(jù)的解析
通過上面的方法獲得元數(shù)據(jù)以及keys屬性的內(nèi)容轉(zhuǎn)換之后,接下來到了最難的部分,就是理解key對應(yīng)value中的數(shù)據(jù).當(dāng)value是一個(gè)簡單字符串時(shí),比如歌手或唱片名稱或年份,這樣容易理解的是不需要轉(zhuǎn)換的.但是有很多復(fù)雜key的value需要轉(zhuǎn)換解析:
-
Artwork:
元數(shù)據(jù)Artwork對應(yīng)的value會以多種不同的格式返回,比如封面和海報(bào)等,它保存在一個(gè)NSData中,我們要先定位
// 解析
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
NSImage *image = nil;
if ([item.value isKindOfClass:[NSData class]]) {
image = [[NSImage alloc] initWithData:item.dataValue];
}
else if ([item.value isKindOfClass:[NSDictionary class]]) { // 如果對象是MP3格式.value就可能是個(gè)字典.
NSDictionary *dict = (NSDictionary *)item.value;
image = [[NSImage alloc] initWithData:dict[@"data"]];
}
return image;
}
// 恢復(fù)原格式
- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
withMetadataItem:(AVMetadataItem *)item {
AVMutableMetadataItem *metadataItem = [item mutableCopy];
NSImage *image = (NSImage *)value;
metadataItem.value = image.TIFFRepresentation;
return metadataItem;
}
- 注釋:
當(dāng)處理對象是MPEG-4或QuickTime時(shí),可以直接獲取對應(yīng)字符串,如果是mp3格式:
// 解析
- (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;
}
// 恢復(fù)
- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
withMetadataItem:(AVMetadataItem *)item {
AVMutableMetadataItem *metadataItem = [item mutableCopy]; // 3
metadataItem.value = value;
return metadataItem;
}
-
音軌數(shù)據(jù)信息:
音軌通常包含一首歌在整個(gè)唱片中的編號位置信息.
// 解析
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item {
NSNumber *number = nil;
NSNumber *count = nil;
if ([item.value isKindOfClass:[NSString class]]) { // MP3音頻信息以"xx/xx"字符串格式返回.例如一個(gè)包含10首歌曲的唱片中第8首就是8/10;
NSArray *components =
[item.stringValue componentsSeparatedByString:@"/"];
number = @([components[0] integerValue]);
count = @([components[1] integerValue]);
}
else if ([item.value isKindOfClass:[NSData class]]) { // 對于MPEG-4格式,則復(fù)雜點(diǎn).
NSData *data = item.dataValue;
if (data.length == 8) {
uint16_t *values = (uint16_t *) [data bytes];
if (values[1] > 0) {
number = @(CFSwapInt16BigToHost(values[1]));
}
if (values[2] > 0) {
count = @(CFSwapInt16BigToHost(values[2]));
}
}
}
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:number ?: [NSNull null] forKey:THMetadataKeyTrackNumber]; // 得到的音軌編號
[dict setObject:count ?: [NSNull null] forKey:THMetadataKeyTrackCount];// 得到的音軌計(jì)數(shù)
return dict;
}
// 恢復(fù)
- (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;
}
- 風(fēng)格信息:
鄉(xiāng)村,爵士,藍(lán)調(diào)等等.
// 轉(zhuǎn)換
- (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;
}
//恢復(fù)
- (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;
}
1.4 導(dǎo)出修改后的元數(shù)據(jù)
通過上面的解析轉(zhuǎn)換方法,我們就可以進(jìn)行元數(shù)據(jù)信息的讀取與修改,修改完當(dāng)然需要保存了.不過中間還有一個(gè)問題: 由于AVAsset是一個(gè)不可變類,所以我們不能直接修改AVAsset,而是使用AVAssetExportSession
類導(dǎo)出一個(gè)新的資源副本.
-
AVAssetExportSession配置
AVAssetExportSession是用于將AVAsset內(nèi)容根據(jù)預(yù)設(shè)的導(dǎo)出條件進(jìn)行轉(zhuǎn)碼,并寫入磁盤中,用它可以實(shí)現(xiàn)將一種格式轉(zhuǎn)換成另一種格式,修訂資源內(nèi)容,修改資源的音視頻行為.也包含了寫入新的元數(shù)據(jù).
所以創(chuàng)建AVAssetExportSession實(shí)例要先提供資源和預(yù)設(shè)條件; 導(dǎo)出預(yù)設(shè)用于確定導(dǎo)出內(nèi)容的質(zhì)量,大小等屬性. 創(chuàng)建完成后還需要指定一個(gè)outputURL寫入地址,并且給outputFileType一個(gè)格式.代碼如下:
- (void)saveWithCompletionHandler:(THCompletionHandler)handler {
// 先用AVAssetExportPresetPassthrough預(yù)設(shè)值創(chuàng)建一個(gè)AVAssetExportSession
NSString *presetName = AVAssetExportPresetPassthrough;
AVAssetExportSession *session =
[[AVAssetExportSession alloc] initWithAsset:self.asset
presetName:presetName];
// 配置導(dǎo)出預(yù)設(shè)
NSURL *outputURL = [self tempURL];
session.outputURL = outputURL;
session.outputFileType = self.filetype;
// 用上面提到的解析與恢復(fù)方法修改元數(shù)據(jù)并返回
session.metadata = [self.metadata metadataItems];
// 最后異步導(dǎo)出修改后的元數(shù)據(jù)
[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];
}
if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(success);
});
}
}];
}
AVAssetExportPresetPassthrough預(yù)設(shè)值允許修改預(yù)設(shè)值,但是不能用于添加元數(shù)據(jù),如果要添加元數(shù)據(jù),需要使用轉(zhuǎn)碼預(yù)設(shè)值.