8.1 綜述
AV Foundation定義了一組功能可以用于創(chuàng)建媒體應(yīng)用程序時(shí)遇到的大部分用例場(chǎng)景。在使用框架時(shí),開發(fā)者首選的方法還是使用專門的高級(jí)功能,不過隨著我們開發(fā)更復(fù)雜的媒體應(yīng)用程序,就可能遇到一些用例,它們需要的功能并不受AV Foundation框架的內(nèi)置支持。這意味著我們不夠幸運(yùn)并需要到其他地方尋找解決方案嗎?不,這時(shí)需要使用框架的AVAssetReader和AVAssetWriter類提供的低級(jí)功能(如圖8-1所示)。這些類可以讓開發(fā)者直接處理媒體樣本,打開了機(jī)會(huì)之窗。

8.1.1 AVAssetReader
AVAssetReader用于從AVAsset實(shí)例中讀取媒體樣本。通常會(huì)配置一個(gè)或多個(gè)AVAssetReaderOutput實(shí)例,并通過copyNextSampleBuffer方法可以訪問音頻樣本和視頻幀。AVAssetReaderOutput是一個(gè)抽象類,不過框架定義了3個(gè)具體實(shí)例來從指定的AVAssetTrack中讀取解碼的媒體樣本,從多音頻軌道中讀取混合輸出,或者從多視頻軌道中讀取組合輸出。一個(gè)資源讀取器的內(nèi)部通道都是以多線程的方式不斷提取下一個(gè)可用樣本的,這樣可以在系統(tǒng)請(qǐng)求資源時(shí)最小化時(shí)延。盡管提供了低時(shí)延的檢索操作,還是不傾向于實(shí)時(shí)操作,比如播放。
注意:
AVAssetReader只針對(duì)帶有一個(gè)資源的媒體樣本。如果需要同時(shí)從多個(gè)基于文件的資源中讀取樣本,可將它們組合到一個(gè)AVAsset子類AVComposition 中,下一章我們會(huì)講到相關(guān)內(nèi)容。
8.1.2 AVAssetWriter
AVAssetWriter是AVAssetReader對(duì)應(yīng)的兄弟類,它用于對(duì)媒體資源進(jìn)行編碼并將其寫入到容器文件中,比如一個(gè)MPEG-4文件或一個(gè)QuickTime文件。它由一個(gè)或多個(gè)AVAssetWriterInput對(duì)象配置,用于附加將包含要寫入容器的媒體樣本的CMSampleBuffer對(duì)象。AVAssetWriterInput被配置為可以處理指定的媒體類型,比如音頻或視頻,并且附加在其后的樣本會(huì)在最終輸出時(shí)生成一個(gè)獨(dú)立的AVAssetTrack。當(dāng)使用一個(gè)配置 了處理視頻樣本的AVAssetWriterInput時(shí),開發(fā)者會(huì)經(jīng)常用到一個(gè)專門的適配器對(duì)象AVAssetWriterInputPixelBufferAdaptor。這個(gè)類在附加被包裝為CVPixelBuffer對(duì)象的視頻樣本時(shí)提供最優(yōu)性能。輸入信息也可以通過使用AVAssetWriterInputGroup組成互斥的參數(shù)。這就讓開發(fā)者能夠創(chuàng)建特定資源,其中包含在播放時(shí)使用AVMediaSelectionGroup和AVMediaSelectionOption類選擇的指定語言媒體軌道,第4章介紹過這些類。
AVAssetWriter可以自動(dòng)支持交叉媒體樣本。將樣本寫入磁盤的一種方式是按照順序?qū)懭?,如圖8-2所示。需要將所有的媒體樣本都捕捉好,不過這會(huì)導(dǎo)致數(shù)據(jù)的低效率排列,因?yàn)楸緫?yīng)該整體呈現(xiàn)的樣本數(shù)據(jù)可能會(huì)彼此分開。這就使得存儲(chǔ)設(shè)備更難有效地讀取數(shù)據(jù),并在播放和尋找資源時(shí)產(chǎn)生負(fù)面效果和性能問題。

一種更好安排這些樣本的方法是使用交錯(cuò)模式,如圖8-3所示。為保持一個(gè)合適的交錯(cuò)模式,AVAssetWriterInput提供一個(gè)readyForMoreMediaData屬性來指示在保持所需的交錯(cuò)情況下輸入信息是否還可以附加更多數(shù)據(jù)。只有在這個(gè)屬性值為YES時(shí)才可以將一個(gè) 新的樣本添加 到寫入輸入信息中。

