在上文中,簡述了經過AVCapturePhotoOutput、AVCapturePhotoSettings來實現代理,獲取當前攝像頭所捕捉到的photo數據,生成一張圖片。數組
視頻錄製過程大體也是如此,經過AVCaptureMovieFileOutput來獲取視頻數據,大體流程以下:bash
/// 是否在錄製狀態
- (BOOL)isRecording {
return self.movieOutput.isRecording;
}
複製代碼
/// 開始錄製
- (void)startRecording {
if (![self isRecording]) {
//獲取當前視頻捕捉鏈接信息
AVCaptureConnection *videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
//調整方向
if ([videoConnection isVideoOrientationSupported]) {
videoConnection.videoOrientation = [self currentVideoOrientation];
}
//判斷是否支持視頻穩定功能(保證視頻質量)
if ([videoConnection isVideoStabilizationSupported]) {
videoConnection.preferredVideoStabilizationMode = YES;
}
//拿到活躍的攝像頭
AVCaptureDevice *device = [self activeCamera];
//判斷是否支持平滑對焦(當用戶移動設備時, 能自動且快速的對焦)
if (device.isSmoothAutoFocusEnabled) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.smoothAutoFocusEnabled = YES;
[device unlockForConfiguration];
} else {
//失敗回調
}
}
//獲取路徑
self.outputURL = [self uniqueURL];
//攝像頭的相關配置完成, 也獲取到路徑, 開始錄製(這裏錄製QuckTime視頻文件, 保存到相冊)
[self.movieOutput startRecordingToOutputFileURL:self.outputURL recordingDelegate:self];
}
}
複製代碼
/// 中止錄製
- (void)stopRecording {
if ([self isRecording]) {
[self.movieOutput stopRecording];
}
}
///路徑轉換
- (NSURL *)uniqueURL {
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), @"output.mov"]];
return url;
}
///獲取方向值
- (AVCaptureVideoOrientation)currentVideoOrientation {
AVCaptureVideoOrientation result;
UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
switch (deviceOrientation) {
case UIDeviceOrientationPortrait:
case UIDeviceOrientationFaceUp:
case UIDeviceOrientationFaceDown:
result = AVCaptureVideoOrientationPortrait;
break;
case UIDeviceOrientationPortraitUpsideDown:
//若是這裏設置成AVCaptureVideoOrientationPortraitUpsideDown,則視頻方向和拍攝時的方向是相反的。
result = AVCaptureVideoOrientationPortrait;
break;
case UIDeviceOrientationLandscapeLeft:
result = AVCaptureVideoOrientationLandscapeRight;
break;
case UIDeviceOrientationLandscapeRight:
result = AVCaptureVideoOrientationLandscapeLeft;
break;
default:
result = AVCaptureVideoOrientationPortrait;
break;
}
return result;
}
複製代碼
///經過代理來獲取視頻數據
#pragma mark - AVCaptureFileOutputRecordingDelegate
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput
didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL
fromConnections:(NSArray *)connections
error:(NSError *)error {
if (error) {
//錯誤回調
} else {
//視頻寫入到相冊
[self writeVideoToAssetsLibrary:[self.outputURL copy]];
}
self.outputURL = nil;
}
//寫入捕捉到的視頻
- (void)writeVideoToAssetsLibrary:(NSURL *)videoURL {
__block PHObjectPlaceholder *assetPlaceholder = nil;
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
//保存進相冊
PHAssetChangeRequest *changeRequest = [PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoURL];
assetPlaceholder = changeRequest.placeholderForCreatedAsset;
} completionHandler:^(BOOL success, NSError * _Nullable error) {
NSLog(@"OK");
//保存成功
dispatch_async(dispatch_get_main_queue(), ^{
//通知外部一個略縮圖
[self generateThumbnailForVideoAtURL:videoURL];
});
}];
}
複製代碼
///經過視頻獲取視頻的第一幀圖片當作略縮圖
- (void)generateThumbnailForVideoAtURL:(NSURL *)videoURL {
dispatch_async(self.videoQueue, ^{
//拿到視頻信息
AVAsset *asset = [AVAsset assetWithURL:videoURL];
AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
imageGenerator.maximumSize = CGSizeMake(100, 0);
imageGenerator.appliesPreferredTrackTransform = YES;
//經過視頻將第一幀圖片數據轉化爲CGImage
CGImageRef imageRef = [imageGenerator copyCGImageAtTime:kCMTimeZero actualTime:NULL error:nil];
UIImage *image = [UIImage imageWithCGImage:imageRef];
//通知外部
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:ThumbnailCreatedNotification object:image];
});
}
複製代碼
上文主要是經過AVCaptureMovieFileOutput將QuickTime影片捕捉到磁盤,這個類大多數核心功能繼承與超類AVCaptureFileOutput。它有不少實用的功能,例如:錄製到最長時限或錄製到特定文件大小爲止。session
一般當QuickTime影片準備發佈時,影片頭的元數據處於文件的開始位置。這樣可讓視頻播放器快速讀取頭包含信息,來肯定文件的內容、結構和其包含的多個樣本的位置。當錄製一個QuickTime影片時,直到全部的樣片都完成捕捉後才能建立信息頭。當錄製結束時,建立頭數據並將它附在文件結尾。app
將建立頭的過程放在全部影片樣本完成捕捉以後存在一個問題。 在移動設備中,好比錄製的時候接到電話或者程序崩潰等問題,影片頭就不能被正確寫入。會在磁盤生成一個不可讀的影片文件。AVCaptureMovieFileOutput提供一個核心功能就是分段捕捉QuickTime影片。async
人臉識別其實是很是複雜的一個功能,要想本身徹底實現人臉識別是很是困難的。蘋果爲咱們作了不少人臉識別的功能,例如CoreImage、AVFoundation,都是有人臉識別的功能的。還有Vision face++ 等。這裏就簡單介紹一下AVFoundation中的人臉識別。ide
在拍攝視頻中,咱們經過AVFoundation的人臉識別,在屏幕界面上用一個紅色矩形來標識識別到的人臉。post
- (BOOL)setupSessionOutputs:(NSError **)error {
//配置輸入信息
self.metadataOutput = [[AVCaptureMetadataOutput alloc] init];
//對session添加輸出
if ([self.captureSession canAddOutput:self.metadataOutput]) {
[self.captureSession addOutput:self.metadataOutput];
//從輸出數據中設置只獲取人臉數據(能夠是人臉、二維碼、一維碼....)
NSArray *metadataObjectType = @[AVMetadataObjectTypeFace];
self.metadataOutput.metadataObjectTypes = metadataObjectType;
//由於人臉檢測使用了硬件加速器GPU, 因此它的任務須要在主線程中執行
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//設置metadataOutput代理方法, 檢測視頻中一幀一幀數據裏是否包含人臉數據. 若是包含則調用回調方法
[self.metadataOutput setMetadataObjectsDelegate:self queue:mainQueue];
return YES;
} else {
//錯誤回調
}
return NO;
}
複製代碼
- (void)captureOutput:(AVCaptureOutput *)output didOutputMetadataObjects:(NSArray<__kindof AVMetadataObject *> *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
//metadataObjects包含了捕獲到的人臉數據(人臉數據會重複, 會一直捕獲人臉數據)
for (AVMetadataFaceObject *face in metadataObjects) {
NSLog(@"Face ID:%li",(long)face.faceID);
}
//將人臉數據經過代理髮送給外部的layer層
[self.faceDetectionDelegate didDetectFaces:metadataObjects];
}
複製代碼
- (void)setupView {
//用來記錄人臉圖層
self.faceLayers = [NSMutableDictionary dictionary];
//圖層的填充方式: 設置videoGravity 使用AVLayerVideoGravityResizeAspectFill 鋪滿整個預覽層的邊界範圍
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
//在previewLayer上添加一個透明的圖層
self.overlayLayer = [CALayer layer];
self.overlayLayer.frame = self.bounds;
//假設你的圖層上的圖形會發生3D變換, 設置投影方式
self.overlayLayer.sublayerTransform = CATransform3DMakePerspective(1000);
[self.previewLayer addSublayer:self.overlayLayer];
}
static CATransform3D CATransform3DMakePerspective(CGFloat eyePosition) {
//CATransform3D 圖層的旋轉,縮放,偏移,歪斜和應用的透
//CATransform3DIdentity是單位矩陣,該矩陣沒有縮放,旋轉,歪斜,透視。該矩陣應用到圖層上,就是設置默認值。
CATransform3D transform = CATransform3DIdentity;
//透視效果(就是近大遠小),是經過設置m34 m34 = -1.0/D 默認是0.D越小透視效果越明顯
//D:eyePosition 觀察者到投射面的距離
transform.m34 = -1.0/eyePosition;
return transform;
}
複製代碼
注意:此處省略了一些3D轉換的方法ui
- (void)didDetectFaces:(NSArray *)faces {
//人臉數據位置信息(攝像頭座標系)轉換爲屏幕座標系
NSArray *transfromedFaces = [self transformedFacesFromFaces:faces];
//人臉消失, 刪除圖層
//須要刪除的人臉數據列表
NSMutableArray *lostFaces = [self.faceLayers.allValues mutableCopy];
//遍歷每一個人臉數據
for (AVMetadataFaceObject *face in transfromedFaces) {
//face ID
NSNumber *faceID = @(face.faceID);
//face ID存在即不須要刪除(從刪除列表中移除)
[lostFaces removeObject:faceID];
//假若有新的人臉加入
CALayer *layer = self.faceLayers[faceID];
if (!layer) {
NSLog(@"新增人臉");
layer = [self makeFaceLayer];
[self.overlayLayer addSublayer:layer];
//更新字典
self.faceLayers[faceID] = layer;
}
//根據人臉的bounds設置layer的frame
layer.frame = face.bounds;
CGSize size = self.bounds.size;
//當人臉特別靠近屏幕邊緣, 直接看成沒法識別此人臉(由於人臉離開屏幕不會走此代理方法, 須要提早作移除)
if (face.bounds.origin.x < 3 ||
face.bounds.origin.x > size.width - layer.frame.size.width - 3 ||
face.bounds.origin.y < 3 ||
face.bounds.origin.y > size.height - layer.frame.size.height - 3 ) {
[layer removeFromSuperlayer];
[self.faceLayers removeObjectForKey:faceID];
}
//設置3D屬性(人臉是3D的, 須要根據人臉的3D變化作不一樣的變化處理)
layer.transform = CATransform3DIdentity;
//人臉z軸變化
if (face.hasRollAngle) {
CATransform3D t = [self transformForRollAngle:face.rollAngle];
//矩陣相乘
layer.transform = CATransform3DConcat(layer.transform, t);
}
//人臉y軸變化
if (face.hasYawAngle) {
CATransform3D t = [self transformForYawAngle:face.hasYawAngle];
//矩陣相乘
layer.transform = CATransform3DConcat(layer.transform, t);
}
}
//處理已經從鏡頭消失的人臉(人臉消失,圖層並無消失)
for (NSNumber *faceID in lostFaces) {
CALayer *layer = self.faceLayers[faceID];
[self.faceLayers removeObjectForKey:faceID];
[layer removeFromSuperlayer];
}
}
複製代碼
有的同窗在設置AVMetadataObjectTypeFace的可能會發現,還有會有一些其餘的類型,例如AVMetadataObjectTypeQRCode等,就是從攝像頭中捕獲二維碼數據,它的流程與人臉識別極度類似,甚至要更爲簡單一些,由於二維碼並不像人臉同樣須要作一些3D的轉換等操做,因此此處再也不示例捕捉二維碼。url