《AV Foundation 開發(fā)秘籍》讀書筆記(三)

第三章 資源和元數(shù)據(jù)

1. 資源簡介

AV Foundation 把所有的代碼設(shè)計圍繞資源(assert)進(jìn)行,AVAssert 是 AV Foundation 設(shè)計的核心。

AVAssert 不需要考慮的兩個重要范疇:第一、它提供了對基本媒體格式的層抽象,這意味著無論是處理 MPEG-4 視頻還是 MP3 音頻,對你而言面對的只有資源這個概念。第二、不用我們管理資源的位置信息,不管是本地 URL 還是遠(yuǎn)程服務(wù)器上的一個音頻流、視頻流的 URL。

AVAssert 本身并不是媒體資源,但是它可以作為時基媒體的容器,它由一個或多個帶有描述自身元數(shù)據(jù)的媒體組成。我們使用 AVAssetTrack 類代表保存在資源中的統(tǒng)一類型媒體,并對每個資源建立相應(yīng)的模型。AVAssetTrack 最常見的形態(tài)就是音頻和視頻流,但是它還可以表示文本、副標(biāo)題或隱藏字幕等媒體類型。

2. 資源創(chuàng)建

為一個媒體資源創(chuàng)建 AVAsset 對象時,可以通過 URL 對它進(jìn)行初始化來實現(xiàn),可以是本地 URL,也可以是遠(yuǎn)程資源的 URL

NSURL *url = [NSURL URLWithString:@""];
AVAsset *asset = [AVAsset assetWithURL:url];

AVAsset 是一個抽象類,意味著不能直接被實例化。當(dāng)創(chuàng)建實例時,實際上是創(chuàng)建了它子類的一個實例,子類名為 AVURLAsset。有時我們會直接使用這個類,因為它允許通過傳遞選項字典來精細(xì)調(diào)整資源的創(chuàng)建方式。比如,如果創(chuàng)建一個用在音頻或視頻編輯場景中的資源,希望傳遞一個選項來告訴程序提供更精確的時長和計時信息:

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

3. 異步載入

AVAsset 具有多種方法和屬性,可以提供有關(guān)資源的信息,比如時長、創(chuàng)建日期和元數(shù)據(jù)等。AVAsset 使用了一種高效的設(shè)計方法,延遲載入資源屬性,直到請求時才載入,這樣就可以快速創(chuàng)建資源。不過屬性的訪問是同步發(fā)生的,如果正在請求的屬性沒有預(yù)先載入,程序就會阻塞,所以開發(fā)者應(yīng)該使用異步來查詢資源的屬性。

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. 媒體元數(shù)據(jù)

4.1 元數(shù)據(jù)格式

我們在 Apple 環(huán)境下遇到的媒體類型主要有四種:

  • QuickTime( .mov )
  • MPEG-4 video( .mp4 和 .m4v )
  • MPEG-4 audio( .m4a )
  • MPEG-Layer III audio( .mp3 )

QuickTime( .mov )

QuickTime 是由蘋果公司開發(fā)的一種功能強大、跨平臺的媒體架構(gòu)。了解 QuickTime 格式的一個好辦法就是在十六進(jìn)制編譯器中打開一個 .mov 格式的文件,常見的十六進(jìn)制編譯器有 Hex Fiend 或 Synalyze It! Pro。更好的方法是借助 Apple Developer Center 中找到 Atom Inspector 工具,它可以將 atom 結(jié)構(gòu)以 NSOutlineView 方式呈現(xiàn),所以可以對 atom 之間的繼承關(guān)系等信息有比較清晰的了解。

QuickTime

上圖為 QuickTime 結(jié)構(gòu)的簡化示意圖,該文件最小限度的包含了三個高級 atom,分別是用于描述文件類型和兼容類型的 ftyp,包含實際音視頻媒體的 mdat 以及非常重要的 moov atom,它對媒體資源的所有相關(guān)細(xì)節(jié)做了完整描述,包括可呈現(xiàn)的元數(shù)據(jù)。

MPEG-4 video( .mp4 和 .m4v )和 MPEG-4 audio( .m4a )

mp4 直接派生于 QuickTime 文件格式,這就意味著它們的結(jié)構(gòu)是類似的。實際上,我們經(jīng)常會發(fā)現(xiàn),能夠解析一種文件類型的工具也可以處理其他文件類型,就像 QuickTime 文件一樣,mp4 文件也由 atom 數(shù)據(jù)結(jié)構(gòu)組成。

MPEG-4 video

mp4 是對 MPEG-4 媒體的標(biāo)準(zhǔn)擴展,m4v、m4a、m4p、m4b 這些變體都使用 MPEG-4 容器格式,但是包含了附加的拓展功能。m4v 文件是帶有針對 FairPlay 加密和 AC3-audio 擴展的 MPEG-4 視頻格式,如果不涉及這些 mp4 和 m4v 僅僅是擴展名不同而已。m4a 針對音頻,讓使用者知道該文件只帶有音頻資源。m4p 是蘋果較舊的 iTunes 音頻格式,使用其 FairPlay 擴展。m4b 用于有聲讀物,通常包含章節(jié)標(biāo)簽和書簽功能。

MPEG-Layer III audio( .mp3 )