AVAssetWriter可用于實(shí)時(shí)操作和離線操作兩種情況,不過對(duì)于每個(gè)場(chǎng)景都有不同的方法將樣本buffer添加到寫入對(duì)象的輸入中。
●實(shí)時(shí):當(dāng)處理實(shí)時(shí)資源時(shí),比如從AVCaptureVideoDataOutput寫入捕捉的樣本時(shí),AVAssetWriterInput應(yīng)該令expectsMediaDataInRealTime屬性為YES來確保readyForMoreMediaData值被正確計(jì)算。從實(shí)時(shí)資源寫入數(shù)據(jù)優(yōu)化了寫入器,這樣一來,與維持理想交錯(cuò)效果相比,快速寫入樣本具有更高的優(yōu)先級(jí)。這一優(yōu)化效果不錯(cuò),視頻和音頻樣本以大致相同的速率捕捉,傳入數(shù)據(jù)自然交錯(cuò)。
●離線:當(dāng)從離線資源中讀取媒體資源時(shí),比如從AVAssetReader讀取樣本buffer, 在附加樣本前仍然需要觀察寫入器輸入的readyForMoreMediaData屬性的狀態(tài),不過可以使用requestMediaDataWhenReadyOnQueue:usingBlock:方法控制數(shù)據(jù)的提供。 傳到這個(gè)方法中的代碼塊會(huì)隨寫入器輸入準(zhǔn)備附加更多的樣本而不斷被調(diào)用,添加樣本時(shí)開發(fā)者需要檢索數(shù)據(jù)并從資源中找到下一個(gè)樣本進(jìn)行添加。
8.1.3 讀寫示例
下面通過一個(gè)基礎(chǔ)示例學(xué)習(xí)在一個(gè)離線場(chǎng)景下如何使用AVAssetReader和AVAssetWriter。在示例中,我們用AVAssetReader從 資源的視頻軌道讀取樣本,并使用AVAssetWriter將它們寫入到一個(gè)新的QuickTime電影文件中。雖然這是一個(gè)虛構(gòu)的示例,不過它展示了在同時(shí)使用這些類時(shí)所涉及的一些基本步驟。下面首先來設(shè)置和配置AVAssetReader。
AVAsset *asset = // Asynchronously loaded video asset
AVAssetTrack *track = [[asset tracksWithMediaType:AVMediaTypeVideo] firstobject];
self.assetReader = [[AVAssetReader alloc] initWithAsset:asset error:nil];
NSDictionary *readerOutputSettings = @{
(id) kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA)
};
AVAssetReaderTrackOutput *trackOutput = [[AVAsse tReaderTrackOutput alloc] initwithTrack:track
outputSettings: readerOutputSettings];
[self.assetReader addOutput:trackOutput];
[self.assetReader startReading];
首先創(chuàng)建一個(gè)新的AVAssetReader,傳遞讀取的AVAsset實(shí)例。創(chuàng)建一個(gè)AVAssetReaderTrackOutput從資源的視頻軌道中讀取樣本,將視頻幀解壓縮為BGRA格式。添加輸出到讀取器,并調(diào)用startReading方法開始讀取過程。
接下來創(chuàng)建并配置AVAssetWriter。
NSURL *outputURL = // Destination output URL
self.assetWriter = [[AVAssetWriter alloc] initwithURL:outputURL
fileType:AVFileTypeQuickTimeMovie
error:nil];
NSDictionary *writerOutputSettings = @{
AVVideoCodecKey:AVVideoCodecH264,
AVVideoWidthKey:@1280,
AVVideoHeightKey:@720,
AVVideoCompressionPropertiesKey:@{
AVVideoMaxKeyF rameIntervalKey:@1
AVVideoAverageBitRateKey:@10500000,
AVVideoProfileLevelKey:AVVideoProfileLeve1H264Main31,
}
};
AVAssetWriterInput *writerInput = [[AVAssetWriterInput alloc] initwithMediaType:AVMediaTypeVideo
outputSettings:writerOutputSettings];
[self.assetWriter addInput:writerInput];
[self.assetWriter startWriting];
示例中創(chuàng)建了一個(gè)新的AVAssetWriter對(duì)象,并傳遞了一個(gè)新文件寫入目的地的輸出URL和所希望的文件類型。創(chuàng)建了一個(gè)新的AVAssetWriterInput,它帶有相應(yīng)的媒體類型和輸出設(shè)置, 以便創(chuàng)建一個(gè)720p H.264格式的視頻。將輸入添加到寫入器并調(diào)用startWriting方法。
注意:
與AVAssetExportSession相比,AVAssetWriter 明顯的優(yōu)勢(shì)就是它對(duì)輸出進(jìn)行編碼時(shí)能夠進(jìn)行更加細(xì)致的壓縮設(shè)置控制。可以讓開發(fā)者指定諸如關(guān)鍵幀間隔、視頻比特率、H.264配置文件、像素寬高比和純凈光圈等設(shè)置。
在完成AVAssetReader和AVAssetWriter對(duì)象的設(shè)置后,是時(shí)候創(chuàng)建一個(gè)新的寫入會(huì)話來從資源中讀取樣本并將它們寫入到新位置。示例中使用的是拉模式(pull model),即當(dāng)寫入器輸入準(zhǔn)備附加更多的樣本時(shí)從資源中拉取樣本。這是當(dāng)我們從一個(gè)非實(shí)時(shí)資源中寫入樣本時(shí)所使用的模式。
// Serial Queue
dispatch_queue_t dispatchQueue = dispatch_queue_create("com.tapharmonic.WriterQueue", NULL);
[self.assetWriter startSessionAtSourceTime:kCMTimeZero];
[writerInput requestMediaDataWhenReadyOnQueue:dispatchQueue usingBlock:^{
BOOL complete = NO;
while ([writerInput isReadyForMoreMediaData] && !complete) {
CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer];
if (sampleBuffer) {
BOOL result = [writerInput appendSampleBuffer:sampleBuffer];
CFRelease (sampleBuffer);
complete = !result;
} else {
[writerInput ma rkAsFinished];
complete = YES;
}
}
if (complete) {
[self.assetWriter fini shWritingWithCompletionHandler:^{
AVAssetWriterStatus status = self. assetWriter .status;
if (status == AVAssetWriterStatusCompleted) {
// Handle success case
} else {
// Handle failure case
}
}];
}
}];
示例中首先使用startSessionAtSourceTime:方法創(chuàng)建了一個(gè)新的寫入會(huì)話,并傳遞kCMTimeZero參數(shù)作為資源樣本的開始時(shí)間。傳給requestMediaDataWhenReadyOnQueue:usingBlock:方法的代碼塊在寫入器輸入準(zhǔn)備好添加更多樣本時(shí)會(huì)不斷被調(diào)用。在每次調(diào)用期間,輸入準(zhǔn)備添加更多數(shù)據(jù)時(shí),再從軌道的輸出中復(fù)制可用的樣本,并將其附加到輸入中。當(dāng)所有樣本都從軌道輸出中復(fù)制后,需要標(biāo)記AVAssetWriterInput已結(jié)束并指明添加操作已經(jīng)完成。最后,調(diào)用finishWritingWithCompletionHandler:關(guān)閉寫入會(huì)話。資源寫入器的status屬性可在completion handler中查詢, 來確定寫入會(huì)話是否成功完成、失敗或取消。
現(xiàn)在我們對(duì)AVAssetReader和AVAssetWriter這兩個(gè)類有了更多了解。上述示例中已經(jīng)給出了我們使用這些類處理離線資源時(shí)所使用的基礎(chǔ)模式。下面繼續(xù)學(xué)習(xí)并討論一些更加具體、真實(shí)的情況,這樣會(huì)更好地理解AVAssetReader和AVAssetWriter的價(jià)值。
8.2 創(chuàng)建音頻波形視圖
對(duì)于大部分音頻和視頻應(yīng)用程序,一個(gè)常見的需求就是提供圖像化顯示的音頻波形(waveform),如圖8-4所示。這個(gè)功能可以讓用戶更簡單地查看音頻軌道,也就更容易操作滑動(dòng)條或選定希望的位置進(jìn)行編輯。本節(jié)將討論如何使用AVFoundation來實(shí)現(xiàn)這個(gè)功能。

繪制一個(gè)波形的基本技巧包括以下三個(gè)步驟:
(1)讀取:第一步是讀取音頻樣本進(jìn)行渲染。需要讀取或可能解壓音頻數(shù)據(jù),比如對(duì)于線性PCM資源?;仡櫟?章所講到的,線性PCM是一種未壓縮的音頻樣本格式。
(2)縮減:實(shí)際讀取到的樣本數(shù)量要遠(yuǎn)比我們?cè)谄聊簧箱秩镜亩???紤]到單聲道音頻文件是以44.1kHz比率進(jìn)行采樣的,所得到的樣本要比我們具有的像素多得多。縮減的過程必須作用于這個(gè)樣本集。這一過程通常包括將樣本總量分為小的樣本塊,并在每個(gè)樣本塊上找到最大的樣本、所有樣本的平均值或min/max值。
(3)渲染:在這一步我們將縮減后的樣本呈現(xiàn)在屏幕上。通常會(huì)用到Quartz框架,不過也可以使用任何蘋果公司支持的繪圖框架。如何繪制這些數(shù)據(jù)的類型取決于開發(fā)者是如何縮減樣本的。如果采用min/max對(duì),則為它的每一對(duì)繪制一 條垂線。如果使用每個(gè)樣本塊的平均值或最大值,會(huì)發(fā)現(xiàn)使用Quartz Bezier路徑繪制波形是最合適的。
在Chapter 8目錄下可以找到名為THWaveformView_Starter的示例項(xiàng)目。這里我們創(chuàng)建了一個(gè)UIView的子類來展示如圖8-5所示的波形效果。下面從第一步開始吧。

