在前面【iOS】自定義相機(jī)(六)拍照錄像中伶棒,我們介紹了如何使用AVCaptureMovieFileOutput
進(jìn)行視頻的錄制糯而。AVCaptureMovieFileOutput
雖然十分方便,但是我們無法使用AVCaptureVideoDataOutput
獲取幀圖片進(jìn)行處理坤候。因此這一篇文章就介紹一下直接使用AVCaptureVideoDataOutput
和AVCaptureAudioDataOutput
進(jìn)行視頻的錄制的姿勢。
對應(yīng)的代碼在SCCamera中的SCMovieManager.m和SCCameraController.m中歉胶。
在AVCaptureVideoDataOutputSampleBufferDelegate
和AVCaptureAudioDataOutputSampleBufferDelegate
的captureOutput:didOutputSampleBuffer:fromConnection:
中我們可以獲取到音視頻數(shù)據(jù)CMSampleBufferRef
婿斥。通過使用AVAssetWriter
我們就能達(dá)到劝篷,將每一幀的視頻和音頻寫入文件的目的。從而模擬出AVCaptureMovieFileOutput
的效果民宿。
AVAssetWriter 使用
AVFoundation 中提供了直接讀寫視頻媒體樣本的類娇妓,分別為AVAssetReader與AVAssetWriter。AVAssetReader 用于將資源從 AVAsset 實(shí)例中讀取媒體樣本活鹰,AVAssetWriter 用于對媒體資源進(jìn)行編碼并將其寫入到容器文件中哈恰。
AVAssetWriter
是一個用于寫入資源的類,他由一個或多個AVAssetWriterInput
組成志群,用于添加要寫入容器的特定的媒體樣本的CMSampleBuffer
着绷。AVAssetWriterInput
需要配置指定的媒體類型,比如音頻或視頻锌云。對于自定義相機(jī)的視頻錄制荠医,AVAssetWriter
的使用如下:
AVAssetWriter
的創(chuàng)建十分簡單,只需要指定目標(biāo)容器的NSURL
即可。重點(diǎn)需要注意的地方是AVAssetWriterInput
:
- 創(chuàng)建
AVAssetWriterInput
需要的outputSettings
- 音視頻樣本寫入模式
outputSettings
outputSettings
是一個NSDictionary<NSString *, id>
彬向,用鍵值對的方式表示音視頻編碼與壓縮相關(guān)信息豫喧,例子如下:
NSDictionary *outSettings = @{
AVVideoCodecKey: AVVideoCodecH264,
AVVideoWidthKey: @1280,
AVVideoHeightKey: @720,
AVVideoCompressionPropertiesKey: @{
AVVideoMaxKeyFrameIntervalKey: @1,
AVVideoAverageBitRateKey: @105000000,
AVVideoProfileLevelKey: AVVideoProfileLevelH264Main31
}
}
逐個鍵值對進(jìn)行設(shè)置可控性比較強(qiáng),但容易寫錯幢泼。方便起見紧显,我們可以使用AVCaptureVideoDataOutput
和AVCaptureAudioDataOutput
的recommendedVideoSettingsForAssetWriterWithOutputFileType:
方法。只需要傳入我們需要導(dǎo)出的文件類型缕棵,即可獲取一個當(dāng)前系統(tǒng)可以使用的一個settings
字典孵班。
音視頻樣本寫入模式
我們都知道,在使用AVCaptureVideoOutput
和AVCaotureAudioOutput
進(jìn)行視頻錄制中招驴,音頻數(shù)據(jù)和視頻數(shù)據(jù)是分開獲取的篙程。因此我們在使用AVAssetWriter
進(jìn)行寫入視頻的時候需要注意數(shù)據(jù)的寫入順序。為了使數(shù)據(jù)有較高效率的排列别厘,存儲設(shè)備能更方便有效地讀取數(shù)據(jù)虱饿,并在播放和尋找資源時提高性能,我們在音視頻樣本獲取的時候需要采用交錯模式触趴。
交錯模式:音頻數(shù)據(jù)和視頻數(shù)據(jù)是逐個交錯排列的氮发。
在AVAssetWriter
中我們并不能直接設(shè)置寫入模式,但是在AVAssetWriterInput
中的readyForMoreMediaData
屬性可用于判斷該input
是否可以進(jìn)行數(shù)據(jù)拼接冗懦。只需input
在使用appendSampleBuffer:
寫入數(shù)據(jù)之前進(jìn)行一層判斷爽冕,即可控制最后的音視頻樣本排列方式為交錯模式。
錄制視頻步驟
為了方便編寫披蕉,我將關(guān)于AVAssetWriter
的代碼都放進(jìn)了SCMovieManager
中颈畸,完整代碼點(diǎn)這里。錄制視頻分為三個步驟没讲,分別是:
- 錄制開始
- 錄制過程
- 錄制結(jié)束
錄制開始
錄制開始需要做的步驟有以下三步:
- (void)startRecordWithVideoSettings:(NSDictionary *)videoSettings
audioSettings:(NSDictionary *)audioSettings
handle:(void (^ _Nullable)(NSError * _Nonnull))handle {
dispatch_async(self.movieQueue, ^{
// 第一步:創(chuàng)建 AVAssetWriter
// 第二步:創(chuàng)建視頻輸入
// 第三步:創(chuàng)建音頻輸入
self.recording = YES;
self.firstSample = YES;
});
}
PS: self.firstSample
用于控制錄制第一幀必須是視頻幀眯娱。
第一步:創(chuàng)建 AVAssetWriter
NSError *error;
self.movieWriter = [AVAssetWriter assetWriterWithURL:self.movieURL fileType:AVFileTypeQuickTimeMovie error:&error];
if (!self.movieWriter || error) {
NSLog(@"movieWriter error.");
return;
}
PS:self.movieURL
需要開發(fā)者自己管理好,最好在每次錄制之前保證該路徑下并沒有文件爬凑。
第二步:創(chuàng)建視頻輸入
self.movieVideoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings];
// 針對實(shí)時性進(jìn)行優(yōu)化
self.movieVideoInput.expectsMediaDataInRealTime = YES;
if ([self.movieWriter canAddInput:self.movieVideoInput]) {
[self.movieWriter addInput:self.movieVideoInput];
} else {
NSLog(@"Unable to add video input.");
}
PS:如果應(yīng)用程序只支持一個視頻方向就需要根據(jù)當(dāng)前設(shè)備方向做圖像旋轉(zhuǎn)轉(zhuǎn)換徙缴,并設(shè)置self.movieVideoInput.transform
屬性。
第三步:創(chuàng)建音頻輸入
self.movieAudioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings];
// 針對實(shí)時性進(jìn)行優(yōu)化
self.movieAudioInput.expectsMediaDataInRealTime = YES;
if ([self.movieWriter canAddInput:self.movieAudioInput]) {
[self.movieWriter addInput:self.movieAudioInput];
} else {
NSLog(@"Unable to add audio input.");
}
錄制過程
使用AVCaptureVideoDataOutput
和AVCaptureAudioDataOutput
進(jìn)行獲取數(shù)據(jù)的時候贰谣,我們都需要實(shí)現(xiàn)下面的代理方法并進(jìn)行樣本數(shù)據(jù)寫入:
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
if (self.movieManager.isRecording) {
[self.movieManager recordSampleBuffer:sampleBuffer];
}
}
recordSampleBuffer:
方法是錄制的核心方法娜搂,主要是根據(jù)mediaType
區(qū)分音頻和視頻數(shù)據(jù)進(jìn)行相應(yīng)的寫入。
- (void)recordSampleBuffer:(CMSampleBufferRef)sampleBuffer {
if (!self.isRecording) {
return;
}
CMFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
CMMediaType mediaType = CMFormatDescriptionGetMediaType(formatDesc);
if (mediaType == kCMMediaType_Video) {
// 視頻數(shù)據(jù)處理
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
if (self.isFirstSample) {
if ([self.movieWriter startWriting]) {
[self.movieWriter startSessionAtSourceTime: timestamp];
} else {
NSLog(@"Failed to start writing.");
}
self.firstSample = NO;
}
if (self.movieVideoInput.readyForMoreMediaData) {
if (![self.movieVideoInput appendSampleBuffer:sampleBuffer]) {
NSLog(@"Error appending video sample buffer.");
}
}
} else if (!self.firstSample && mediaType == kCMMediaType_Audio) {
// 音頻數(shù)據(jù)處理(已處理至少一個視頻數(shù)據(jù))
if (self.movieAudioInput.readyForMoreMediaData) {
if (![self.movieAudioInput appendSampleBuffer:sampleBuffer]) {
NSLog(@"Error appending audio sample buffer.");
}
}
}
}
錄制結(jié)束
錄制結(jié)束比較簡單吱抚,主要是就是調(diào)用self.movieWriter
的finishWritingWithCompletionHandler:
方式百宇,并根據(jù)self.movieWriter
的狀態(tài)進(jìn)行后續(xù)的處理。
- (void)stopRecordWithCompletion:(void (^)(BOOL, NSURL * _Nullable))completion {
self.recording = NO;
dispatch_async(self.movieQueue, ^{
[self.movieWriter finishWritingWithCompletionHandler:^{
switch (self.movieWriter.status) {
case AVAssetWriterStatusCompleted:{
self.firstSample = YES;
NSURL *fileURL = [self.movieWriter outputURL];
completion(YES, fileURL);
// FIXME: - 測試用保存
[self saveMovieToCameraRoll:fileURL authHandle:^(BOOL success, PHAuthorizationStatus status) {
NSLog(@"相冊添加權(quán)限:%d, %ld", success, (long)status);
} completion:^(BOOL success, NSError * _Nullable error) {
NSLog(@"視頻添加結(jié)果:%d, %@", success, error);
}];
break;
}
default:
NSLog(@"Failed to write movie: %@", self.movieWriter.error);
break;
}
}];
});
}
最后在fileURL
中的視頻文件就是我們錄制的成果了秘豹。
由于錄制的全過程并不需要在主線程中進(jìn)行操作携御,因此,我們都需要在
self.movieQueue
這個串行隊(duì)列中異步執(zhí)行。