mp3 文件和上面兩種有顯著區(qū)別,mp3 文件不使用容器格式,而使用編碼音頻數(shù)據(jù),包含的可選元數(shù)據(jù)的結(jié)構(gòu)塊通常位于文件開頭。mp3 文件使用一種稱為 ID3v2 格式來保存關(guān)于音頻內(nèi)容的描述信息,包含的數(shù)據(jù)有歌曲演唱者、所屬唱片和音樂風(fēng)格等。

MPEG-Layer III audio

AV Foundation 支持讀取 ID3v2 標(biāo)簽的所有版本,但是不支持寫入,所以 AV Foundation 無法支持對 mp3 進(jìn)行編碼。

4.2 獲取元數(shù)據(jù)

AVAsset 和 AVAssetTrack 都可以實現(xiàn)查詢相關(guān)元數(shù)據(jù)的功能。一般使用 AVAsset 提供的元數(shù)據(jù),當(dāng)涉及獲取曲目一級元數(shù)據(jù)等情況時會使用 AVAssestTrack。讀取具體資源元數(shù)據(jù)的接口由 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 : 夏洛特?zé)?電影原聲帶
    }
}];

上面我們使用的是鍵和鍵空間來獲取元數(shù)據(jù),iOS 8 以后還引進(jìn)了標(biāo)識符獲取元數(shù)據(jù)的方法:

// 歌曲名稱
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 為一串?dāng)?shù)字,所以我們創(chuàng)建一個 AVMetadataItem 的分類,將數(shù)字轉(zhuǎn)換成字符串

NSLog(@"%@ : %@", titleItem.keyString,  titleItem.value);
NSLog(@"%@ : %@", artistItem.keyString, artistItem.value);
NSLog(@"%@ : %@", albumItem.keyString,  albumItem.value);
#import "AVMetadataItem+Extend.h"

@implementation AVMetadataItem (Extend)

- (NSString *)keyString
{
    // 如果 key 是一個字符串,則原樣返回
    if ([self.key isKindOfClass:[NSString class]]) {
        return (NSString *)self.key;
    }
    else if ([self.key isKindOfClass:[NSNumber class]]) {
        
        UInt32 keyValue = [(NSNumber *)self.key unsignedIntValue];
        
        // 大部分情況下,key 是一個 4 字符代碼,比如 ?gen 或 TRAK,不過對于 mp3 文件,鍵值只有 3 個字符的長度
        size_t length = sizeof(UInt32);
        if ((keyValue >> 24) == 0) --length;
        if ((keyValue >> 16) == 0) --length;
        if ((keyValue >> 8)  == 0) --length;
        if ((keyValue >> 0)  == 0) --length;
        
        long address = (unsigned long)&keyValue;
        address += (sizeof(UInt32) - length);
        
        // 由于數(shù)字是 big endian 格式,因此使用 CFSwapInt32BigToHost() 函數(shù)將其轉(zhuǎn)換為符合主 CPU 順序的 little endian 格式
        keyValue = CFSwapInt32BigToHost(keyValue);
        
        // 創(chuàng)建一個字符數(shù)組,并使用 strncpy 函數(shù)將字符字節(jié)填充到該數(shù)組中
        char cstring[length];
        strncpy(cstring, (char *) address, length);
        cstring[length] = '\0';
        
        // 大量 QuickTime 用戶數(shù)據(jù)和 iTunes key 的前綴都帶有一個 ? 符號
        // 不過 AVMetadataFormat.h 中定義 key 所使用的前綴符號為 @
        // 所以為了進(jìn)行 key 常量字符串比較,需要先將 ? 替換為 @
        if (cstring[0] == '\xA9') {
            cstring[0] = '@';
        }
        
        return [NSString stringWithCString:(char *) cstring
                                  encoding:NSUTF8StringEncoding];
    }
    else {
        return @"<<unknown>>";
    }
}

@end

總結(jié)

找不到相關(guān)資料,根據(jù)我的理解,availableMetadataFormats、metadata、commonMetadata 三者的關(guān)系如下

  • availableMetadataFormats:asset.availableMetadataFormats 獲得所有 keys,遍歷 keys 根據(jù) [asset metadataForFormat:key] 方法獲取所有元數(shù)據(jù) AVMetadataItem

  • metadata:通過 asset.metadata 方法直接可以得到所有元數(shù)據(jù),據(jù)測試和 availableMetadataFormats 獲得到的元數(shù)據(jù)相同

  • commonMetadata:通過 asset.commonMetadata 方法直接可以得到常用的元數(shù)據(jù),但是獲取的不全

所以我認(rèn)為最簡單獲取元數(shù)據(jù)方法如下,如果有錯誤,日后更正:

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 編輯元數(shù)據(jù)

AVAssetExportSession 用于將 AVAsset 內(nèi)容根據(jù)導(dǎo)出預(yù)設(shè)條件進(jìn)行轉(zhuǎn)碼,并將導(dǎo)出資源寫到磁盤中。

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 容器中存在的元數(shù)據(jù)信息,不過它不能添加新的元數(shù)據(jù)。添加元數(shù)據(jù)唯一方法是使用轉(zhuǎn)碼預(yù)設(shè)值。此外,它不能修改 ID3 標(biāo)簽,不支持寫入 MP3 數(shù)據(jù)。

5. 封裝框架

根據(jù) Learning-AV-Foundation Chapter 3 自行封裝了一個元數(shù)據(jù)獲取、編輯的框架,下載地址:https://github.com/Mayan29/MYAVFoundation

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容