8.2.1 讀取音頻樣本
要?jiǎng)?chuàng)建的第一個(gè)類稱為THSampleDataProvider,這個(gè)類使用AVAssetReader實(shí)例從AVAsset中讀取音頻樣本并返回一個(gè)NSData對(duì)象。代碼清單8-1給出了這個(gè)類的接口。
代碼清單8-1 THSampleDataProvider 接口
#import <AVFoundation/AVFoundation.h>
typedef void(^THSampleDataCompletionBlock)(NSData *);
@interface THSampleDataProvider : NSObject
+ (void)loadAudioSamplesFromAsset:(AVAsset *)asset
completionBlock:(THSampleDataCompletionBlock)completionBlock;
@end
這個(gè)類的接口最直接的關(guān)注點(diǎn)就是loadAudioSamplesFromAsset:completionBlock:類方法,調(diào)用這個(gè)方法可以讀取音頻樣本。下 面看一下該類的具體實(shí)現(xiàn),如代碼清單8-2所示。
代碼清單8-2 THSampleDataProvider 實(shí)現(xiàn)
#import "THSampleDataProvider.h"
@implementation THSampleDataProvider
+ (void)loadAudioSamplesFromAsset:(AVAsset *)asset
completionBlock:(THSampleDataCompletionBlock)completionBlock {
NSString *tracks = @"tracks";
[asset loadValuesAsynchronouslyForKeys:@[tracks] completionHandler:^{ // 1
AVKeyValueStatus status = [asset statusOfValueForKey:tracks error:nil];
NSData *sampleData = nil;
if (status == AVKeyValueStatusLoaded) { // 2
sampleData = [self readAudioSamplesFromAsset:asset];
}
dispatch_async(dispatch_get_main_queue(), ^{ // 3
completionBlock(sampleData);
});
}];
}
+ (NSData *)readAudioSamplesFromAsset:(AVAsset *)asset {
// To be implemented
return nil;
}
@end
(1)首先對(duì)資源所需的鍵執(zhí)行標(biāo)準(zhǔn)的異步載入操作,這樣在訪問資源的tracks屬性時(shí)就不會(huì)遇到阻礙。
(2)如果tracks鍵 成功載入,則調(diào)用私有方法readAudioSamplesFromAsset:從資源音頻軌道中讀取樣本。
(3)由于載入操作可能發(fā)生在任意后臺(tái)隊(duì)列上,所以我們希望調(diào)度回主隊(duì)列,并調(diào)用帶有檢索到的音頻樣本的completion block,如果沒有讀取成功則為nil。
現(xiàn)在我們討論readAudioSamplesFromAsset:方法的實(shí)現(xiàn)。上一章討論了使用CMSampleBuffer訪問AVCaptureVideoDataOutput對(duì)象渲染的視頻幀。當(dāng)我們處理未壓縮的視頻數(shù)據(jù)時(shí),可以使用CMSampleBufferGetlmageBuffer函數(shù)檢索包含有幀的像素信息的基礎(chǔ)CVImageBufferRef。從一個(gè)資源中讀取音頻樣本時(shí),會(huì)再次用到CMSampleBuffer,不過本例基礎(chǔ)數(shù)據(jù)將會(huì)以Core Media類型CMBlockBuffer的形式提供。Block buffer用于在Core Media通道中傳送任意字節(jié)的數(shù)據(jù)。根據(jù)開發(fā)者對(duì)音頻樣本的使用目的,有多種方法可以訪問帶有音頻數(shù)據(jù)的block buffer。使用CMSampleBufferGetDataBuffer函 數(shù)得到一個(gè)到block buffer的不可保留(unretained)引用,這個(gè)方法適用于我們只需要訪問數(shù)據(jù)而不進(jìn)行后續(xù)處理的情況。相反,如果需要對(duì)音頻數(shù)據(jù)進(jìn)行處理,比如把它傳遞到Core Audio, 可以使用CMSample-BufferGetAudioBufferListWithRetainedBlockBuffer函數(shù)將數(shù)據(jù)作為AudioBufferList訪問。這會(huì)返回Core Audio的AudioBufferList,并帶有保留的CMBlockBuffer用于管理所包含的樣本的生命周期。由于我們已經(jīng)檢索了樣本并將它們復(fù)制到NSData中,就可以使用之前的方法檢索音頻數(shù)據(jù)了,如代碼清單8-3所示。
代碼清單8-3讀取資源的音頻樣本
+ (NSData *)readAudioSamplesFromAsset:(AVAsset *)asset {
NSError *error = nil;
AVAssetReader *assetReader = // 1
[[AVAssetReader alloc] initWithAsset:asset error:&error];
if (!assetReader) {
NSLog(@"Error creating asset reader: %@", [error localizedDescription]);
return nil;
}
AVAssetTrack *track = // 2
[[asset tracksWithMediaType:AVMediaTypeAudio] firstObject];
NSDictionary *outputSettings = @{ // 3
AVFormatIDKey : @(kAudioFormatLinearPCM),
AVLinearPCMIsBigEndianKey : @NO,
AVLinearPCMIsFloatKey : @NO,
AVLinearPCMBitDepthKey : @(16)
};
AVAssetReaderTrackOutput *trackOutput = // 4
[[AVAssetReaderTrackOutput alloc] initWithTrack:track
outputSettings:outputSettings];
[assetReader addOutput:trackOutput];
[assetReader startReading];
NSMutableData *sampleData = [NSMutableData data];
while (assetReader.status == AVAssetReaderStatusReading) {
CMSampleBufferRef sampleBuffer = [trackOutput copyNextSampleBuffer];// 5
if (sampleBuffer) {
CMBlockBufferRef blockBufferRef = // 6
CMSampleBufferGetDataBuffer(sampleBuffer);
size_t length = CMBlockBufferGetDataLength(blockBufferRef);
SInt16 sampleBytes[length];
CMBlockBufferCopyDataBytes(blockBufferRef, // 7
0,
length,
sampleBytes);
[sampleData appendBytes:sampleBytes length:length];
CMSampleBufferInvalidate(sampleBuffer); // 8
CFRelease(sampleBuffer);
}
}
if (assetReader.status == AVAssetReaderStatusCompleted) { // 9
return sampleData;
} else {
NSLog(@"Failed to read audio samples from asset");
return nil;
}
}
(1)創(chuàng)建一個(gè)新的AVAssetReader實(shí)例,并賦給它一個(gè)資源來讀取。如果在初始化對(duì)象時(shí)出錯(cuò),就將這個(gè)錯(cuò)誤信息打印到控制臺(tái)并返回nil。
(2)獲取資源中找到的第一個(gè)音頻軌道。包含在示例項(xiàng)目中的音頻文件只含有一個(gè)軌道,不過最好總是根據(jù)期望的媒體類型獲取軌道。
(3)創(chuàng)建一個(gè)NSDictionary來保存 從資源軌道讀取音頻樣本時(shí)使用的解壓設(shè)置。樣本需要以未壓縮的格式被讀取,所有我們指定kAudioFormatLinearPCM作為格式鍵。我們還希望確保以16位、lttle-endian字 節(jié)順序的有符號(hào)整型方式讀取。這些設(shè)置對(duì)于示例項(xiàng)目已經(jīng)足夠了,不過我們可以在AVAudioSettings.h文件中找到許多額外的鍵,它們可以對(duì)格式轉(zhuǎn)換進(jìn)行更詳細(xì)的控制。
(4)創(chuàng)建一個(gè)新的AVAssetReaderTrackOutput實(shí)例,并將上一步我們創(chuàng)建的輸出設(shè)置傳遞給它。將其作為AVAssetReader的輸出并調(diào)用startReading來允許資源讀取器開始預(yù)收取樣本數(shù)據(jù)。.
(5)調(diào)用跟蹤輸出的copyNextSampleBuffer方法開始每個(gè)迭代,每次都返回一個(gè)包含音頻樣本的下一個(gè)可用樣本buffer.
(6) CMSampleBuffer中 的音頻樣本被包含在一個(gè)CMBlockBuffer類型中。使用CMSampleBufferGetDataBuffer函數(shù)可以訪問這個(gè)block buffer。 使用CMBlockBufferGetDataLength函數(shù)確定其長度并創(chuàng)建一個(gè) 16位的帶符號(hào)整型數(shù)組來保存這些音頻樣本。
(7)使用CMBlockBufferCopyDataBytes函數(shù)生成一個(gè) 數(shù)組,數(shù)組中的元素為CMBlock-Buffer所包含的數(shù)據(jù),并將數(shù)組的內(nèi)容附加在NSData實(shí)例后。
(8)用CMSampleBufferInvalidate函 數(shù)來指定樣本buffer已經(jīng)處理和不可再繼續(xù)使用。此外,需要釋放CMSampleBuffer副本來釋放內(nèi)容。
(9)如果資源讀取器的status值等于AVAssetReaderStatusCompleted,則數(shù)據(jù)被成功讀取,返回包含音頻樣本數(shù)據(jù)的NSData即可。如果出現(xiàn)錯(cuò)誤,則返回nil。
第一步就這樣完成了,現(xiàn)在我們學(xué)會(huì)了如何從不同的音頻格式中成功讀取音頻樣本。下一步就是對(duì)數(shù)據(jù)進(jìn)行縮減以滿足可在屏幕上進(jìn)行繪制的要求。
8.2.2 縮減音頻樣本
THSampleDataProvider將從一個(gè)給定 的視頻資源中提取全部的樣本集合。即使是非常小的音頻文件,都可能有數(shù)十萬個(gè)樣本,遠(yuǎn)大于在屏幕上進(jìn)行繪制所需的樣本。我們需要定義一個(gè)篩選方法來得到最終在屏幕上呈現(xiàn)的值集合。要實(shí)現(xiàn)這一縮減操作,需要?jiǎng)?chuàng)建一個(gè)THSampleDataFilter對(duì)象。這個(gè)類的接口如代碼清單8-4所示。
代碼清單8-4 THSampleDataFilter 接口
@interface THSampleDataFilter : NSObject
- (id)initWithData:(NSData *)sampleData;
- (NSArray *)filteredSamplesForSize:(CGSize)size;
@end
用一個(gè)帶有音頻樣本信息的NSData來初始化這個(gè)類的實(shí)例。提供filteredSamplesForSize:方法按照指定的尺寸約束來篩選數(shù)據(jù)集。
共分兩步來處理這個(gè)數(shù)據(jù)。首先將樣本分成“箱”,找到每個(gè)箱里面的最大樣本。當(dāng)所有箱都處理完成后,對(duì)這些與傳遞給filteredSamplesForSize:方法的尺寸約束有關(guān)的樣本應(yīng)用比例因子。下面看下這個(gè)類的具體實(shí)現(xiàn),如代碼清單8-5所示。
代碼清單8-5 THSampleDataFilter 實(shí)現(xiàn)
#import "THSampleDataFilter.h"
@interface THSampleDataFilter ()
@property (nonatomic, strong) NSData *sampleData;
@end
@implementation THSampleDataFilter
- (id)initWithData:(NSData *)sampleData {
self = [super init];
if (self) {
_sampleData = sampleData;
}
return self;
}
- (NSArray *)filteredSamplesForSize:(CGSize)size {
NSMutableArray *filteredSamples = [[NSMutableArray alloc] init]; // 1
NSUInteger sampleCount = self.sampleData.length / sizeof(SInt16);
NSUInteger binSize = sampleCount / size.width;
SInt16 *bytes = (SInt16 *) self.sampleData.bytes;
SInt16 maxSample = 0;
for (NSUInteger i = 0; i < sampleCount; i += binSize) {
SInt16 sampleBin[binSize];
for (NSUInteger j = 0; j < binSize; j++) { // 2
sampleBin[j] = CFSwapInt16LittleToHost(bytes[i + j]);
}
SInt16 value = [self maxValueInArray:sampleBin ofSize:binSize]; // 3
[filteredSamples addObject:@(value)];
if (value > maxSample) { // 4
maxSample = value;
}
}
CGFloat scaleFactor = (size.height / 2) / maxSample; // 5
for (NSUInteger i = 0; i < filteredSamples.count; i++) {
filteredSamples[i] = @([filteredSamples[i] integerValue] * scaleFactor);
}
return filteredSamples;
}
- (SInt16)maxValueInArray:(SInt16[])values ofSize:(NSUInteger)size {
SInt16 maxValue = 0;
for (int i = 0; i < size; i++) {
if (abs(values[i]) > maxValue) {
maxValue = abs(values[I]);
}
}
return maxValue;
}
@end
(1)首先創(chuàng)建一個(gè)NSMutableArray來保存篩選的音頻樣本數(shù)組。我們還確定了要處理的樣本總數(shù)并計(jì)算與傳入方法的尺寸約束相應(yīng)的“箱”尺寸值。一個(gè)箱包含一個(gè)需要被篩選的樣本子集。
(2)迭代全部音頻樣本集合,在每個(gè)迭代中構(gòu)建一個(gè)需要處理的數(shù)據(jù)箱。當(dāng)處理音頻樣 本時(shí),要時(shí)刻記得字節(jié)的順序,所以用到了CFSwapInt16LittleToHost函數(shù)來確保樣本是按主機(jī)內(nèi)置的字節(jié)順序處理的。
(3)對(duì)于每個(gè)箱,調(diào)用maxValueInArray:方法找到最大樣本。 這個(gè)方法會(huì)迭代箱中的所有樣本并找到最大絕對(duì)值。結(jié)果值被添加到fiteredSamples數(shù)組。
(4)當(dāng)我們遍歷所有音頻樣本時(shí),在篩選結(jié)果中計(jì)算最大值。這個(gè)值作為我們篩選樣本所使用的比例因子。
(5)在返回篩選樣本前,需要相對(duì)于傳遞給方法的尺寸約束來縮放值。這會(huì)得到一個(gè)浮點(diǎn)值的數(shù)組,這些值可以在屏幕上呈現(xiàn)。當(dāng)這些值完成縮放后,就可以將數(shù)組返回給調(diào)用方法。
THSampleDataFilter類完成了,下 面我們準(zhǔn)備討論如何創(chuàng)建視圖來渲染這些音頻樣本。
8.2.3 渲染音頻樣本
我們需要?jiǎng)?chuàng)建一個(gè)UIView子類來渲染結(jié)果。下面先來看這個(gè)類的接口,如代碼清單8-6所示。
代碼清單8-6 THWaveformView 接口
@class AVAsset;
@interface THWaveformView : UIView
@property (strong, nonatomic) AVAsset *asset;
@property (strong, nonatomic) UIColor *waveColor;
@end
視圖提供了一個(gè)簡單接口來設(shè)置AVAsset和波形繪制時(shí)所用的顏色。下面看下這個(gè)類的具體實(shí)現(xiàn),如代碼清單8-7所示。一些UIView樣板代碼被我們省略掉了。要查看全部的實(shí)現(xiàn)代碼,可以去看項(xiàng)目源代碼。
代碼清單8-7實(shí)現(xiàn) setAsset:方法
#import "THWaveformView.h"
#import "THSampleDataProvider.h"
#import "THSampleDataFilter.h"
#import <QuartzCore/QuartzCore.h>
static const CGFloat THWidthScaling = 0.95;
static const CGFloat THHeightScaling = 0.85;
@interface THWaveformView ()
@property (strong, nonatomic) THSampleDataFilter *filter;
@property (strong, nonatomic) UIActivityIndicatorView *loadingView;
@end
@implementation THWaveformView
...
- (void)setAsset:(AVAsset *)asset {
if (_asset != asset) {
_asset = asset;
[THSampleDataProvider loadAudioSamplesFromAsset:self.asset // 1
completionBlock:^(NSData *sampleData) {
self.filter = // 2
[[THSampleDataFilter alloc] initWithData:sampleData];
[self.loadingView stopAnimating]; // 3
[self setNeedsDisplay];
}];
}
}
- (void)drawRect:(CGRect)rect {
// To be implemented
}
@end
(1)首先調(diào)用THSampleDataProvider類的loadAudioSamplesFromAssetcompletionBlock:方法開始載入音頻樣本。
(2)當(dāng)樣本被載入后,構(gòu)建一個(gè)新的THSampleDataFilter實(shí)例, 為其傳遞包含音頻樣本的NSData。
(3)將視圖的載入圖標(biāo)移除并調(diào)用setNeedsDisplay來對(duì)視圖進(jìn)行清理,此時(shí)會(huì)調(diào)用drawRect:方法。
下面看一下drawRect方法的實(shí)現(xiàn),學(xué)習(xí)數(shù)據(jù)是如何在屏幕上繪制的,如代碼清單8-8所示。
代碼清單8-8實(shí)現(xiàn) drawRect:方法
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextScaleCTM(context, THWidthScaling, THHeightScaling); // 1
CGFloat xOffset = self.bounds.size.width -
(self.bounds.size.width * THWidthScaling);
CGFloat yOffset = self.bounds.size.height -
(self.bounds.size.height * THHeightScaling);
CGContextTranslateCTM(context, xOffset / 2, yOffset / 2);
NSArray *filteredSamples = // 2
[self.filter filteredSamplesForSize:self.bounds.size];
CGFloat midY = CGRectGetMidY(rect);
CGMutablePathRef halfPath = CGPathCreateMutable(); // 3
CGPathMoveToPoint(halfPath, NULL, 0.0f, midY);
for (NSUInteger i = 0; i < filteredSamples.count; i++) {
float sample = [filteredSamples[i] floatValue];
CGPathAddLineToPoint(halfPath, NULL, i, midY - sample);
}
CGPathAddLineToPoint(halfPath, NULL, filteredSamples.count, midY);
CGMutablePathRef fullPath = CGPathCreateMutable(); // 4
CGPathAddPath(fullPath, NULL, halfPath);
CGAffineTransform transform = CGAffineTransformIdentity; // 5
transform = CGAffineTransformTranslate(transform, 0, CGRectGetHeight(rect));
transform = CGAffineTransformScale(transform, 1.0, -1.0);
CGPathAddPath(fullPath, &transform, halfPath);
CGContextAddPath(context, fullPath); // 6
CGContextSetFillColorWithColor(context, self.waveColor.CGColor);
CGContextDrawPath(context, kCGPathFill);
CGPathRelease(halfPath); // 7
CGPathRelease(fullPath);
}
(1)我們希望在視圖內(nèi)呈現(xiàn)這個(gè)波形,所以首先基于定義的高和寬常量來縮放圖像上下文。還要計(jì)算x和y偏移量,轉(zhuǎn)換上下文,在縮放上下文中適當(dāng)調(diào)整偏移。
(2)從THSampleDataFilter實(shí)例 獲取篩選的樣本,并傳遞視圖邊界的尺寸。在實(shí)際代碼中,開發(fā)者可能希望在drawRec:方法之外執(zhí)行這一檢索操作,這樣在篩選樣本時(shí)會(huì)有更好的優(yōu)化效果。不過在本例中,上面的方法已經(jīng)足以滿足要求了。
(3)創(chuàng)建一個(gè)新的CGMutablePathRef,用來繪制波形Bezier路徑的上半部。迭代篩選的樣本,對(duì)每次迭代調(diào)用CGPathAddLineToPoint向路徑中添加一個(gè)點(diǎn)。利用循環(huán)索引作為x坐標(biāo),樣本值作為y坐標(biāo)。
(4)創(chuàng)建第二個(gè)CGMutablePathRef,傳遞第4步構(gòu)建的Bezier路徑。使用這個(gè)Bezier路徑繪制完整的波形。
(5)要繪制波形的下半部,需要對(duì)上半部路徑應(yīng)用translate和Iscale變化。這會(huì)使得上半部路徑翻轉(zhuǎn)到下面,填滿整個(gè)波形。
(6)將完整的路徑添加到圖像上下文,根據(jù)指定的waveColor值設(shè)置填充顏色,并調(diào)用CGContextDrawPath(context, kCGPathFill)將填充好的路徑繪制到圖像上下文。
(7)每當(dāng)創(chuàng)建Quartz對(duì)象后,開發(fā)者都有責(zé)任在使用完之后釋放相應(yīng)的內(nèi)存,所以最后一.步就是在創(chuàng)建的路徑對(duì)象上調(diào)用CGPathRelease。
應(yīng)用程序的視圖控制器用來繪制兩個(gè)視圖,如圖8-5所示。可以打開應(yīng)用程序的視圖控制器并嘗試其他顏色,或在Estoryboard中修改視圖。 現(xiàn)在,如果在應(yīng)用程序中需要用到波形器,就可以重用我們本章的類了。
8.3 捕捉錄制的高級(jí)方法
上一章最后討論了將AVCaptureVideoDataOutput捕捉的CVPixelBuffer對(duì)象作為OpenGLES的貼圖來呈現(xiàn)。這是一個(gè)非常強(qiáng)大的功能,可以使我們開發(fā)出很多有趣的應(yīng)用程序。不過使用AVCaptureVideoDataOutput的一一個(gè)問題在于會(huì)失去AVCaptureMovieFileOutput來記錄輸出 的便捷性。無論多么神奇的特效,如果應(yīng)用程序無法記錄輸出內(nèi)容,并把它分享到全世界,那都是嚴(yán)重的缺陷。在本節(jié)中,我們會(huì)學(xué)到如何使用AVAssetWriter創(chuàng)建一個(gè)與AVCapture-MovieFileOutput類似的可重用類從高級(jí)捕捉輸出中記錄輸出。
看一下,上一章介紹的有關(guān)OpenGL ES的應(yīng)用。我們可以重用CubeKamera應(yīng)用程序,不過為了避免重復(fù),我們創(chuàng)建一個(gè)新的相機(jī)應(yīng)用程序,使用Core Image框架 來處理視頻幀應(yīng)用實(shí)時(shí)視頻效果。
在Chapter 8目錄中可以找到名為KameraWriter_Starter的示例項(xiàng)目。KameraWriter是一個(gè)帶有實(shí)時(shí)效果的視頻錄制應(yīng)用程序。用戶可從屏幕最上的篩選器選擇可用的效果。如圖8-6所示。

