本文轉自:AVAudioFoundation(1):使用 AVAsset | www.samirchen.com
本文主要內容來自 AVFoundation Programming Guide。
要了解 iOS 上的音視頻相關的內容,首先需要了解的就是 AVFoundation
這個框架。
下圖是 AVFoundation
框架大的層級結構:
在 AVFoundation
框架中,最主要的表示媒體的類就是 AVAsset
,甚至可以認為 AVFoundation
框架的大部分能力都是圍繞着 AVAsset
展開的。
一個 AVAsset
實例表示的是一份或多份音視頻數據(audio and video tracks)的集合,它描述的是這個集合作為一個整體對象的一些屬性,比如:標題、時長、大小等,而不與具體的數據格式綁定。通常,在實際使用時我們可能會基於某個 URL 創建對應的媒體資源對象(AVURLAsset),或者直接創建 compositions(AVComposition),這些類都是 AVAsset
的子類。
一個 AVAsset
中的每一份音頻或視頻數據都稱為一個軌道(track)。在最簡單的情況下,一個媒體文件中可能只有兩個軌道,一個音頻軌道,一個視頻軌道。而復雜的組合中,可能包含多個重疊的音頻軌道和視頻軌道。此外 AVAsset
也可能包含元數據(metadata)。
在 AVFoundation
中另一個非常重要的概念是,初始化一個 AVAsset
或者一個 AVAssetTrack
時並不一定意味着它已經可以立即使用,因為這需要一段時間來做計算,而這個計算可能會阻塞當前線程,所以通常你可以選用異步的方式來初始化,並通過回調來得到異步返回。
我們可以從一個文件或者用戶的相冊中來創建 asset。獲得一個視頻 asset 時,我們可以從中提出靜態圖,對其進行轉碼,裁剪器內容。
創建 Asset
當使用一個 URL 來創建 asset 時,可以用 AVURLAsset
:
NSURL *url = <#A URL that identifies an audiovisual asset such as a movie file#>;
AVURLAsset *anAsset = [[AVURLAsset alloc] initWithURL:url options:nil];
設置 Asset 選項
可以看到當我們創建一個 AVURLAsset
時,是可以設置一個對象的 options 的,這里可選的設置項包括:
AVURLAssetPreferPreciseDurationAndTimingKey
,這個選項對應的值是布爾值,默認為@(NO)
,當設為@(YES)
時表示 asset 應該提供精確的時長,並能根據時間准確地隨機訪問,提供這樣的能力是需要開銷更大的計算的。當你只是想播放視頻時,你可以不設置這個選項,但是如果你想把這個 asset 添加到一個 composition(AVMutableComposition)中去做進一步編輯,你通常需要精確的隨機訪問,這時你最好設置這個選項為 YES。AVURLAssetReferenceRestrictionsKey
,這個選項對應的值是AVAssetReferenceRestrictions
enum。有一些 asset 可以保護一些指向外部數據的引用,這個選項用來表示對外部數據訪問的限制。具體含義參見AVAssetReferenceRestrictions
。AVURLAssetHTTPCookiesKey
,這個選項用來設置 asset 通過 HTTP 請求發送的 HTTP cookies,當然 cookies 只能發給同站。具體參見文檔。AVURLAssetAllowsCellularAccessKey
,這個選項對應的值是布爾值,默認為@(YES)
。表示 asset 是否能使用移動網絡資源。
不過你要注意這幾個選項適用的 iOS 版本。
NSURL *url = <#A URL that identifies an audiovisual asset such as a movie file#>;
NSDictionary *options = @{ AVURLAssetPreferPreciseDurationAndTimingKey : @YES };
AVURLAsset *anAssetToUseInAComposition = [[AVURLAsset alloc] initWithURL:url options:options];
訪問用戶的 Asset
獲取用戶相冊的資源時,你需要借用 ALAssetsLibrary
的相關接口:
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
// Enumerate just the photos and videos group by using ALAssetsGroupSavedPhotos.
[library enumerateGroupsWithTypes:ALAssetsGroupSavedPhotos usingBlock:^(ALAssetsGroup *group, BOOL *stop) {
// Within the group enumeration block, filter to enumerate just videos.
[group setAssetsFilter:[ALAssetsFilter allVideos]];
// For this example, we're only interested in the first item.
[group enumerateAssetsAtIndexes:[NSIndexSet indexSetWithIndex:0]
options:0
usingBlock:^(ALAsset *alAsset, NSUInteger index, BOOL *innerStop) {
// The end of the enumeration is signaled by asset == nil.
if (alAsset) {
ALAssetRepresentation *representation = [alAsset defaultRepresentation];
NSURL *url = [representation url];
AVAsset *avAsset = [AVURLAsset URLAssetWithURL:url options:nil];
// Do something interesting with the AV asset.
}
}];
}
failureBlock: ^(NSError *error) {
// Typically you should handle an error more gracefully than this.
NSLog(@"No groups");
}];
加載 Asset 來使用
初始化一個 AVAsset
或者一個 AVAssetTrack
時並不一定意味着它已經可以立即使用,因為這需要一段時間來做計算,而這個計算可能會阻塞當前線程,所以通常你可以選用異步的方式來初始化,並通過回調來得到異步返回。
這時你可以使用 AVAsynchronousKeyValueLoading
protocol 來獲取加載 asset 的狀態,並在對應的 completion handler 中做對應的處理。AVAsset
和 AVAssetTrack
都是遵循 AVAsynchronousKeyValueLoading
protocol 的。下面是一個示例:
NSURL *url = <#A URL that identifies an audiovisual asset such as a movie file#>;
AVURLAsset *anAsset = [[AVURLAsset alloc] initWithURL:url options:nil];
NSArray *keys = @[@"duration"];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^() {
NSError *error = nil;
AVKeyValueStatus tracksStatus = [asset statusOfValueForKey:@"duration" error:&error];
switch (tracksStatus) {
case AVKeyValueStatusLoaded:
[self updateUserInterfaceForDuration];
break;
case AVKeyValueStatusFailed:
[self reportError:error forAsset:asset];
break;
case AVKeyValueStatusCancelled:
// Do whatever is appropriate for cancelation.
break;
}
}];
需要注意的是:當你需要加載一個 asset 來點播,你應該加載它的 tracks
屬性。
獲取視頻截圖
我們可以用一個 AVAssetImageGenerator
實例來獲取視頻中的截圖。即使初始化時在 asset 中沒有檢查到視覺 track,AVAssetImageGenerator
的初始化也可能會成功,所以必要的情況下,你可以用 tracksWithMediaCharacteristic:
方法去檢查一下 asset 是否有可用的視覺 track。
AVAsset anAsset = <#Get an asset#>;
if ([[anAsset tracksWithMediaType:AVMediaTypeVideo] count] > 0) {
AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:anAsset];
// Implementation continues...
}
我們可以配置一下 AVAssetImageGenerator
,比如用 maximumSize
和 apertureMode
來指定生成圖像的最大尺寸和光圈模式。接下來,可以生成指定時間的一張截圖或者一系列圖集。必須保證在生成圖片時對 AVAssetImageGenerator
實例的強引用。
獲取一張圖片
我們可以用 copyCGImageAtTime:actualTime:error:
來獲得指定時間的截圖。AVFoundation
也許無法精確地獲得你指定時間的截圖,所以你需要傳入一個 actualTime 參數來獲得截圖所對應的實際時間。
AVAsset *myAsset = <#An asset#>];
AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:myAsset];
Float64 durationSeconds = CMTimeGetSeconds([myAsset duration]);
CMTime midpoint = CMTimeMakeWithSeconds(durationSeconds/2.0, 600);
NSError *error;
CMTime actualTime;
CGImageRef halfWayImage = [imageGenerator copyCGImageAtTime:midpoint actualTime:&actualTime error:&error];
if (halfWayImage != NULL) {
NSString *actualTimeString = (NSString *)CMTimeCopyDescription(NULL, actualTime);
NSString *requestedTimeString = (NSString *)CMTimeCopyDescription(NULL, midpoint);
NSLog(@"Got halfWayImage: Asked for %@, got %@", requestedTimeString, actualTimeString);
// Do something interesting with the image.
CGImageRelease(halfWayImage);
}
獲取一組截圖
我們可以用 generateCGImagesAsynchronouslyForTimes:completionHandler:
接口來傳入一組時間來獲取相應的一組截圖。同樣的,必須保證在生成圖片時對 AVAssetImageGenerator
實例的強引用。示例代碼如下:
AVAsset *myAsset = <#An asset#>];
// Assume: @property (strong) AVAssetImageGenerator *imageGenerator;
self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:myAsset];
Float64 durationSeconds = CMTimeGetSeconds([myAsset duration]);
CMTime firstThird = CMTimeMakeWithSeconds(durationSeconds/3.0, 600);
CMTime secondThird = CMTimeMakeWithSeconds(durationSeconds*2.0/3.0, 600);
CMTime end = CMTimeMakeWithSeconds(durationSeconds, 600);
NSArray *times = @[NSValue valueWithCMTime:kCMTimeZero],
[NSValue valueWithCMTime:firstThird], [NSValue valueWithCMTime:secondThird],
[NSValue valueWithCMTime:end]];
[imageGenerator generateCGImagesAsynchronouslyForTimes:times
completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error) {
NSString *requestedTimeString = (NSString *)
CFBridgingRelease(CMTimeCopyDescription(NULL, requestedTime));
NSString *actualTimeString = (NSString *)
CFBridgingRelease(CMTimeCopyDescription(NULL, actualTime));
NSLog(@"Requested: %@; actual %@", requestedTimeString, actualTimeString);
if (result == AVAssetImageGeneratorSucceeded) {
// Do something interesting with the image.
}
if (result == AVAssetImageGeneratorFailed) {
NSLog(@"Failed with error: %@", [error localizedDescription]);
}
if (result == AVAssetImageGeneratorCancelled) {
NSLog(@"Canceled");
}
}];
我們還能使用 cancelAllCGImageGeneration
接口來中斷截圖。
對視頻進行裁剪和轉碼
我們可以使用一個 AVAssetExportSession
實例來對視頻進行裁剪或格式轉換。流程如下圖所示:
AVAssetExportSession
實例用來控制異步的導出 asset。使用 export session 時,首先我們需要傳入要導出的 asset 和對應的 preset 配置,我們可以用 allExportPresets
接口來查看所有可用的 preset 配置。接着,你需要設置導出的 URL 和文件類型。此外,我們還能設置導出視頻文件的 metadata 以及導出的是否應該針對網絡訪問優化。
在下面的示例代碼中,我們用 exportPresetsCompatibleWithAsset:
接口檢查可用的 preset,用 outputURL
和 outputFileType
接口設置導出 URL 和導出文件類型,通過 timeRange
設置導出時間段。此外,我們還能用 shouldOptimizeForNetworkUse
接口設置是否針對網絡使用優化以方便秒開,用 maxDuration
、fileLengthLimit
設置導入限制等等。
我們用 exportAsynchronouslyWithCompletionHandler:
接口來開始導出。
AVAsset *anAsset = <#Get an asset#>;
NSArray *compatiblePresets = [AVAssetExportSession exportPresetsCompatibleWithAsset:anAsset];
if ([compatiblePresets containsObject:AVAssetExportPresetLowQuality]) {
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:anAsset presetName:AVAssetExportPresetLowQuality];
exportSession.outputURL = <#A file URL#>;
exportSession.outputFileType = AVFileTypeQuickTimeMovie;
CMTime start = CMTimeMakeWithSeconds(1.0, 600);
CMTime duration = CMTimeMakeWithSeconds(3.0, 600);
CMTimeRange range = CMTimeRangeMake(start, duration);
exportSession.timeRange = range;
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusFailed:
NSLog(@"Export failed: %@", [[exportSession error] localizedDescription]);
break;
case AVAssetExportSessionStatusCancelled:
NSLog(@"Export canceled");
break;
default:
break;
}
}];
}
此外,我們還可以用 cancelExport
接口來取消導出。
當我們想要覆蓋已有的文件,或者向應用沙盒外寫文件時,導出會失敗。此外,在導出時突然來了電話、導出時應用在后台狀態並且其他應用開始播放時導出也可能會失敗。在這些情況下,你需要提示用戶導出失敗,並允許用戶重新導出。