項目負(fù)責(zé)人:這個版本做一個類似于新浪微博拍攝的功能(不帶美顏以及后續(xù)對視頻圖片的編輯),一天時間能不能搞定?
你:......(內(nèi)心:你行你上?。?br>
我:Github。點擊下載,只需專注界面開發(fā),一天?妥妥的!
言歸正傳,在那段小情景中已經(jīng)將今天的主要內(nèi)容放出。很簡單,Github中的代碼是對AVFoundation的一些簡單封裝,使開發(fā)者無需考慮錄制視頻以及拍照相關(guān)的流程,只需要專注于你的界面邏輯。
此文圍繞對AVFoundation的封裝進(jìn)行展開,TYCamera借助于新浪微博的視頻錄制相關(guān)的基本功能進(jìn)行封裝,當(dāng)然,其他的錄制視頻,拍照等相關(guān)的功能均大同小異,不一樣的只是業(yè)務(wù)邏輯而已。所以TYCamera能使開發(fā)者專注于界面業(yè)務(wù)邏輯開發(fā),提高開發(fā)效率。
仿新浪微博demo效果:

知識點簡介
要完成相機(jī)基本功能并不繁瑣,當(dāng)然AVFoundation是十分強(qiáng)大的。作者通過分解相機(jī)的錄制過程引出相關(guān)的類。
個人認(rèn)為實現(xiàn)錄像/拍照基礎(chǔ)功能可以總結(jié)為多個輸入設(shè)備以及多個輸出通過數(shù)據(jù)采集硬件,連接而成的一次會話任務(wù)。那么需要實現(xiàn)的類便可以從這段總結(jié)中簡單地得出了。
多個輸入設(shè)備:當(dāng)然屬于相機(jī)以及麥克風(fēng),這樣AVCaptureInput是必不可少的,但我們使用更高一層的封裝,使用AVCaptureInput的子類AVCaptureDeviceInput來表示相機(jī)、麥克風(fēng)設(shè)備的輸入。
多個輸出:一般來說,攝像頭的主要作用是采集音視頻數(shù)據(jù),所以音頻輸出的相關(guān)類AVCaptureAudioDataOutput以及視頻輸出相關(guān)的類AVCaptureVideoDataOutput是需要使用的,此外AVFoundation也可拍攝靜態(tài)圖片(照片),故而靜態(tài)圖片相關(guān)的類AVCaptureStillImageOutput也是不可或缺的。
硬件連接:輸入與輸出之間需要構(gòu)建相應(yīng)的聯(lián)系,則需要橋梁AVCaptureConnection構(gòu)建microConnection和cameraConnection。
會話:毋庸置疑AVCaptureSession。
蘋果的文檔很清晰地給出了幾者之間的關(guān)系:

如對其中的基礎(chǔ)知識不是很清楚的同學(xué)請直接跳轉(zhuǎn)在 iOS 上捕獲視頻
TYCamera的封裝說明
類說明:
TYCamera一共由四個類組成。TYCameraVC類用于與開發(fā)者界面布局;TYRecordEngine是TYCamera的核心類,TYRecordEngine將相機(jī)相關(guān)的功能進(jìn)行了封裝,并暴露相關(guān)接口,方便調(diào)用者簡單使用實現(xiàn)相關(guān)功能,若開發(fā)者不想集成TYCameraVC實現(xiàn)相關(guān)功能,完全可以使用TYRecordEngine類的相關(guān)API進(jìn)行自定義功能實現(xiàn);TYRecordEncoder是負(fù)責(zé)數(shù)據(jù)寫入相關(guān)的功能;TYRecordHelper提供一些工廠方法實現(xiàn)視頻編輯的一些操作。
類詳解:
TYRecordEngine
TYRecordEngine為外界提供了相機(jī)對應(yīng)功能的API,那么我們可以從結(jié)果分析需求。
一個基本的相機(jī)功能一般具有閃光燈模式的切換,前后置攝像頭的切換。這樣我們就可以暴露這樣兩個簡單地功能。在TYRecordEngine.h文件中聲明兩個方法。
/**
設(shè)置閃光燈的模式
@param mode 為閃光燈設(shè)置的模式
*/
- (void)setupFlashLight:(AVCaptureFlashMode)mode;
// 切換攝像頭
- (void)switchCamera;
為了方便地控制相機(jī)的相關(guān)的流程,作者提取了幾個重要的步驟。
1.開啟相機(jī)功能并采集相關(guān)的音視頻數(shù)據(jù),客戶端調(diào)用startRunning方法將輸入端采集到的數(shù)據(jù)流通過AVCaptureConnection實例傳輸?shù)捷敵龆耍?br>
2.寫入音視頻數(shù)據(jù)為本地文件
3.停止數(shù)據(jù)的寫入
4.關(guān)閉相機(jī)功能
5.合成視頻并暴露視頻的相關(guān)內(nèi)容,例如封面圖,資源地址,以及視頻時長等
因此,下面五個方法便應(yīng)用而生。
- (void)openRecordFunctions;
- (void)closeRecordFunctions;
- (void)startRecord;
- (void)stopRecord;
- (void)finishCaptureHandler:(void(^)(UIImage *coverImage, NSString *filePath, NSTimeInterval duration))handler failure:(void(^)(NSError *error))failure;
在我們開啟相機(jī)相關(guān)功能之前,我們可能還需要做一些基本設(shè)置,為方便開發(fā)者調(diào)用,我們自定義相關(guān)的類方法和實例方法來創(chuàng)建TYRecordEngine實例。
/**
初始化TYRecordEngines實例對象
@param preset 設(shè)置錄制視頻的質(zhì)量
@param position 設(shè)置攝像頭(前置攝像頭/后置攝像頭)
@param recordType 錄制視頻的類型
@return 返回TYRecordEngines實例對象
*/
- (instancetype)initRecordEngineSessionPreset:(NSString *)preset
devicePosition:(AVCaptureDevicePosition)position
recordType:(TYRecordEngineType)recordType;
最后比較重要的一個實例是顯示數(shù)據(jù)流的Layer:
@property (nonatomic, strong) AVCaptureVideoPreviewLayer *previewLayer;
下面我們便來實現(xiàn)相關(guān)的方法:
在上文中作者已經(jīng)通過一句話總結(jié)出了實現(xiàn)相機(jī)功能的基本類,所以起始必然是對這些類使用懶加載的方式進(jìn)行實例化。
@property (nonatomic, strong) AVCaptureSession *captureSession;
@property (nonatomic, strong) AVCaptureDeviceInput *cameraInput;
@property (nonatomic, strong) AVCaptureDeviceInput *microInput;
@property (nonatomic, strong) AVCaptureVideoDataOutput *cameraOutput;
@property (nonatomic, strong) AVCaptureAudioDataOutput *microOutput;
@property (nonatomic, strong) AVCaptureStillImageOutput *photoOutput;
@property (nonatomic, strong) AVCaptureConnection *cameraConnection;
@property (nonatomic, strong) AVCaptureConnection *microConnection;
相關(guān)的懶加載代碼便不在此處貼出,具體細(xì)節(jié)請查看TYRecordEngine.m文件相關(guān)的代碼。在這里需要指出的是microOutput以及cameraOutput實例需要設(shè)置相應(yīng)的delegate(調(diào)用方法setSampleBufferDelegate:queue:),實現(xiàn)相應(yīng)的代理方法才能正確監(jiān)測到攝像頭以及麥克風(fēng)采集的buffer.如若對應(yīng)的類不清楚如何實例化的同學(xué),可跳入對應(yīng)類的頭文件進(jìn)行查看相關(guān)API。
一些必備的實例初始化成功后,我們便可實現(xiàn)我們前面在TYRecordEngine.h文件中聲明的相關(guān)的方法。
- (instancetype)initRecordEngineSessionPreset:(NSString *)preset
devicePosition:(AVCaptureDevicePosition)position
recordType:(TYRecordEngineType)recordType {
if (self = [self init]) {
_presetName = preset;
_position = position;
_recordType = recordType;
[self addInputOutput];
}
return self;
}
初始化方法的實現(xiàn),主要記錄相關(guān)的基本屬性,并為session增加對應(yīng)的輸出與輸入。因功能類型不一致所以我們構(gòu)造一個私有方法去實現(xiàn)不同類型的輸入與輸出的添加。
- (void)addInputOutput {
[self.captureSession beginConfiguration];
if ([self.captureSession canAddInput:self.cameraInput]) {
[self.captureSession addInput:self.cameraInput];
}
switch (_recordType) {
case TYRecordEngineTypeBoth: {
[self addVideoInputOutput];
if ([self.captureSession canAddOutput:self.photoOutput]) {
[self.captureSession addOutput:self.photoOutput];
}
}
break;
case TYRecordEngineTypeVideo: {
[self addVideoInputOutput];
}
break;
case TYRecordEngineTypePhoto: {
if ([self.captureSession canAddOutput:self.photoOutput]) {
[self.captureSession addOutput:self.photoOutput];
}
}
break;
default:
break;
}
[self.captureSession commitConfiguration];
}
在該過程中我們又發(fā)現(xiàn)在類型為TYRecordEngineTypeBoth和TYRecordEngineTypeVideo的情況下都需要加入與視頻相關(guān)的輸入與輸出,為了代碼的復(fù)用性,我們再實現(xiàn)一個添加視頻相關(guān)輸入輸出的方法。
- (void)addVideoInputOutput {
if ([self.captureSession canAddInput:self.microInput]) {
[self.captureSession addInput:self.microInput];
}
if ([self.captureSession canAddOutput:self.cameraOutput]) {
[self.captureSession addOutput:self.cameraOutput];
[self.cameraOutput setSampleBufferDelegate:self queue:self.captureQueue];
}
if ([self.captureSession canAddOutput:self.microOutput]) {
[self.captureSession addOutput:self.microOutput];
[self.microOutput setSampleBufferDelegate:self queue:self.captureQueue];
}
self.cameraConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
}
TYRecordEngine初始化方法實現(xiàn)完成,我們開始打開相機(jī)的相關(guān)功能。在此之前我們跳入到AVCaptureSession頭文件中查看相關(guān)的API,可以發(fā)現(xiàn),在該類中與開始結(jié)束相關(guān)的方法只有startRunning和stopRuning方法,而要實現(xiàn)一進(jìn)入到控制器相機(jī)就采集到相關(guān)數(shù)據(jù)我們需要在viewWillApperar或者viewDidLoad方法中實現(xiàn),但是我們一般是在一些操作后才開始錄制視頻,所以我們此時需要聲明一個私有屬性來表示是否正在錄制視頻的一個全局變量。
@property (atomic, assign) BOOL isCapturing;
因此,在關(guān)閉視頻功能時需要將isCapturing屬性設(shè)置為NO。
- (void)openRecordFunctions {
if (![self.captureSession isRunning]) {
[self.captureSession startRunning];
}
}
- (void)closeRecordFunctions {
if ([self.captureSession isRunning]) {
[self.captureSession stopRunning];
}
self.isCapturing = NO;
}
緊接著我們可以實現(xiàn)攝像頭切換以及閃光燈模式切換的功能,攝像頭切換功能實現(xiàn)起來也是比較簡單的,大概來講就是移除當(dāng)前的輸入,添加新的輸入,并添加動畫。而切換閃光燈模式只要設(shè)置后置攝像頭的mode屬性為相應(yīng)的枚舉值即可,但在此處需要注意的是在設(shè)置mode的值之前調(diào)用lockForConfiguration:方法,設(shè)置完后調(diào)用unlockForConfiguration方法。
在這兩個方法中我們需要頻繁地獲取對應(yīng)的攝像頭設(shè)備,我們可以總結(jié)出這樣的私有方法去獲取對應(yīng)的攝像頭設(shè)備。
- (AVCaptureDevice *)captureDeviceInput:(AVCaptureDevicePosition)position {
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if (device.position == position) {
return device;
}
}
return nil;
}
這樣一來便可以減少代碼的冗余。
- (void)switchCamera {
AVCaptureDevicePosition currentDevicePositon = [self.cameraInput device].position;
if (currentDevicePositon == AVCaptureDevicePositionBack) {
currentDevicePositon = AVCaptureDevicePositionFront;
} else {
currentDevicePositon = AVCaptureDevicePositionBack;
}
NSError *error = nil;
AVCaptureDeviceInput *newCameraInput = [AVCaptureDeviceInput deviceInputWithDevice:[self captureDeviceInput:currentDevicePositon] error:&error];
if (error) {
NSLog(@"Get New Camera Input Failure! Error:%@", error);
return;
}
[self.captureSession beginConfiguration];
[self.captureSession removeInput:self.cameraInput];
if ([self.captureSession canAddInput:newCameraInput]) {
[self.captureSession addInput:newCameraInput];
self.cameraInput = newCameraInput;
} else {
[self.captureSession addInput:self.cameraInput];
}
[self.captureSession commitConfiguration];
[self switchCameraAnimation];
}
- (void)switchCameraAnimation {
CATransition *switchAnimation = [CATransition animation];
switchAnimation.delegate = self;
switchAnimation.duration = 0.45f;
switchAnimation.type = @"oglFlip";
switchAnimation.subtype = [self isFrontFacingCameraPreset] ? kCATransitionFromRight : kCATransitionFromLeft;
switchAnimation.timingFunction = UIViewAnimationCurveEaseInOut;
[self.previewLayer addAnimation:switchAnimation forKey:@"changeAnimation"];
}
- (void)setupFlashLight:(AVCaptureFlashMode)mode {
AVCaptureDevice *backCamera = [self captureDeviceInput:AVCaptureDevicePositionBack];
if (backCamera.hasFlash) {
[backCamera lockForConfiguration:nil];
backCamera.flashMode = mode;
[backCamera unlockForConfiguration];
}
[self.captureSession startRunning];
}
在這些簡單的功能實現(xiàn)以后,我們開始實現(xiàn)核心的功能。上文已經(jīng)提及到,AVCaptureSession只有startRunning和stopRuning方法去實現(xiàn)開始和停止,但是我們在TYRecordEngine.h文件中聲明了startRecord和stopRecord方法,并且聲明了isCapturing私有屬性,所以我們借助isCapturing屬性來實現(xiàn)數(shù)據(jù)寫入功能。那么在startRecord方法中我們需要設(shè)置isCapturing屬性為YES。又由于我們需要得出錄制視頻對應(yīng)的時長,所以我們使用Dispatch_source_t創(chuàng)建一個定時器。并使用全局屬性currentDuration計時。
- (void)startRecord {
self.isCapturing = YES;
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.captureQueue);
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
self.currentDuration += 0.1;
});
dispatch_resume(_timer);
}
獲取設(shè)備采集到的音視頻數(shù)據(jù)是通過AVCaptureVideoDataOutputSampleBufferDelegate的代理方法進(jìn)行監(jiān)聽的,因此我們在這里做相關(guān)的操作。在此該代理方法中我們主要是處理將數(shù)據(jù)寫入至本地,并且給出錄制的總時長小于設(shè)置的最低時長,錄制的總時長大于等于設(shè)置的最大時長,以及錄制某段視頻時的當(dāng)前進(jìn)度。
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection {
// 處理視頻鏡像問題
self.cameraConnection.videoMirrored = [self isFrontFacingCameraPreset];
// 非錄制狀態(tài)不做任何處理
if (!self.isCapturing) { return; }
// 獲取音頻相關(guān)參數(shù)
if (!self.recordEncoder && captureOutput == self.microOutput) {
// 使用TYRecordHelper獲取要寫入文件的地址
self.filePath = [TYRecordHelper videoPath];
// videosPath為可變數(shù)組,存入視頻片段地址,為后續(xù)的刪除指定視頻片段提供支持
[self.videosPath addObject:self.filePath];
// 獲取音頻相關(guān)的屬性,因本項目使用ALAssertWriter寫文件,初始化音頻輸入的時候一定需要指定相應(yīng)的setting,否則初始化失敗會造成crash
[self setAudioFormat:sampleBuffer];
// 初始化TYRecordEncoder
self.recordEncoder = [TYRecordEncoder recordEncoderPath:self.filePath videoWidth:_videoW videoHeight:_videoH audioChannel:_channel audioRate:_rate];
}
// 回調(diào)錄制當(dāng)前視頻的進(jìn)度
if (self.delegate && [self.delegate respondsToSelector:@selector(recordProgress:)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate recordProgress:self.currentDuration];
});
}
// 回調(diào)錄制錄制時間大于等于剩余錄制時間即錄制的總時間大于等于錄制允許的最大時間
if (self.currentDuration >= (self.maxRecordTime - self.videoDuration)) {
[self closeRecordFunctions];
return;
}
// 使用TYRecordEncoder實例的相關(guān)方法將數(shù)據(jù)寫入本地
[self.recordEncoder encoderFrame:sampleBuffer isVideo:captureOutput != self.microOutput];
}
// 獲取音頻必備的兩個屬性值
- (void)setAudioFormat:(CMSampleBufferRef)sampleBuffer {
CMFormatDescriptionRef fmt = CMSampleBufferGetFormatDescription(sampleBuffer);
const AudioStreamBasicDescription *asbd = CMAudioFormatDescriptionGetStreamBasicDescription(fmt);
_rate = asbd->mSampleRate;
_channel = asbd->mChannelsPerFrame;
}
停止視頻錄制后,我們需要重新設(shè)置一些狀態(tài)值,例如currentDuration,isCapturing,并計算視屏錄制的總時長videoDuration,關(guān)閉定時器,告訴ALAssertWritter寫入數(shù)據(jù)完成。最后比較當(dāng)前視頻錄制的總時間與設(shè)置的最低時長以及最大時長并給出相應(yīng)的回調(diào)。
- (void)stopRecord {
self.isCapturing = NO;
self.videoDuration += self.currentDuration;
[self.durations addObject:@(self.currentDuration)];
dispatch_source_cancel(self.timer);
self.timer = nil;
self.currentDuration = 0.f;
[self.recordEncoder encoderFinishCompletionHandler:^{
self.recordEncoder = nil;
if (self.videoDuration < self.minRecordTime) {
if (self.delegate && [self.delegate respondsToSelector:@selector(recordDurationLessMinRecordDuration)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate recordDurationLessMinRecordDuration];
});
}
} else if (self.videoDuration >= self.maxRecordTime) {
if (self.delegate && [self.delegate respondsToSelector:@selector(recordDurationLargerEqualMaxRecordDuration)]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate recordDurationLargerEqualMaxRecordDuration];
});
}
}
}];
}
這樣就實現(xiàn)了音視頻數(shù)據(jù)的采集并寫入到本地的過程,最后,我們需要獲取最終的視頻文件,我們需要獲取視頻的封面圖,并轉(zhuǎn)換相應(yīng)的格式。畢竟音視頻也需要在安卓設(shè)備或其他設(shè)備上播放,所以我們一般講視頻轉(zhuǎn)換為MP4格式即可。當(dāng)然多個視頻片段的話我們就需要對視頻進(jìn)行拼接。這些方法都在TYRecordHelper類中使用類方法實現(xiàn)了相關(guān)功能,我們只需要進(jìn)行相關(guān)調(diào)用,并給出對應(yīng)回調(diào)即可。
- (void)finishCaptureHandler:(void (^)(UIImage *, NSString *, NSTimeInterval))handler failure:(void (^)(NSError *))failure {
NSMutableArray *avassets = [NSMutableArray array];
for (NSString *videoPath in self.videosPath) {
AVAsset *asset = [AVAsset assetWithURL:[NSURL fileURLWithPath:videoPath]];
[avassets addObject:asset];
}
AVMutableComposition *compisition = [TYRecordHelper combineVideosWithAssetArray:avassets];
[TYRecordHelper transformFormatToMp4WithAsset:compisition presetName:AVAssetExportPreset1280x720 success:^(UIImage *coverImage, NSString *filePath) {
if (handler) {
handler(coverImage, filePath, self.videoDuration);
}
} failure:^(NSError *error) {
if (failure) {
failure(error);
}
}];
}
所有工作完成,最后關(guān)閉相關(guān)的功能停止采集數(shù)據(jù)。
- (void)closeRecordFunctions {
if ([self.captureSession isRunning]) {
[self.captureSession stopRunning];
}
self.isCapturing = NO;
}
在看項目中的代碼的時候我們會看到一些數(shù)組,它們都是為實現(xiàn)視頻片段拼接、刪除等操作而存在的。邏輯的話較為簡單,便不再此贅述。
TYRecordEncoder
TYRecordEncoder類的作用很明顯,是與音視頻編碼相關(guān)的類,該類主要使用是對AVAssetWriter的一個封裝。在該類的.h文件中我們暴露了幾個相關(guān)的API,類方法與實例方法的初始化方法,將buffer寫入本地文件的方法和結(jié)束寫入資源的方法。在此類中我們還需要初始化相應(yīng)的音視頻輸入,使用AVAssetWriterInput類,在初始化對應(yīng)的音視頻輸入實例時,這里需要注意的是,初始化videoInput時,需要為以下三個key設(shè)置對應(yīng)的值,分別是AVVideoCodecKey、AVVideoWidthKey、AVVideoHeightKey;初始化microInput時,需要下面這些key的值,分別是AVFormatIDKey、AVNumberOfChannelsKey、AVSampleRateKey。如果沒有設(shè)置的話會crash,不清楚的可以跳入相關(guān)的頭文件,其中的描述有具體的說明。
- (AVAssetWriterInput *)videoInput {
if (!_videoInput) {
NSDictionary *setting = [NSDictionary dictionaryWithObjectsAndKeys:
AVVideoCodecH264, AVVideoCodecKey,
[NSNumber numberWithInteger: _videoW], AVVideoWidthKey,
[NSNumber numberWithInteger: _videoH], AVVideoHeightKey,
nil];
_videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:setting];
_videoInput.expectsMediaDataInRealTime = YES;
}
return _videoInput;
}
- (AVAssetWriterInput *)audioInput {
if (!_audioInput) {
NSDictionary *setting = [NSDictionary dictionaryWithObjectsAndKeys:
@(kAudioFormatMPEG4AAC), AVFormatIDKey,
@(_channel), AVNumberOfChannelsKey,
@(_rate), AVSampleRateKey,
nil];
_audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:setting];
_audioInput.expectsMediaDataInRealTime = YES;
}
return _audioInput;
}
這樣我們對AVFoundation的封裝便基本完成了。詳細(xì)用法請下載Github參考其中的Demo,如果覺得不錯給個star吧。后續(xù)會添加一些自定義的濾鏡。需要學(xué)習(xí)AVFoundation其它知識點的同學(xué)可以直接閱讀AVFoundation Programming Guide。
總結(jié):
錄像或者拍照基礎(chǔ)功能的實現(xiàn)可以總結(jié)為多個輸入設(shè)備以及多個輸出通過數(shù)據(jù)采集硬件,連接而成的一次會話任務(wù)