從iOS 7開始,內(nèi)置的相機(jī)應(yīng)用程序在拍攝靜態(tài)圖片時(shí)就可以應(yīng)用篩選器了。這些篩選器可以在Core Image框架中找到,并且在最新的iOS設(shè)備中它們還可用于將實(shí)時(shí)視頻效果添加到應(yīng)用程序中。Core Image框架的詳細(xì)內(nèi)容不在本書的討論范圍內(nèi),不過在開發(fā)本章這個(gè)功能時(shí),也與它進(jìn)行最小限度的交互。
第一步創(chuàng)建攝像頭控制器。我們創(chuàng)建一個(gè)與 上一章一樣的基礎(chǔ)結(jié)構(gòu),這樣可以減少在創(chuàng)建過程中樣板的代碼量。
先從THCameraController接口開始,如代碼清單8-9所示。
代碼清單8-9 THCameraController 接口
#import "THImageTarget.h"
#import "THBaseCameraController.h"
@interface THCameraController : THBaseCameraController
- (void)startRecording;
- (void)stopRecording;
@property (nonatomic, getter = isRecording) BOOL recording;
@property (weak, nonatomic) id <THImageTarget> imageTarget;
@end
這個(gè)接口定義了開始錄制、停止錄制以及確定錄制狀態(tài)的方法。還提供了一個(gè)imageTarget屬性,作為Core Image的CIImage對(duì)象的可視化輸出。
下一步就是完成攝像頭控制器的實(shí)現(xiàn),首先配置捕捉會(huì)話輸出,如代碼清單8- 10所示。
代碼清單8-10配置會(huì)話輸出
#import "THCameraController.h"
#import <AVFoundation/AVFoundation.h>
@interface THCameraController () <AVCaptureVideoDataOutputSampleBufferDelegate,
AVCaptureAudioDataOutputSampleBufferDelegate>
@property (strong, nonatomic) AVCaptureVideoDataOutput *videoDataOutput;
@property (strong, nonatomic) AVCaptureAudioDataOutput *audioDataOutput;
@end
@implementation THCameraController
- (BOOL)setupSessionOutputs:(NSError **)error {
self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init]; // 1
NSDictionary *outputSettings =
@{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};
self.videoDataOutput.videoSettings = outputSettings;
self.videoDataOutput.alwaysDiscardsLateVideoFrames = NO; // 2
[self.videoDataOutput setSampleBufferDelegate:self
queue:self.dispatchQueue];
if ([self.captureSession canAddOutput:self.videoDataOutput]) {
[self.captureSession addOutput:self.videoDataOutput];
} else {
return NO;
}
self.audioDataOutput = [[AVCaptureAudioDataOutput alloc] init]; // 3
[self.audioDataOutput setSampleBufferDelegate:self
queue:self.dispatchQueue];
if ([self.captureSession canAddOutput:self.audioDataOutput]) {
[self.captureSession addOutput:self.audioDataOutput];
} else {
return NO;
}
return YES;
}
- (NSString *)sessionPreset { // 4
return AVCaptureSessionPresetMedium;
}
- (void)startRecording {
// To be implemented
}
- (void)stopRecording {
// To be implemented
}
#pragma mark - Delegate methods
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
// To be implemented
}
}
@end
(1)首先創(chuàng)建一個(gè)新的AVCaptureVideoDataOutput。用上一章的方法對(duì)它進(jìn)行配置,設(shè)置它的輸出格式為kCVPixelFormatType_32BGRA。當(dāng)結(jié)合OpenGL ES和Corelmage時(shí)這一格式非常適合。
(2)設(shè)置alwaysDiscardsL ateVideoFrames為NO。由于我們要記錄輸出內(nèi)容,所以通常我們希望捕捉全部的可用幀。設(shè)置這個(gè)屬性為NO會(huì)給委托方法一些額外的時(shí)間來處理樣本buffer,不過這會(huì)增加內(nèi)存消耗。不過在處理每個(gè)樣本buffer時(shí)都要做到盡可能高效,這樣才能保障實(shí)時(shí)性能。
(3)還創(chuàng)建了一個(gè)新的AVCaptureAudioDataOutput實(shí)例。這個(gè)類是AVCaptureVideoDataOutput的兄弟類,用于從帶有活動(dòng)AVCaptureSession的音頻設(shè)備中捕捉音頻樣本。
(4)設(shè)置會(huì)話預(yù)設(shè)值為AVCaptureSessionPresetMedium。開發(fā)這個(gè)應(yīng)用程序時(shí)最好從一個(gè)簡單的預(yù)設(shè)值開始,等我們熟悉了更多功能后,再一點(diǎn)點(diǎn)增加難度來滿足更高級(jí)的需求。
捕捉會(huì)話的輸出配置完畢后,就需要實(shí)現(xiàn)委托回調(diào)方法了,如代碼清單8-11所示。
代碼清單8-11捕捉輸出委托
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
if (captureOutput == self.videoDataOutput) { // 1
CVPixelBufferRef imageBuffer = // 2
CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage *sourceImage = // 3
[CIImage imageWithCVPixelBuffer:imageBuffer options:nil];
[self.imageTarget setImage:sourceImage];
}
}
(1) captureOutput:didOutputSampleBuffer:fromConnection:方法是兩類捕捉輸出的委托回調(diào)。目前我們只處理視頻樣本,所以我們僅限于處理來自AVCaptureVideoDataOutput實(shí)例的回調(diào)。
(2)使用CMSampleBufferGetImageBuffer函 數(shù)從樣本buffer中獲取基礎(chǔ)CVPixelBuffer。
(3)從CVPixelBuffer中創(chuàng)建一個(gè)新的CIlmage, 并將它傳遞給需要在屏幕上呈現(xiàn)的圖片目標(biāo)。
基本的捕捉功能已經(jīng)完成了,現(xiàn)在可以在設(shè)備上編譯和運(yùn)行應(yīng)用程序來實(shí)際體驗(yàn)篩選的功能。點(diǎn)擊篩選器名稱旁邊的箭頭來進(jìn)行切換?,F(xiàn)在我們已經(jīng)成功開發(fā)了一個(gè)應(yīng)用于視頻幀的實(shí)時(shí)視頻篩選器,感謝Core Image框架的幫助!
注意:
示例應(yīng)用程序?yàn)锳VCaptureVideoDataOutput和AVCaptureAudioDataOutput兩個(gè)實(shí)例使用了一個(gè)調(diào)度隊(duì)列。這對(duì)于我們的示例應(yīng)用程序來說是足夠的,不過如果希望對(duì)數(shù)據(jù)進(jìn)行更復(fù)雜的處理,可能需要考慮為每一個(gè)使用單獨(dú)的隊(duì)列。蘋果公司有- -個(gè)示例應(yīng)用程序稱為RosyWriter(在ADC上可用)用的就是這種方法。它還給出了一些供有效處理CMSampleBuffers的更高級(jí)性能選項(xiàng)。
KameraWriter應(yīng)用程序看起來還不錯(cuò),基本功能都也都實(shí)現(xiàn)了,不過還不能記錄輸出內(nèi)容。要解決這一問題, 需要?jiǎng)?chuàng)建一個(gè)THMovieWriter對(duì)象,該對(duì)象和AVCaptureMoviceFileOutput的功能類似,不過它使用AVAssetWriter來執(zhí)行視頻編碼和文件寫入。下面看一下這個(gè)對(duì)象的接口定義,如代碼清單8-12所示。
代碼清單8-12 THMovieWriter 接口
#import <AVFoundation/AVFoundation.h>
@protocol THMovieWriterDelegate <NSObject>
- (void)didWriteMovieAtURL:(NSURL *)outputURL;
@end
@interface THMovieWriter : NSObject
- (id)initWithVideoSettings:(NSDictionary *)videoSettings // 1
audioSettings:(NSDictionary *)audioSettings
dispatchQueue:(dispatch_queue_t)dispatchQueue;
- (void)startWriting;
- (void)stopWriting;
@property (nonatomic) BOOL isWriting;
@property (weak, nonatomic) id<THMovieWriterDelegate> delegate; // 2
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer; // 3
@end
(1) THMovieWriter實(shí)例帶有兩個(gè)字典,用來描述基礎(chǔ)AVAssetWriter實(shí)例的配置參數(shù)和調(diào)度隊(duì)列。定義了寫入進(jìn)程的開始和停止方法及監(jiān)聽其工作狀態(tài)的方法。
(2)這個(gè)類定義了一個(gè)委托協(xié)議THMovieWriterDelegate來表示影片文件什么時(shí)候被寫入 磁盤,之后委托會(huì)收到通知并采取相應(yīng)的行動(dòng)。
(3) THMovieWriter的一個(gè)關(guān)鍵 方法是processSampleBuffer,每當(dāng)有新的樣本被捕捉輸出對(duì)象捕捉到時(shí),都會(huì)調(diào)用這個(gè)方法。
對(duì)于AVAssetWriter,即使是簡單的示例也會(huì)比較復(fù)雜。所以我們將這一開發(fā)過程分解成一些小段。首先我們處理有關(guān)生命周期的方法,如代碼清單8-13所示。
代碼清單8-13 THMovieWriter 生命周期方法
#import "THMovieWriter.h"
#import <AVFoundation/AVFoundation.h>
#import "THContextManager.h"
#import "THFunctions.h"
#import "THPhotoFilters.h"
#import "THNotifications.h"
static NSString *const THVideoFilename = @"movie.mov";
@interface THMovieWriter ()
@property (strong, nonatomic) AVAssetWriter *assetWriter; // 1
@property (strong, nonatomic) AVAssetWriterInput *assetWriterVideoInput;
@property (strong, nonatomic) AVAssetWriterInput *assetWriterAudioInput;
@property (strong, nonatomic)
AVAssetWriterInputPixelBufferAdaptor *assetWriterInputPixelBufferAdaptor;
@property (strong, nonatomic) dispatch_queue_t dispatchQueue;
@property (weak, nonatomic) CIContext *ciContext;
@property (nonatomic) CGColorSpaceRef colorSpace;
@property (strong, nonatomic) CIFilter *activeFilter;
@property (strong, nonatomic) NSDictionary *videoSettings;
@property (strong, nonatomic) NSDictionary *audioSettings;
@property (nonatomic) BOOL firstSample;
@end
@implementation THMovieWriter
- (id)initWithVideoSettings:(NSDictionary *)videoSettings
audioSettings:(NSDictionary *)audioSettings
dispatchQueue:(dispatch_queue_t)dispatchQueue {
self = [super init];
if (self) {
_videoSettings = videoSettings;
_audioSettings = audioSettings;
_dispatchQueue = dispatchQueue;
_ciContext = [THContextManager sharedInstance].ciContext; // 2
_colorSpace = CGColorSpaceCreateDeviceRGB();
_activeFilter = [THPhotoFilters defaultFilter];
_firstSample = YES;
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; // 3
[nc addObserver:self
selector:@selector(filterChanged:)
name:THFilterSelectionChangedNotification
object:nil];
}
return self;
}
- (void)dealloc {
CGColorSpaceRelease(_colorSpace);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)filterChanged:(NSNotification *)notification {
self.activeFilter = [notification.object copy];
}
- (void)startWriting {
// To be implemented
}
- (void) processSampleBuffer:(CMSampleBufferRef)sampleBuffer
mediaType:(CMMediaType)mediaType {
// To be implemented
}
- (void)stopWriting {
// To be implemented
}
- (NSURL *)outputURL { // 4
NSString *filePath =
[NSTemporaryDirectory() stringByAppendingPathComponent:THVideoFilename];
NSURL *url = [NSURL fileURLWithPath:filePath];
if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
}
return url;
}
@end
(1)在類擴(kuò)展中我們?yōu)锳VAssetWriter創(chuàng)建了一些屬性和相關(guān)的對(duì) 象。每當(dāng)startWriting方法被調(diào)用時(shí),就創(chuàng)建對(duì)象的圖片并在寫入會(huì)話的持續(xù)時(shí)間內(nèi)保持對(duì)它們的強(qiáng)引用關(guān)系。
(2)從THContextManager對(duì)象得到分享的Core Image上下文。這個(gè)對(duì)象受OpenGL ES的支持并用于篩選傳進(jìn)來的視頻樣本,最后得到一一個(gè)CVPixelBuffer。
(3)注冊(cè)THFilterSelectionChangedNotification通 知的監(jiān)聽器。當(dāng)用戶切換可用篩選器列表時(shí)就會(huì)從用戶界面發(fā)送該通知。每當(dāng)通知發(fā)送時(shí),filterChanged:方法就 會(huì)被調(diào)用,并相應(yīng)地 更新activeFilter屬性。
(4)定義一個(gè)outputURL方法來配置AVAssetWriter實(shí)例。這個(gè)方法在臨時(shí)目錄中定義了一個(gè)NSURL,并將之前的同名文件刪除。
有關(guān)生命周期的配置完成后,我們?cè)賮砜匆幌聅tartWriting方法的實(shí)現(xiàn),如代碼清單8-14所示。
代碼清單8-14設(shè)置AVAssetWriter圖片
- (void)startWriting {
dispatch_async(self.dispatchQueue, ^{ // 1
NSError *error = nil;
NSString *fileType = AVFileTypeQuickTimeMovie;
self.assetWriter = // 2
[AVAssetWriter assetWriterWithURL:[self outputURL]
fileType:fileType
error:&error];
if (!self.assetWriter || error) {
NSString *formatString = @"Could not create AVAssetWriter: %@";
NSLog(@"%@", [NSString stringWithFormat:formatString, error]);
return;
}
self.assetWriterVideoInput = // 3
[[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo
outputSettings:self.videoSettings];
self.assetWriterVideoInput.expectsMediaDataInRealTime = YES;
UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
self.assetWriterVideoInput.transform = // 4
THTransformForDeviceOrientation(orientation);
NSDictionary *attributes = @{ // 5
(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
(id)kCVPixelBufferWidthKey : self.videoSettings[AVVideoWidthKey],
(id)kCVPixelBufferHeightKey : self.videoSettings[AVVideoHeightKey],
(id)kCVPixelFormatOpenGLESCompatibility : (id)kCFBooleanTrue
};
self.assetWriterInputPixelBufferAdaptor = // 6
[[AVAssetWriterInputPixelBufferAdaptor alloc]
initWithAssetWriterInput:self.assetWriterVideoInput
sourcePixelBufferAttributes:attributes];
if ([self.assetWriter canAddInput:self.assetWriterVideoInput]) { // 7
[self.assetWriter addInput:self.assetWriterVideoInput];
} else {
NSLog(@"Unable to add video input.");
return;
}
self.assetWriterAudioInput = // 8
[[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio
outputSettings:self.audioSettings];
self.assetWriterAudioInput.expectsMediaDataInRealTime = YES;
if ([self.assetWriter canAddInput:self.assetWriterAudioInput]) { // 9
[self.assetWriter addInput:self.assetWriterAudioInput];
} else {
NSLog(@"Unable to add audio input.");
}
self.isWriting = YES; // 10
self.firstSample = YES;
});
}
(1)讓用戶點(diǎn)擊Record按鈕是為了避免卡頓,以異步方式調(diào)度到dispatchQueue隊(duì)列,以便設(shè)置AVAssetWriter對(duì)象。
(2)創(chuàng)建一個(gè)新的AVAssetWriter實(shí)例,將寫入目的文件的輸出URL、文件類型常量和一個(gè)NSError傳遞給它。在創(chuàng)建對(duì)象時(shí)如果出現(xiàn)錯(cuò)誤,則將錯(cuò)誤信息輸出到控制臺(tái)并返回。
(3)創(chuàng)建一個(gè)新的AVAssetWriterInput,以附加從AVCaptureVideoDataOuput中得到的樣本。傳遞給初始化方法一個(gè) AVMediaTypeVideo媒體類型和創(chuàng)建THMovieWriter的視頻設(shè)置。設(shè)置expectsMediaDataInRealTime屬性為YES來指明這個(gè)輸入應(yīng)該針對(duì)實(shí)時(shí)性進(jìn)行優(yōu)化。
(4)應(yīng)用程序的用戶界面鎖定為垂直方向,不過我們希望捕捉應(yīng)該可以支持任何方向。判斷用戶界面的方向并使用THTransformForDeviceOrientation函數(shù)為輸入設(shè)置一個(gè) 合適的轉(zhuǎn)換。在寫入會(huì)話期間,方向會(huì)按照這一設(shè)定保持不變。
(5) 定義屬性的NSDictionary用于配置將在下一步中創(chuàng)建的AVAssetWriterInput-PixelIBufferAdaptor。要保證最大效率,字典中的值應(yīng)該對(duì)應(yīng)于在配置AVCaptureVideoData-Output時(shí)所使用的原像素格式。
(6)創(chuàng)建一個(gè)新的AVAssetWriterInputPixelBufferAdaptor,傳遞上一步中創(chuàng)建的屬性給它。這個(gè)對(duì)象提供了一個(gè)優(yōu)化的CVPixelBufferPool,使用它可以創(chuàng)建CVPixelBuffer對(duì)象來渲染篩選視頻幀。
(7)將視頻輸入添加到資源寫入器,如果輸入不能被添加,將錯(cuò)誤信息輸出到控制臺(tái)并返回。
(8)創(chuàng)建AVAssetWriterInput,用于附加來自AVCaptureAudioDataOutput的樣本。給初始化方法傳遞一個(gè)AVMediaTypeAudio媒體類型和創(chuàng)建THMovieWriter的音頻設(shè)置。設(shè)置expectsMediaDatalnRealTime屬性為YES來指明這個(gè)輸入應(yīng)該針對(duì)實(shí)時(shí)性進(jìn)行優(yōu)化。
(9)將音頻輸入添加到資源寫入器。如果輸入不能被添加,則將錯(cuò)誤信息輸出到控制臺(tái)并返回。
(10)設(shè)置isWriting 和firstSample屬性為YES,就可以開始附加樣本了。
接下來,下面看一下procesSampleBuffer:方法的實(shí)現(xiàn),這個(gè)方法里我們將附加從捕捉輸出得到的CMSampleBuffer對(duì)象,如代碼清單8-15所示。
代碼清單8-15 處理樣本 buffer
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer {
if (!self.isWriting) {
return;
}
CMFormatDescriptionRef formatDesc = // 1
CMSampleBufferGetFormatDescription(sampleBuffer);
CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc);
if (mediaType == kCMMediaType_Video) {
CMTime timestamp =
CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
if (self.firstSample) { // 2
if ([self.assetWriter startWriting]) {
[self.assetWriter startSessionAtSourceTime:timestamp];
} else {
NSLog(@"Failed to start writing.");
}
self.firstSample = NO;
}
CVPixelBufferRef outputRenderBuffer = NULL;
CVPixelBufferPoolRef pixelBufferPool =
self.assetWriterInputPixelBufferAdaptor.pixelBufferPool;
OSStatus err = CVPixelBufferPoolCreatePixelBuffer(NULL, // 3
pixelBufferPool,
&outputRenderBuffer);
if (err) {
NSLog(@"Unable to obtain a pixel buffer from the pool.");
return;
}
CVPixelBufferRef imageBuffer = // 4
CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:imageBuffer
options:nil];
[self.activeFilter setValue:sourceImage forKey:kCIInputImageKey];
CIImage *filteredImage = self.activeFilter.outputImage;
if (!filteredImage) {
filteredImage = sourceImage;
}
[self.ciContext render:filteredImage // 5
toCVPixelBuffer:outputRenderBuffer
bounds:filteredImage.extent
colorSpace:self.colorSpace];
if (self.assetWriterVideoInput.readyForMoreMediaData) { // 6
if (![self.assetWriterInputPixelBufferAdaptor
appendPixelBuffer:outputRenderBuffer
withPresentationTime:timestamp]) {
NSLog(@"Error appending pixel buffer.");
}
}
CVPixelBufferRelease(outputRenderBuffer);
}
else if (!self.firstSample && mediaType == kCMMediaType_Audio) { // 7
if (self.assetWriterAudioInput.isReadyForMoreMediaData) {
if (![self.assetWriterAudioInput appendSampleBuffer:sampleBuffer]) {
NSLog(@"Error appending audio sample buffer.");
}
}
}
}
(1)這個(gè)方法可以處理音頻和視頻兩類樣本,所以我們需要確定樣本的媒體類型才能附加到正確的寫入器輸入。查看樣本buffer的CMFormatDescription并使用CMFormatDescriptionGetMediaType方法來判斷它的媒體類型。
(2)如果當(dāng)用戶點(diǎn)擊Record按鈕后正在處理的是第一個(gè) 視頻樣本,則調(diào)用資源寫入器的startWriting和startSessionAtSourceTime:方法啟動(dòng)一個(gè)新的寫入會(huì)話,將樣本的呈現(xiàn)時(shí)間作為源時(shí)間傳遞到方法中。
(3)從像素buffer適配器池中創(chuàng)建一個(gè)空的CVPixelBuffer, 使用該像素buffer渲染篩選好的視頻幀的輸出。
(4)使用CMSampleBufferGetImageBuffer函 數(shù)獲取當(dāng)前視頻樣本的CVPixelBuffer。根據(jù)像素buffer創(chuàng)建-一個(gè)新的CIlmage并將它設(shè)置為活動(dòng)篩選器的kCIInputImageKey值。通過篩選器得到輸出圖片,會(huì)返回一個(gè)封裝了CIFilter操作的CIlmage對(duì)象。如果因?yàn)槟撤N原因filteredIlmage為nil,則設(shè)置CIImage的引用為原始的sourceImage。
(5)將篩選好的CIlmage的輸出渲染到第3步創(chuàng)建的CVPixelBuffer中。
(6)如果視頻輸入的readyForMoreMediaData屬性為YES,則將像素buffer連同當(dāng)前樣本的呈現(xiàn)時(shí)間都附加到AVAssetWriterPixelBufferAdaptor。現(xiàn)在就完成了對(duì)當(dāng)前視頻樣本的處理,所以此時(shí)應(yīng)該調(diào)用CVPixelBufferRelease函數(shù)釋放像素buffer。
(7)如果第一個(gè)樣本處理完成并且當(dāng)前的CMSampleBuffer是一個(gè)音頻樣本,則詢問音頻AVAssetWriterInput是否準(zhǔn)備接收更多的數(shù)據(jù)。如果可以,則將它添加到輸入。
processSampleBuffer:方法就完成了,最后一個(gè)需要實(shí)現(xiàn)的功能是stopWriting方法,如代碼清單8- 16所示。
代碼清單8-16完成寫入會(huì)話
- (void)stopWriting {
self.isWriting = NO; // 1
dispatch_async(self.dispatchQueue, ^{
[self.assetWriter finishWritingWithCompletionHandler:^{ // 2
if (self.assetWriter.status == AVAssetWriterStatusCompleted) {
dispatch_async(dispatch_get_main_queue(), ^{ // 3
NSURL *fileURL = [self.assetWriter outputURL];
[self.delegate didWriteMovieAtURL:fileURL];
});
} else {
NSLog(@"Failed to write movie: %@", self.assetWriter.error);
}
}];
});
}
(1)設(shè)置isWriting的標(biāo)志為NO,這樣processSampleBuffer:mediaType:方法就不會(huì)再處理更多的樣本。
(2)調(diào)用finishWritingWithCompletionHandler:方法來終 止寫入會(huì)話并關(guān)閉磁盤上的文件。
(3)判斷資源寫入器的狀態(tài)。如果status等 于AVAssetWriterStatusCompleted,則表示文件成功寫入,調(diào)度回到主線程調(diào)用委托的didWriteMovieAtURL:方法。如果status等 于其他值,則將資源寫入器的錯(cuò)誤信息輸出到控制臺(tái)。
THMovieWriter類的實(shí)現(xiàn)就全部完成了,最后一步就是將它整合到THCameraController中,如代碼清單8-17所示。
代碼清單8-17應(yīng)用THMovieWriter
#import "THCameraController.h"
#import <AVFoundation/AVFoundation.h>
#import "THMovieWriter.h"
#import <AssetsLibrary/AssetsLibrary.h>
@interface THCameraController () <AVCaptureVideoDataOutputSampleBufferDelegate,
AVCaptureAudioDataOutputSampleBufferDelegate,
THMovieWriterDelegate>
@property (strong, nonatomic) AVCaptureVideoDataOutput *videoDataOutput;
@property (strong, nonatomic) AVCaptureAudioDataOutput *audioDataOutput;
@property (strong, nonatomic) THMovieWriter *movieWriter; // 1
@end
@implementation THCameraController
- (BOOL)setupSessionOutputs:(NSError **)error {
// AVCaptureVideoDataOutput and AVCaptureAudioDataOutput set up and
// configuration previously covered in Listing 8.x
NSString *fileType = AVFileTypeQuickTimeMovie;
NSDictionary *videoSettings = // 2
[self.videoDataOutput
recommendedVideoSettingsForAssetWriterWithOutputFileType:fileType];
NSDictionary *audioSettings = [self.audioDataOutput
recommendedAudioSettingsForAssetWriterWithOutputFileType:fileType];
self.movieWriter = // 3
[[THMovieWriter alloc] initWithVideoSettings:videoSettings
audioSettings:audioSettings
dispatchQueue:self.dispatchQueue];
self.movieWriter.delegate = self;
return YES;
}
- (NSString *)sessionPreset {
return AVCaptureSessionPreset1280x720;
}
- (void)startRecording { // 4
[self.movieWriter startWriting];
self.recording = YES;
}
- (void)stopRecording {
[self.movieWriter stopWriting];
self.recording = NO;
}
#pragma mark - Delegate methods
- (void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
[self.movieWriter processSampleBuffer:sampleBuffer]; // 5
if (captureOutput == self.videoDataOutput) {
CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage *sourceImage = [CIImage imageWithCVPixelBuffer:imageBuffer options:nil];
[self.imageTarget setImage:sourceImage];
}
}
- (void)didWriteMovieAtURL:(NSURL *)outputURL { // 6
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:outputURL]) {
ALAssetsLibraryWriteVideoCompletionBlock completionBlock;
completionBlock = ^(NSURL *assetURL, NSError *error){
if (error) {
[self.delegate assetLibraryWriteFailedWithError:error];
}
};
[library writeVideoAtPathToSavedPhotosAlbum:outputURL
completionBlock:completionBlock];
}
}
@end
(1)創(chuàng)建一個(gè)新屬性,保存關(guān)于THMovieWriter對(duì)象的強(qiáng)引用。同時(shí)需要類遵循THMovieWriterDelegate協(xié)議。
(2) iOs 7版本中引入了一些便捷方法讓開發(fā)者可以簡單地創(chuàng)建帶有推薦音頻和視頻設(shè)置的字典對(duì)象,這些設(shè)置都針對(duì)配置AVAssetWriter需要的文件類型。調(diào)用這些方法并創(chuàng)建傳遞到THMovieWriter中的字典。
(3)創(chuàng)建一個(gè)新的THMovieWriter實(shí)例,傳遞配置字典和控制器的dispatchQueue的引用,該引用是在它的超類(THBaseCameraController)中定義的。將該控制器作為影片寫入器的委托。
(4)實(shí)現(xiàn)startRecording和stopRecording方法, 并在THMovieWriter.上調(diào)用相應(yīng)的方法。對(duì)每個(gè)方法都按照需要更新recording狀態(tài)。
(5)調(diào)用影片寫入器的processSampleBuffer:方法,將當(dāng)前CMSampleBuffer和媒體類型傳遞給方法。
(6)實(shí)現(xiàn)委托協(xié)議的didWriteMovieAtURL:方法。 使用前面介紹過的ALAssetsLibrary框架將最新創(chuàng)建的影片寫入到用戶的Photos library。
現(xiàn)在可以運(yùn)行應(yīng)用程序并錄制視頻了。打開應(yīng)用程序并點(diǎn)擊Record按鈕,當(dāng)錄制進(jìn)行時(shí),可以點(diǎn)擊可用的篩選器。再次點(diǎn)擊Record按鈕可以停止錄制??梢郧袚Q到iOS相機(jī)應(yīng)用程序體驗(yàn)這個(gè)視頻帶來的喜悅吧。
8.4 小結(jié)
本章主要學(xué)習(xí)了AVAssetReader和AVAssetWriter所提供的強(qiáng)大功能。這些類都是AVFoundation在處理媒體資源對(duì)象時(shí)的底層方法,在許多高級(jí)用例中都起到了至關(guān)重要的作用。雖然在CMSampleBuffer對(duì)象一級(jí)處理媒體比較復(fù)雜, 但是它可以提供僅使用框架頂層功能無法實(shí)現(xiàn)的一些高級(jí)功能。精通AVAssetReader和AVAssetWriter的用法需要一定的時(shí)間,不過本章的主題以及示例項(xiàng)目可以為開發(fā)者創(chuàng)建自己的應(yīng)用程序奠定一個(gè) 良好的基礎(chǔ)。