1. 媒體捕捉的類
AVFoundation的照片和視頻捕捉功能是它的強項.先看一下其中捕捉相關(guān)的類.
-
捕捉會話 AVCaptureSession:
AVCaptureSession
相當于一個虛擬的插座,連接了輸入和輸出資源.它管理著從物理設(shè)備(攝像頭和麥克風)得到的數(shù)據(jù)流,然后輸出到其他地方.
可以額外配置一個會話預(yù)設(shè)值,用來控制捕捉數(shù)據(jù)的格式和質(zhì)量,默認是AVCaptureSessionPresetHigh
。 -
捕捉設(shè)備 AVCaptureDevice:
AVCaptureDevice
為物理設(shè)備定義一個接口和大量控制方法,例如對焦幽歼、曝光漱受、白平衡和閃光等屹篓。 -
捕捉設(shè)備輸入 AVCaptureDeviceInput:
在使用AVCaptureDevice
進行處理前,需要將它封裝到AVCaptureDeviceInput
實例中.因為一個捕捉設(shè)備不能直接添加到AVCaptureSession
中; 如下:
NSError *error;
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
-
捕捉的輸出 AVCaptureOutput:
AVCaptureOutput
有許多擴展類,它本身只是一個抽象基類,用于為從Session得到的數(shù)據(jù)尋找輸出目的地. 看上面結(jié)構(gòu)圖就可以看出各擴展類功能方向, 這里單獨說一下,AVCaptureAudioDataOutput
和AVCaptureVideoDataOutput
可以直接訪問硬件撲捉到的數(shù)字樣本,可以用于音視頻流進行實施處理. -
捕捉連接 AVCaptureConnection
這個類其實就是上圖中連接不同組件的連接箭頭所表示. 對這些連接的訪問可以讓開發(fā)者對信號流就行底層控制,比如禁用某些特定的連接,或者音頻連接中限制單獨的音頻軌道. -
捕捉預(yù)覽 AVCaptureVideoPreviewLayer
這個類不在上圖中, 它是對捕捉視頻數(shù)據(jù)進行實時預(yù)覽.在視頻角色中類似于AVPlayerLayer
捕捉會話應(yīng)用流程:
- 第一步:配置會話
- (BOOL)setupSession:(NSError **)error {
// 1. 創(chuàng)建捕捉會話
self.captureSession = [[AVCaptureSession alloc] init];
self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
//2. 根據(jù)要生成的媒體類型獲取捕捉設(shè)備,并添加到Session上.
AVCaptureDevice *videoDevice =
[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// 在把此捕捉設(shè)備添加到AVCaptureSession之前,先封裝成一個input對象.
AVCaptureDeviceInput *videoInput =
[AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
if (videoInput) {
if ([self.captureSession canAddInput:videoInput]) { // 測試是否可以add
[self.captureSession addInput:videoInput];
self.activeVideoInput = videoInput;
}
} else {
return NO;
}
/*// 如果要獲取音頻捕捉部分
AVCaptureDevice *audioDevice =
[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioInput =
[AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];
if (audioInput) {
if ([self.captureSession canAddInput:audioInput]) {
[self.captureSession addInput:audioInput];
}
} else {
return NO;
}
*/
//3. 設(shè)置捕捉會話的輸出部分
// image輸出
self.imageOutput = [[AVCaptureStillImageOutput alloc] init];
self.imageOutput.outputSettings = @{AVVideoCodecKey : AVVideoCodecJPEG};
if ([self.captureSession canAddOutput:self.imageOutput]) {
[self.captureSession addOutput:self.imageOutput];
}
// movie輸出
self.movieOutput = [[AVCaptureMovieFileOutput alloc] init];
if ([self.captureSession canAddOutput:self.movieOutput]) {
[self.captureSession addOutput:self.movieOutput];
}
return YES;
}
- 第二: 啟動和停止會話
- (void)startSession {
if (![self.captureSession isRunning]) {
dispatch_async([self globalQueue], ^{
[self.captureSession startRunning];
});
}
}
- (void)stopSession {
if ([self.captureSession isRunning]) {
dispatch_async([self globalQueue], ^{
[self.captureSession stopRunning];
});
}
}
// 開始和停止會話都是同步耗時操作,所以采用異步執(zhí)行.
- (dispatch_queue_t)globalQueue {
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
}
iOS 8之后使用硬件設(shè)備都需要用戶給予權(quán)限,使用前都要判斷下. 例如:
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if(authStatus == AVAuthorizationStatusRestricted || authStatus == AVAuthorizationStatusDenied) {
NSLog(@"相機權(quán)限受限!");
}
2. 細節(jié)部分.
2.1 捕捉設(shè)備的修改與配置
- 獲取攝像頭,切換攝像頭.
// 當前捕捉會話對應(yīng)的攝像頭.
- (AVCaptureDevice *)activeCamera {
return self.activeVideoInput.device;
}
// 當前未激活的攝像頭
- (AVCaptureDevice *)inactiveCamera {
AVCaptureDevice *device = nil;
if (self.cameraCount > 1) {
if ([self activeCamera].position == AVCaptureDevicePositionBack) {
device = [self cameraWithPosition:AVCaptureDevicePositionFront];
} else {
device = [self cameraWithPosition:AVCaptureDevicePositionBack];
}
}
return device;
}
// 切換攝像頭
- (BOOL)switchCameras {
if (![self canSwitchCameras]) {
return NO;
}
// 1. 獲取當前未使用的攝像頭,并為他創(chuàng)建一個新的Input.
NSError *error;
AVCaptureDevice *videoDevice = [self inactiveCamera];
AVCaptureDeviceInput *videoInput =
[AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
// 2. 用新的Input替換掉正在激活的Input
if (videoInput) {
[self.captureSession beginConfiguration]; // 原子性配置開始.保證線程安全
[self.captureSession removeInput:self.activeVideoInput];
if ([self.captureSession canAddInput:videoInput]) {
[self.captureSession addInput:videoInput];
self.activeVideoInput = videoInput;
} else { // 確保安全,如果新的Input不能添加,繼續(xù)使用舊的
[self.captureSession addInput:self.activeVideoInput];
}
[self.captureSession commitConfiguration]; // 原子性配置完成
} else { // 錯誤處理
[self.delegate deviceConfigurationFailedWithError:error];
return NO;
}
return YES;
}
- (BOOL)canSwitchCameras {
return self.cameraCount > 1;
}
- (NSUInteger)cameraCount {
return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}
// 獲取指定位置的AVCaptureDevice
- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if (device.position == position) {
return device;
}
}
return nil;
}
- 調(diào)整焦距和曝光
修改AVCaptureDevice配置設(shè)備時,一定要先測試修改動作是否被硬件設(shè)備支持. 比如前置攝像頭不支持對焦操作.嘗試一個不被支持的修改動作就會導(dǎo)致程序崩潰. 所以任何修改都要先通過判斷一下.
/* 調(diào)整對焦 */
- (void)focusAtPoint:(CGPoint)point {
AVCaptureDevice *device = self.activeVideoInput.device;
if (device.isFocusPointOfInterestSupported &&
[device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { // 判斷設(shè)備是否支持興趣點對焦和自動對焦模式.
NSError *error;
if ([device lockForConfiguration:&error]) { // 鎖定設(shè)備,準備配置修改. 之后修改完釋放鎖定.
device.focusPointOfInterest = point;
device.focusMode = AVCaptureFocusModeAutoFocus;
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
/*
調(diào)整曝光
曝光模式定義了四種方式:
AVCaptureExposureModeLocked 鎖定
AVCaptureExposureModeAutoExpose 調(diào)整一次并鎖定
AVCaptureExposureModeContinuousAutoExposure 一直自動調(diào)整
AVCaptureExposureModeCustom
大部分設(shè)備是根據(jù)環(huán)境自動調(diào)整, 現(xiàn)在我們做調(diào)整曝光度并鎖定. 但在iOS中并不支持AVCaptureExposureModeAutoExpose, 所以我們要用其他定義來實現(xiàn).這個功能
*/
static const NSString *THCameraAdjustingExposureContext;
- (void)exposeAtPoint:(CGPoint)point {
AVCaptureDevice *device = self.activeVideoInput.device;
AVCaptureExposureMode exposureMode =
AVCaptureExposureModeContinuousAutoExposure;
if (device.isExposurePointOfInterestSupported &&
[device isExposureModeSupported:exposureMode]) { // 判斷設(shè)備是否支持AutoExposure模式
NSError *error;
if ([device lockForConfiguration:&error]) { // 鎖定設(shè)備,準備配置修改. 之后修改完釋放鎖定.
device.exposurePointOfInterest = point;
device.exposureMode = exposureMode;
// 判斷設(shè)備是否支持鎖定曝光設(shè)置, 如果支持,使用KVO來觀察設(shè)備"adjustingExposure"屬性狀態(tài). 然后在曝光調(diào)整完成時在該點上鎖定曝光.
if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
[device addObserver:self
forKeyPath:@"adjustingExposure"
options:NSKeyValueObservingOptionNew
context:&THCameraAdjustingExposureContext];
}
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == &THCameraAdjustingExposureContext) {
// 這里通過測試Context是否為&THCameraAdjustingExposureContext指針,來判斷監(jiān)聽到的回調(diào)是否對應(yīng)我們期望的變更操作.
AVCaptureDevice *device = (AVCaptureDevice *)object;
if (!device.isAdjustingExposure &&
[device isExposureModeSupported:AVCaptureExposureModeLocked]) {
[object removeObserver:self
forKeyPath:@"adjustingExposure"
context:&THCameraAdjustingExposureContext];
// 回到主隊列設(shè)置exposureMode
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error;
if ([device lockForConfiguration:&error]) {
device.exposureMode = AVCaptureExposureModeLocked;
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
});
}
} else {
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
/* 恢復(fù)對焦和曝光設(shè)置 */
- (void)resetFocusAndExposureModes {
AVCaptureDevice *device = self.activeVideoInput.device;
AVCaptureExposureMode exposureMode =
AVCaptureExposureModeContinuousAutoExposure;
AVCaptureFocusMode focusMode = AVCaptureFocusModeContinuousAutoFocus;
BOOL canResetFocus = [device isFocusPointOfInterestSupported] &&
[device isFocusModeSupported:focusMode];
BOOL canResetExposure = [device isExposurePointOfInterestSupported] &&
[device isExposureModeSupported:exposureMode];
CGPoint centerPoint = CGPointMake(0.5f, 0.5f); // 創(chuàng)建一個中心掃描點.
NSError *error;
if ([device lockForConfiguration:&error]) {
if (canResetFocus) { // 焦點可以重置
device.focusMode = focusMode;
device.focusPointOfInterest = centerPoint;
}
if (canResetExposure) { // 曝光可以充值
device.exposureMode = exposureMode;
device.exposurePointOfInterest = centerPoint;
}
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
- 調(diào)整閃光燈和手電筒模式(Flash和 Torch 模式)
AVCaptureDevice可以控制攝像頭的LED燈,當拍照時是做閃光燈,當拍視頻時做手電筒.
- (void)setFlashMode:(AVCaptureFlashMode)flashMode {
AVCaptureDevice *device = self.activeVideoInput.device;
if (device.flashMode != flashMode &&
[device isFlashModeSupported:flashMode]) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.flashMode = flashMode;
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
- (void)setTorchMode:(AVCaptureTorchMode)torchMode {
AVCaptureDevice *device = self.activeVideoInput.device;
if (device.torchMode != torchMode &&
[device isTorchModeSupported:torchMode]) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.torchMode = torchMode;
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
3. 基本功能實現(xiàn)-拍照和攝像
- 拍照
上面介紹了基本流程,如果要拍照就是希望捕捉會話的輸出Output實例是AVCaptureStillImageOutput
類型,這需要在第一步配置會話時添加上去. 而關(guān)于接下來具體拍攝方法就定義在AVCaptureStillImageOutput中. 代碼如下:
- (void)captureStillImage {
// 獲取Output對象當前使用的AVCaptureConnection連接.
// 原始相機只支持垂直方向,所以當設(shè)備旋轉(zhuǎn)時,用戶界面保持不變.我們希望橫向時相應(yīng)調(diào)整圖片方向.
AVCaptureConnection *connection =
[self.imageOutput connectionWithMediaType:AVMediaTypeVideo];
// 這里要先確定Connection 是否支持設(shè)置視頻方向. 然后設(shè)置圖片方向.
if (connection.isVideoOrientationSupported) {
connection.videoOrientation = [self currentVideoOrientation];
}
id handler = ^(CMSampleBufferRef sampleBuffer, NSError *error) {
if (sampleBuffer != NULL) {
NSData *imageData =
[AVCaptureStillImageOutput
jpegStillImageNSDataRepresentation:sampleBuffer];
// 得到的照片
UIImage *image = [[UIImage alloc] initWithData:imageData];
} else {
NSLog(@"NULL sampleBuffer: %@", [error localizedDescription]);
}
};
// 拍攝靜態(tài)圖片
[self.imageOutput captureStillImageAsynchronouslyFromConnection:connection
completionHandler:handler];
}
- 視頻
同拍照流程一樣,設(shè)置捕捉會話的輸出Output實例是AVCaptureMovieFileOutput
類型. 具體攝像代碼如下:
- (void)startRecording {
if (!self.movieOutput.isRecording) { // 是否在錄制中
// 設(shè)置錄制視頻方向為當前方向
AVCaptureConnection *videoConnection =
[self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
if ([videoConnection isVideoOrientationSupported]) {
videoConnection.videoOrientation = self.currentVideoOrientation;
}
// 設(shè)置VideoStabilization.可以提高視頻質(zhì)量.
if ([videoConnection isVideoStabilizationSupported]) {
if ([[[UIDevice currentDevice] systemVersion] floatValue] < 8.0) {
videoConnection.enablesVideoStabilizationWhenAvailable = YES;
} else {
videoConnection.preferredVideoStabilizationMode = AVCaptureVideoStabilizationModeAuto;
}
}
AVCaptureDevice *device = self.activeVideoInput.device;
// 設(shè)置平滑對焦模式,提升移動錄制質(zhì)量
if (device.isSmoothAutoFocusSupported) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.smoothAutoFocusEnabled = NO;
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
NSURL *outputURL = [self uniqueURL]; // 定義視頻文件唯一輸出路徑.
// 最后,開始錄制. 通過AVCaptureFileOutputRecordingDelegate控制錄制過程.
[self.movieOutput startRecordingToOutputFileURL:outputURL
recordingDelegate:self];
}
}
AVCaptureMovieFileOutput支持分段捕捉.