iOS音頻開發之資源(AVAsset)與元數據,解決獲取資源屬性問題(三) – iPhone手機開發 iPhone軟體開發教學課程

本文主要是源碼的分析,程序成功解決獲取媒體元數據的信息,還可以對除瞭MP3之外的所有資源寫入元數據信息。再次回顧我們的問題。

先提出一個問題,生活中有很多的媒體格式,mp3,avi,rmvb等等,在蘋果環境下主要的媒體格式有4種,QuickTime(mov),MPEG-4 video(mp4,m4v),MPEG-4 audio(m4a),MPEG-Layer III audio(mp3),那麼問題來瞭,假如給你一個mp3文件,比如歌曲《再見.mp3》張震嶽,你是不是有辦法讀取裡面的數據,比如讀取它的歌名,演唱者,屬於哪個專輯,專輯的封面,文件的長度等等信息???

這是個Mac程序,用戶UI並不是我要討論的重點。下面的代碼,可以獲取資源的路徑,從而開始我們分析元數據。

 

- (void)loadTable {
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *rootURL = [NSURL fileURLWithPath:[fileManager applicationSupportDirectory]];
    NSArray *items = [fileManager contentsOfDirectoryAtURL:rootURL
                                includingPropertiesForKeys:@[NSURLNameKey, NSURLEffectiveIconKey]
                                                   options:NSDirectoryEnumerationSkipsHiddenFiles
                                                     error:nil];
    [items enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
        [self.mediaItemsController addObject:[[THMediaItem alloc] initWithURL:url]];
    }];
    [self.tableView reloadData];
}

我們查看文件的組織結構,如下圖,這裡引出瞭THMediaItem類,我們的程序主要圍繞這個類進行展開的。註意文件夾converters。

 


分析頭文件,幾個接口函數及幾個屬性函數。

 

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

 

需要註意類THMetadata。editable 表示是否可以編輯,mp3格式是無法編輯的,所以要區別對待,因為mp3有專利上的問題,所以蘋果不支持。前面說過,不同的媒體的元數據格式是不同的,所以有個filetype屬性,下面代碼能解決這個這個問題,有m4a,m4v,mov,mp4,這些格式。

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

獲取元數據的信息主要是在函數

prepareWithCompletionHandler:(THCompletionHandler)completionHandler

中實現,實現的代碼如下

 

- (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 commonKey : %@", item.keyString, item.value,item.commonKey);
                [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];
                    }
                }
            }
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            completionHandler(self.prepared);
        });

    }];
}

其中COMMON_META_KEY,AVAILABLE_META_KEY是定義的宏,這個函數在上一篇文章中介紹過。下面是一些打印信息

 

 

2016-09-18 16:20:33.239 MetaManager[2582:194668] TIT2: 再見 item commonKey : title

2016-09-18 16:20:33.239 MetaManager[2582:194668] the keyMapping {

"@cmt" = comments;

"@gen" = genre;

"@grp" = grouping;

"@wrt" = composer;

COM = comments;

COMM = comments;

TBP = bpm;

TBPM = bpm;

TDRC = year;

TP2 = albumArtist;

TPA = discNumber;

TPE2 = albumArtist;

TPOS = discNumber;

TRCK = trackNumber;

TRK = trackNumber;

TYE = year;

TYER = year;

aART = albumArtist;

albumName = album;

artist = artist;

artwork = artwork;

"com.apple.quicktime.director" = composer;

"com.apple.quicktime.genre" = genre;

"com.apple.quicktime.producer" = artist;

"com.apple.quicktime.year" = year;

creationDate = year;

creator = composer;

description = comments;

disk = discNumber;

grup = grouping;

ldes = comments;

subject = grouping;

title = name;

tmpo = bpm;

trkn = trackNumber;

type = genre;

}

2016-09-18 16:20:33.239 MetaManager[2582:194668] TALB: Ok item commonKey : albumName2016-09-18 16:20:33.240 MetaManager[2582:194668] TPE1: 張震嶽 item commonKey : artist2016-09-18 16:20:33.246 MetaManager[2582:194668] APIC:

0 fffe003c 43524541 544f523a 2067642d 6a706567 2076312e 30202875 73696e67 20494a47 204a5045 47207636 32292c20 7175616c 69747920 3d203130 300affdb 00430001 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 010101ff db004301 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 01010101 ffc00011 0800f000 f0030122 00021101 031101ff c4001f00 00000603 01010100 00000000 00000000 04050708 090a0306 0b020100 ffc40041 10000201 03040004 04040502 050303…….

