AVFoundation開發祕籍筆記-06捕捉媒體
一、捕捉功能
1、捕捉會話 AVCaptureSession
AVFoundation捕捉棧的核心類是AVCaptureSession
。一個捕捉會話相當於一個虛擬的“插線板”,用於連線輸入和輸出的資源。
捕捉會話管理從屋裡裝置得到的資料流,比如攝像頭和麥克風裝置,輸出到一個或多個目的地。可以動態配置輸入和輸出的線路,可以再會話進行中按需配置捕捉環境。
捕捉會話還可以額外配置一個會話預設值(session preset),用來控制捕捉資料的格式和質量。會話預設值預設為AVCaptureSessionPresetHigh
,適用於大多數情況。還有很多預設值,可以根據需求設定。
2、捕捉裝置 AVCaptureDevice
AVCaptureDevice
為攝像頭或麥克風等物理裝置定義了一個介面。對硬體裝置定義了大量的控制方法,如對焦、曝光、白平衡和閃光燈等。
AVCaptureDevice
定義大量類方法用用訪問系統的捕捉裝置,最常用的是defaultDeviceWithMediaType:
,根據給定的媒體型別返回一個系統指定的預設裝置
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
請求的是一個預設的視訊裝置,在包含前置和後置攝像頭的iOS系統,返回後置攝像頭。
3、捕捉裝置的輸入 AVCaptureInput
AVCaptureInput
是一個抽象類,提供一個連線介面將捕獲到的輸入源連線到AVCaptureSession
。
抽象類無法直接使用,只能通過其子類滿足需求:AVCaptureDeviceInput
-使用該物件從AVCaptureDevice
獲取裝置資料(攝像頭、麥克風等)、AVCaptureScreenInput
-通過螢幕獲取資料(如錄屏)、AVCaptureMetaDataInput
-獲取元資料
- 以 AVCaptureDeviceInput 為例
使用捕捉裝置進行處理前,需要將它新增為捕捉會話的輸入。通過將裝置(AVCaptureDevice
)封裝到AVCaptureDeviceInput
AVCaptureSession
中。
AVCaptureDeviceInput
在裝置輸出資料和捕捉會話間,扮演接線板的作用。
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
4、捕捉的輸出 AVCaptureOutput
AVCaptureOutput
是一個抽象基類,用於從捕捉會話得到的資料尋找輸出目的地。
框架定義一些這個基類的高階擴充套件類,比如
AVCaptureStillImageOutput
用來捕捉靜態圖片,AVCaptureMovieFileOutput
捕捉視訊
還有一些底層擴充套件,如AVCaptureAudioDataOutput
和AVCaptureVideoDataOutput
使用它們可以直接訪問硬體捕捉到的數字樣本。使用底層輸出類需要對捕捉裝置的資料渲染有更好的理解,不過這些類可以提供更強大的功能,比如對音訊和視訊流進行實時處理。
5、捕捉連線 AVCaptureConnection
AVCaptureConnection
連線
捕捉會話首先確定有給定捕捉裝置輸入渲染的媒體型別,並自動建立其到能夠接收該媒體型別的捕捉輸出端的連線。
對連線的訪問可以對訊號流進行底層的空值,比如禁用某些特定的連線,或者再音訊連線中訪問單獨的音訊軌道(一些高階用法,不糾結)。
- 附加
AVCaptureConnection
解決一個影象旋轉90°的問題:(setVideoOrientation:
方法)
AVCaptureConnection *stillImageConnection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
AVCaptureVideoOrientation avcaptureOrientation = [self avOrientationForDeviceOrientation:UIDeviceOrientationPortrait];
[stillImageConnection setVideoOrientation:avcaptureOrientation];
6、捕捉預覽 AVCaptureVideoPreviewLayer
AVCaptureVideoPreviewLayer
是一個CoreAnimation
的CALayer
的子類,對捕捉視訊資料進行實時預覽。
類似於AVPlayerLayer
,不過針對攝像頭捕捉的需求進行了定製。他也支援視訊重力概念setVideoGravity:
- AVLayerVideoGravityResizeAspect –在承載層範圍內縮放視訊大小來保持視訊原始寬高比,預設值,適用於大部分情況
- AVLayerVideoGravityResizeAspectFill –保留視訊寬高比,通過縮放填滿層的範圍區域,會導致視訊圖片被部分裁剪。
- AVLayerVideoGravityResize –拉伸視訊內容拼配承載層的範圍,會導致圖片扭曲,funhouse effect效應。
二、建立簡單捕捉會話
當如庫檔案 #import <AVFoundation/AVFoundation.h>
1、建立捕捉會話 AVCaptureSession,可以設定為成員變數,開始會話以及停止會話都是用到例項物件。
AVCaptureSession *session = [[AVCaptureSession alloc] init];
2、建立獲取捕捉裝置 AVCaptureDevice
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
3、建立捕捉輸入 AVCaptureDeviceInput
NSError *error;
AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
- 4、將捕捉輸入加到會話中
if ([session canAddInput:input]) {
//首先檢測是否能夠新增輸入,直接新增可能會有crash
[session addInput:input];
}
- 5、建立一個靜態圖片輸出
AVCaptureStillImageOutput
AVCaptureStillImageOutput *imageOutput = [[AVCaptureStillImageOutput alloc] init];
- 6、將捕捉輸出新增到會話中
if ([session canAddOutput:imageOutput]) {
//檢測是否可以新增輸出
[session addOutput:imageOutput];
}
- 7、建立影象預覽層
AVCaptureVideoPreviewLayer
AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
previewLayer.frame = self.view.frame;
[self.view.layer addSublayer:previewLayer];
- 8、開始會話
[session startRunning];
開始之前先獲取裝置攝像頭許可權。info.plist
中新增Privacy - Camera Usage Description
。
這裡只是實現捕捉流程,梳理核心元件的關係,沒有任何操作。典型的會話建立過程會更復雜,這是毋庸置疑的。當開始執行會話,視訊資料流就可以再系統中傳輸。
三、建立一個簡單的拍照視訊專案
整個的邏輯依舊是上面的幾步,更多的是一些新的屬性設定,因為是簡單專案,所以,只是實現了功能,並沒有作具體的優化。怎麼簡單怎麼來,主要是熟悉一下主要功能。
1、建立捕捉會話
專案裡不只是要實現靜態圖片捕捉,還會有視訊拍攝,所以還有視訊和音訊輸入。
就是前面說的【建立簡單會話】流程的升級版,可以同時給會話新增多個輸入和多個輸出,然後分別單獨處理。
self.captureSession = [[AVCaptureSession alloc] init];
self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
//獲取裝置攝像頭
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// 得到一個指向預設視訊捕捉裝置的指標。
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
//將裝置新增到Session之前,先封裝到AVCaptureDeviceInput物件
if (videoInput) {
if ([self.captureSession canAddInput:videoInput]) {
[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]) {
//對於有效的input,新增到會話並給它傳遞捕捉裝置的輸入資訊
[self.captureSession addInput:audioInput];
}
} else {
return NO ;
}
//設定 靜態圖片輸出
self.stillImageOutput = [[AVCaptureStillImageOutput alloc] init];
self.stillImageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
//配置字典表示希望捕捉JPEG格式圖片
if ([self.captureSession canAddOutput:self.stillImageOutput]) {
// 測試輸出是否可以新增到捕捉對話,然後再新增
[self.captureSession addOutput:self.stillImageOutput];
}
//設定視訊檔案輸出
self.movieOutput = [[AVCaptureMovieFileOutput alloc] init];
if ([self.captureSession canAddOutput:self.movieOutput]) {
[self.captureSession addOutput:self.movieOutput];
NSLog(@"add movie output success");
}
2、開始和結束會話
- (dispatch_queue_t)globalQueue {
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
}
//開始捕捉會話
- (void)startSession {
if (![self.captureSession isRunning]) {
dispatch_async([self globalQueue], ^{
//開始會話 同步呼叫會消耗一定時間,所以用非同步方式在videoQueue排隊呼叫該方法,不會阻塞主執行緒。
[self.captureSession startRunning];
});
}
}
//停止捕捉會話
- (void)stopSession {
if ([self.captureSession isRunning]) {
dispatch_async([self globalQueue], ^{
[self.captureSession stopRunning];
});
}
}
3、切換攝像頭
切換前置和後置攝像頭需要重新配置捕捉回話,可以動態重新配置AVCaptureSession,不必擔心停止會話和重新啟動會話帶來的開銷。
對會話進行的任何改變,都要通beginConfiguration
和commitConfiguration
,進行單獨的、原子性的變化。
- (BOOL)switchCameras { //驗證是否有可切換的攝像頭
if (![self canSwitchCameras]) {
return NO;
}
NSError *error;
AVCaptureDevice *videoDevice = [self inactiveCamera];
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:&error];
if (videoInput) {
[self.captureSession beginConfiguration];
// 標註源自配置變化的開始
[self.captureSession removeInput:self.activeVideoInput];
if ([self.captureSession canAddInput:videoInput]) {
[self.captureSession addInput:videoInput];
self.activeVideoInput = videoInput;
} else if (self.activeVideoInput) {
[self.captureSession addInput:self.activeVideoInput];
}
[self.captureSession commitConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
return NO;
}
return YES;
}
// 返回指定位置的AVCaptureDevice 有效位置為 AVCaptureDevicePositionFront 和AVCaptureDevicePositionBack,遍歷可用視訊裝置,並返回position引數對應的值
- (AVCaptureDevice *)cameraWithPosition:(AVCaptureDevicePosition)position {
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if (device.position == position) {
return device;
}
}
return nil;
}
// 當前捕捉會話對應的攝像頭,返回啟用的捕捉裝置輸入的device屬性
- (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)canSwitchCameras {
return self.cameraCount > 1;
}
// 返回可用視訊捕捉裝置的數量
- (NSUInteger)cameraCount {
return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}
4、捕獲靜態圖片
AVCaptureConnection,當建立一個會話並新增捕捉裝置輸入和捕捉輸出時,會話自動建立輸入和輸出的連結,按需選擇訊號流線路。訪問這些連線,可以更好地對傳送到輸出端的資料進行控制。
CMSampleBuffer
是有CoreMedia
框架定義的CoreFoundation
物件。可以用來儲存捕捉到的圖片資料。圖片格式根據輸出物件設定的格式決定。
- (void)captureStillImage {
NSLog(@"still Image");
AVCaptureConnection *connection = [self.stillImageOutput connectionWithMediaType:AVMediaTypeVideo];
if (connection.isVideoOrientationSupported) {
connection.videoOrientation = [self currentVideoOrientation];
}
id handler = ^(CMSampleBufferRef sampleBuffer,NSError *error) {
if (sampleBuffer != NULL) {
NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:sampleBuffer];
UIImage *image = [UIImage imageWithData:imageData];
//這就得到了拍攝到的圖片,可以做響應處理。
} else {
NSLog(@"NULL sampleBuffer :%@",[error localizedDescription]);
}
};
[self.stillImageOutput captureStillImageAsynchronouslyFromConnection:connection completionHandler:handler];
}
處理圖片方向問題。
- (AVCaptureVideoOrientation)currentVideoOrientation {
AVCaptureVideoOrientation orientation;
switch ([[UIDevice currentDevice] orientation]) {
case UIDeviceOrientationPortrait:
orientation = AVCaptureVideoOrientationPortrait;
break;
case UIDeviceOrientationLandscapeRight:
orientation = AVCaptureVideoOrientationLandscapeLeft;
break;
case UIDeviceOrientationPortraitUpsideDown:
orientation = AVCaptureVideoOrientationPortraitUpsideDown;
break;
default:
orientation = AVCaptureVideoOrientationLandscapeRight;
break;
}
return orientation;
}
5、錄製視訊
視訊內容捕捉,設定捕捉會話,新增名為AVCaptureMovieFileOutput
的輸出。將QuickTime影片捕捉大磁碟,這個類的大多數核心功能繼承與超類AVCaptureFileOutput
。
通常當QuickTime應聘準備釋出時,影片頭的元資料處於檔案的開始位置,有利於視訊播放器快速讀取頭包含的資訊。錄製的過程中,知道所有的樣本都完成捕捉後才能建立資訊頭。
- (void)startRecording {
if (![self isRecording]) {
AVCaptureConnection *videoConnection = [self.movieOutput connectionWithMediaType:AVMediaTypeVideo];
if ([videoConnection isVideoOrientationSupported]) {
videoConnection.videoOrientation = [self currentVideoOrientation];
}
if ([videoConnection isVideoStabilizationSupported]) {
videoConnection.preferredVideoStabilizationMode = YES;
}
//如果支援preferredVideoStabilizationMode,設定為YES。支援視訊穩定可以顯著提升捕捉到的視訊質量。
// 只在錄製視訊檔案時才會涉及。
AVCaptureDevice *device = [self activeCamera];
if (device.isSmoothAutoFocusEnabled) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.smoothAutoFocusEnabled = YES;
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
//攝像頭可以進行平滑對焦模式的操作,減慢攝像頭鏡頭對焦的速度。
//通常情況下,使用者移動拍攝時攝像頭會嘗試快速自動對焦,這會在捕捉視訊中出現脈衝式效果。
//當平滑對焦時,會較低對焦操作的速率,從而提供更加自然的視訊錄製效果。
}
self.outputURL = [self uniqueURL];
NSLog(@"url %@",self.outputURL);
[self.movieOutput startRecordingToOutputFileURL:self.outputURL recordingDelegate:self];
// 查詢寫入捕捉視訊的唯一檔案系統URL。保持對地址的強引用,這個地址在後面處理視訊時會用到
// 新增代理,處理回撥結果。
}
}
// 獲取錄製時間
- (CMTime)recordedDuration {
return self.movieOutput.recordedDuration;
}
// 設定儲存路徑
- (NSURL *)uniqueURL {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *directionPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"camera_movie"];
NSLog(@"unique url :%@",directionPath);
if (![fileManager fileExistsAtPath:directionPath]) {
[fileManager createDirectoryAtPath:directionPath withIntermediateDirectories:YES attributes:nil error:nil];
}
NSString *filePath = [directionPath stringByAppendingPathComponent:@"camera_movie.mov"];
if ([fileManager fileExistsAtPath:filePath]) {
[fileManager removeItemAtPath:filePath error:nil];
}
return [NSURL fileURLWithPath:filePath];
return nil;
}
// 停止錄製
- (void)stopRecording {
if ([self isRecording]) {
[self.movieOutput stopRecording];
}
}
// 驗證錄製狀態
- (BOOL)isRecording {
return self.movieOutput.isRecording;
}
代理回撥,拿到錄製視訊的地址。
#pragma mark -- AVCaptureFileOutputRecordingDelegate
// 錄製完成
- (void)captureOutput:(AVCaptureFileOutput *)output didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray<AVCaptureConnection *> *)connections error:(NSError *)error
{
NSLog(@"capture output");
if (error) {
NSLog(@"record error :%@",error);
[self.delegate mediaCaptureFailedWithError:error];
} else {
// 沒有錯誤的話在儲存響應的路徑下已經完成視訊錄製,可以通過url訪問該檔案。
}
self.outputURL = nil;
}
6、將圖片和視訊儲存到相簿
將拍攝到的圖片和視訊可以通過這個系統庫儲存到相簿。
不過AssetsLibrary
在iOS9.0之後就被棄用了,可以使用從iOS8.0支援的Photos/Photos.h
庫來實現圖片和視訊的儲存。
- (void)writeImageToAssetsLibrary:(UIImage *)image {
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library writeImageToSavedPhotosAlbum:image.CGImage orientation:(NSInteger)image.imageOrientation completionBlock:^(NSURL *assetURL, NSError *error) {
if (!error) {
} else {
NSLog(@"Error :%@",[error localizedDescription]);
}
}];
}
- (void)writeVideoToAssetsLibrary:(NSURL *)videoUrl {
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
if ([library videoAtPathIsCompatibleWithSavedPhotosAlbum:videoUrl]) {
//檢驗是否可以寫入
ALAssetsLibraryWriteVideoCompletionBlock completionBlock;
completionBlock = ^(NSURL *assetURL, NSError *error) {
if (error) {
[self.delegate asssetLibraryWriteFailedWithError:error];
} else {
}
};
[library writeVideoAtPathToSavedPhotosAlbum:videoUrl completionBlock:completionBlock];
}
}
Photos/Photos.h
實現圖片和視訊儲存
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromImage:image];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
NSLog(@"success :%d ,error :%@",success,error);
if (success) {
// DO:
}
}];
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromVideoAtFileURL:videoUrl];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
if (success) {
// DO:
[self generateThumbnailForVideoAtURL:videoUrl];
} else {
[self.delegate asssetLibraryWriteFailedWithError:error];
NSLog(@"video save error :%@",error);
}
}];
7、關於閃光燈和手電筒的設定
裝置後面的LED燈,當拍攝靜態圖片時作為閃光燈,當拍攝視訊時用作連續燈光(手電筒).捕捉裝置的flashMode和torchMode。
- AVCapture(Flash|Torch)ModeAuto:基於周圍環境光照情況自動關閉或開啟
- AVCapture(Flash|Torch)ModeOff:總是關閉
- AVCapture(Flash|Torch)ModeOn:總是開啟
修改閃光燈或手電筒設定的時候,一定要先鎖定裝置再修改,否則會掛掉。
- (BOOL)cameraHasFlash {
return [[self activeCamera] hasFlash];
}
- (AVCaptureFlashMode)flashMode {
return [[self activeCamera] flashMode];
}
- (void)setFlashMode:(AVCaptureFlashMode)flashMode {
AVCaptureDevice *device = [self activeCamera];
if ([device isFlashModeSupported:flashMode]) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.flashMode = flashMode;
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
- (BOOL)cameraHasTorch {
return [[self activeCamera] hasTorch];
}
- (AVCaptureTorchMode)torchMode {
return [[self activeCamera] torchMode];
}
- (void)setTorchMode:(AVCaptureTorchMode)torchMode {
AVCaptureDevice *device = [self activeCamera];
if ([device isTorchModeSupported:torchMode]) {
NSError *error;
if ([device lockForConfiguration:&error]) {
device.torchMode = torchMode;
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
8、其他一些設定
還有許多可以設定的屬性,比如聚焦、曝光等等,設定起來差不多,首先要檢測裝置(攝像頭)是否支援相應功能,鎖定裝置,而後設定相關屬性。
再以對焦為例
// 詢問啟用中的攝像頭是否支援興趣點對焦
- (BOOL)cameraSupportsTapToFocus {
return [[self activeCamera] isFocusPointOfInterestSupported];
}
// 點的座標已經從螢幕座標轉換為捕捉裝置座標。
- (void)focusAtPoint:(CGPoint)point {
AVCaptureDevice *device = [self activeCamera];
if (device.isFocusPointOfInterestSupported && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
// 確認是否支援興趣點對焦並確認是否支援自動對焦模式。
// 這一模式會使用單獨掃描的自動對焦,並將focusMode設定為AVCaptureFocusModeLocked
NSError *error;
if ([device lockForConfiguration:&error]) {
//鎖定裝置準備配置
device.focusPointOfInterest = point;
device.focusMode = AVCaptureFocusModeAutoFocus;
[device unlockForConfiguration];
} else {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
關於螢幕座標與裝置座標的轉換
captureDevicePointOfInterestForPoint:
–獲取螢幕座標系的CGPoint資料,返回轉換得到的裝置座標系CGPoint資料
pointForCaptureDevicePointOfInterest:
–獲取社小偷座標系的CGPoint資料,返回轉換得到的螢幕座標系CGPoint資料