是不是我們找到我們想到的信息,比如歌手,標題名字等等。現在要將目光轉移到THMetadata類,她的主要作用是從相關的元數據元素中提取值,並將她們保存,現在我們應該猜到這個類裡面有很多的屬性瞭吧。請看下面的THMetadata類頭文件文件的內容。

 

@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;

/**
 *  獲取所有當前保存展示內容,並將轉化類轉化為AVMetadataItem
 *
 *  @return 
 */
- (NSArray *)metadataItems;

@end

 

註意兩個接口函數以及類THGenre。我們看看這個類的實現,首先是init函數

 

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

引出瞭兩個重要的屬性,keyMapping 和 converterFactory。還記得前面提到文件夾converterFactory?這些文件都是為這個屬性服務的,她的主要作用是將元數據的值通過提取和轉換為可以用於展示的格式,比如NSString。要實現對不同的鍵空間的鍵和格式的標準化工作,需要創建一個從指定格式鍵到標準化鍵的映射。所以有瞭keyMapping,我們回頭看看前面打印的信息,是不是有個keyMapping,這個就是我們建立的keyMapping。建立keyMapping代碼如下。

 

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

像THMetadataKeyName,THMetadataKeyArtist這些常亮是在另外文件定義好的。接下來的函數的重點。

 

 

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

    NSLog(@"the keyMapping %@",self.keyMapping);  
    NSString *normalizedKey = self.keyMapping[key];                         // 1

    if (normalizedKey) {

        id  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
    }
}

 

這個函數傳入兩個函數 AVMetadataItem數據,指的是item的commonKey,看一段打印信息

 

}, value=張震嶽>,

這裡commonKey指的是artist,定義的常量為THMetadataKeyArtist。

現在註意這行代碼

 

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

終於聯系到這個converterFactory函數瞭,這裡有的協議Protocol,THMetadataConverter,轉換函數都是遵守這個協議的,請看下面代碼尋找各自的類。

 

 

@implementation THMetadataConverterFactory

- (id )converterForKey:(NSString *)key {

    id  converter = nil;

    if ([key isEqualToString:THMetadataKeyArtwork]) {
        converter = [[THArtworkMetadataConverter alloc] init];
    }
    else if ([key isEqualToString:THMetadataKeyTrackNumber]) {
        converter = [[THTrackMetadataConverter alloc] init];
    }
    else if ([key isEqualToString:THMetadataKeyDiscNumber]) {
        converter = [[THDiscMetadataConverter alloc] init];
    }
    else if ([key isEqualToString:THMetadataKeyComments]) {
        converter = [[THCommentMetadataConverter alloc] init];
    }
    else if ([key isEqualToString:THMetadataKeyGenre]) {
        converter = [[THGenreMetadataConverter alloc] init];
    }
    else {
        converter = [[THDefaultMetadataConverter alloc] init];
    }

    return converter;
}
@end

請看下面的這個factory的類文件組織結構。

 

看有這麼多的格式要判斷。回到那個協議,請看下面兩個要實現的接口

 

@protocol THMetadataConverter 

/**
 *  分析AVMetadataItem 將數據轉化為可展示的的格式
 */
- (id)displayValueFromMetadataItem:(AVMetadataItem *)item;

/**
 *  將可展示的內容轉化為之前的 AVMetadataItem
 */
- (AVMetadataItem *)metadataItemFromDisplayValue:(id)value
                                withMetadataItem:(AVMetadataItem *)item;
@end

這兩個函數的主要功能就是上面的註釋!在處理AVMetaItem的時候,最復雜的就是後去value值,看看有些是比較簡單,比如歌名,"張震嶽"直接可以取得,但是想圖片,artwork,comment(評論)這些數據是要分析的。下面給出獲取普通的信息代碼和圖片信息的代碼,不展開分析。

 

@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

 

 

@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

 

 

我還有很多的細節沒有分享,但是總體的思路是這樣的,分析源碼學習瞭很多的東西,其實這個問題沒有想象中這麼簡單,還有一個功能是保存元數據。

二.保存元數據

前面提到過,AVAsset是個不可以變化的類,我們不能修改,我們要使用AVAssetExportSession的類來導出一個新的資源副本以及元數據的改動。AVAssetExportSession是用於將AVAsset內容根據導出預設進行編碼,並將導出資源寫到磁盤中。有興趣大傢可以去研究下。

 

本文

完!

 

 

 

 

 

 

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *