「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
-
突然发现很久没有写文章记录了,2021的flag凉凉,去年换到了新的公司,开始大量接触iOS音视频相关,之前对音视频的接触并不多,所以除了上班就是学习了,赶在更文挑战即将结束前,总结下最近的学习。
AVFoundation
- AVFoundation是苹果OS X和iOS系统中用于处理基于时间的媒体数据的高级OC框架。通过开发所需的工具提供了大量功能集,让开发者能够基于苹果平台创建当下最先进的媒体应用程序。AVFoundation充分利用了苹果A系列处理器的优势...好了,不扯了,总而言之就是很厉害,做iOS音视频开发,你就是必须得学。
AVFoundation捕捉
- AVFoundation核心功能之一
- 短视频App、直播App、相机App都需要用到
- 这篇文章做一个简单的相机,实现基本的拍摄、对焦、测光、手电筒等功能
捕捉用到的类
- 捕捉会话:AVCaptureSession
- 捕捉设备:AVCaptureDevice
- 捕捉设备输入:AVCaptureDeviceInput
- 捕捉设备输出:AVCaptureOutput 抽象类,使用下面4个
- AVCaptureStillImageOutput 静态图片
- AVCaputureMovieFileOutput 电影文件
- AVCaputureAudioDataOutput 音频数据
- AVCaputureVideoDataOutput 视频数据
- 捕捉链接:AVCaptureConnection
- 捕捉预览:AVCaptureVideoPreviewLayer
注意
- AVCaputureAudioDataOutput和AVCaputureMovieFileOutput不能同时使用,如果同时设置了,可以实现‘系统视频录制功能’但是却不再输出‘实时视频流’,如果要同时实现‘视频录制’和‘实时视频流’,可以使用AVCaptureVideoDataOutput代理的sampleBuffer作为数据源,使用videoToolBox编码写入视频文件。关于VideoToolBox H264视频硬编码
捕捉预览层
- 预览层提供预览功能以及对焦曝光拍摄等相关操作,将手指点击曝光对角点传递给AVCaptureDevice
- 摄像头坐标系和屏幕坐标系相互转换 ,这是一个很困难的事情,要考虑镜像、方向、重力、图层变化等等因素,iOS6之后,AVFoundation/AVCaptureVideoPreviewLayer 提供了方法解决,当然有些需求不能使用AVCaptureVideoPreviewLayer,还得自己计算,这个以后再说
/**
摄像头坐标系转屏幕坐标
@param pointInLayer 摄像头坐标
@return 屏幕坐标系
*/
- (CGPoint)captureDevicePointOfInterestForPoint:(CGPoint)pointInLayer API_AVAILABLE(macos(10.15), ios(6.0), macCatalyst(14.0)) API_UNAVAILABLE(tvos);
/**
摄像头坐标系转屏幕坐标
@param pointInLayer 摄像头坐标
@return 屏幕坐标系
*/
- (CGPoint)pointForCaptureDevicePointOfInterest:(CGPoint)captureDevicePointOfInterest API_AVAILABLE(macos(10.15), ios(6.0), macCatalyst(14.0)) API_UNAVAILABLE(tvos);
设置捕捉会话
- 初始化
- 设置分辨率
- 设置视频音频输入设备
- 注意:1-会话不能直接使用AVCaptureDevice,必须将AVCaptureDevice封装成AVCaptureDeviceInput对象;2-使用前判断videoInput是否有效以及能否添加,因为摄像头是一个公共设备,不属于任何App,有可能别的App在使用,添加前应该先进行判断是否可以添加)
- 设置图片视频输出
// 创建捕捉会话 AVCaptureSession 捕捉场景的中心枢纽
#pragma mark - Public Func 设置会话
// 设置会话,设置分辨率,并将输入输出添加到会话中
- (BOOL)setupSession:(NSError * _Nullable *)error {
// 创建捕捉会话 AVCaptureSession 是捕捉场景的中心枢纽
self.captureSession = [[AVCaptureSession alloc] init];
/*
AVCaptureSessionPresetHigh
AVCaptureSessionPresetMedium
AVCaptureSessionPresetLow
AVCaptureSessionPreset640x480
AVCaptureSessionPreset1280x720
AVCaptureSessionPresetPhoto
*/
// 设置图像分辨率
self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
// 设置视频音频输入
// 添加视频捕捉设备
// 拿到默认视频捕捉设备 iOS默认后置摄像头
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// 将捕捉设备转化为AVCaptureDeviceInput
// 注意:会话不能直接使用AVCaptureDevice,必须将AVCaptureDevice封装成AVCaptureDeviceInput对象
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
// 将捕捉设备添加给会话
// 使用前判断videoInput是否有效以及能否添加,因为摄像头是一个公共设备,不属于任何App,有可能别的App在使用,添加前应该先进行判断是否可以添加
if (videoInput && [self.captureSession canAddInput:videoInput]) {
// 将videoInput 添加到 captureSession中
[self.captureSession addInput:videoInput];
self.videoDeviceInput = videoInput;
}else {
return NO;
}
// 添加音频捕捉设备
// 选择默认音频捕捉设备 即返回一个内置麦克风
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];
if (audioInput && [self.captureSession canAddInput:audioInput]) {
[self.captureSession addInput:audioInput];
}else {
return NO;
}
// 设置输出(图片/视频)
// AVCaptureStillImageOutput 从摄像头捕捉静态图片
self.imageOutput = [[AVCaptureStillImageOutput alloc] init];
// 配置字典:希望捕捉到JPEG格式的图片
self.imageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
// 输出连接 判断是否可用,可用则添加到输出连接中去
if ([self.captureSession canAddOutput:self.imageOutput]) {
[self.captureSession addOutput:self.imageOutput];
}
// AVCaptureMovieFileOutput,将QuickTime视频录制到文件系统
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.videoQueue, ^{
[self.captureSession startRunning];
});
}
}
// 停止会话
- (void)stopSession {
// 检查是否处于运行状态
if ([self.captureSession isRunning]) {
dispatch_async(self.videoQueue, ^{
[self.captureSession stopRunning];
});
}
}
- 因为涉及摄像头、相册、麦克风,需要给用户提醒,处理隐私需求
- 隐私需求:通讯录、麦克风、相册、相机、地理位置、使用期间访问地理位置、日历、注意事项、蓝牙,修改plist文件
切换摄像头
- 移除了才能添加新的
- 移除了也有可能无法添加,例如设置分辨率4K,而有的手机前置摄像头不支持就不行
- 已经移除了,还是无法添加新设备,则将原本的视频捕捉设备重新加入到捕捉会话中
#pragma mark - Public Func 镜头切换
/// 根据position拿到摄像头
- (AVCaptureDevice *)getCameraWithPosition:(AVCaptureDevicePosition)position {
NSArray<AVCaptureDevice *> *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
for (AVCaptureDevice *device in devices) {
if (device.position == position) {
return device;
}
}
return nil;
}
/// 获取当前活跃的摄像头
- (AVCaptureDevice *)getActiveCamera {
return self.videoDeviceInput.device;
}
/// 获取未激活的摄像头
- (AVCaptureDevice *)getInactiveCamera {
// 通过查找当前激活摄像头的反向摄像头获得,如果设备只有1个摄像头,则返回nil
AVCaptureDevice *device = nil;
if (self.cameraCount > 1) {
if ([self getActiveCamera].position == AVCaptureDevicePositionBack) {
device = [self getCameraWithPosition:AVCaptureDevicePositionFront];
} else {
device = [self getCameraWithPosition:AVCaptureDevicePositionBack];
}
}
return device;
}
// 切换摄像头
- (BOOL)switchCamera {
if (![self canSwitchCamera]) return NO;
// 获取当前设备的反向设备
AVCaptureDevice *inactiveCamera = [self getInactiveCamera];
// 将输入设备封装成AVCaptureDeviceInput
NSError *error;
AVCaptureDeviceInput *newVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:inactiveCamera error:&error];
if (newVideoInput != nil) {
// 开始配置 标注原始配置要发生改变
[self.captureSession beginConfiguration];
// TODO: 是不是移除了才能加新的?
// FIXME: 是不是移除了才能加新的?
// 将捕捉会话中,原本的捕捉输入设备移除
[self.captureSession removeInput:self.videoDeviceInput];
if ([self.captureSession canAddInput:newVideoInput]) {
[self.captureSession addInput:newVideoInput];
self.videoDeviceInput = newVideoInput;
} else {
// !!!: 是不是要给个回调?
// ???: 是不是要给个回调?
// 已经移除了,还是无法添加新设备,则将原本的视频捕捉设备重新加入到捕捉会话中
[self.captureSession addInput:self.videoDeviceInput];
}
// 提交配置,AVCaptureSession commitConfiguration 会分批的将所有变更整合在一起。
[self.captureSession commitConfiguration];
return YES;
} else {
// 创建AVCaptureDeviceInput 出现错误,回调该错误
[self.delegate deviceConfigurationFailedWithError:error];
return NO;
}
}
// 是否能切换摄像头
- (BOOL)canSwitchCamera {
return self.cameraCount > 1;
}
- (NSUInteger)cameraCount {
return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}
捕捉设备的聚焦/曝光
- AVCaptureDevice定义了很多方法,让开发者控制ios设备上的摄像头。可以独立调整和锁定摄像头的焦距、曝光、白平衡。对焦和曝光可以基于特定的兴趣点进行设置,使其在应用中实现点击对焦、点击曝光的功能。
- 还可以让你控制设备的LED作为拍照的闪光灯或手电筒的使用。
- 每当修改摄像头设备时,一定要先测试修改动作是否能被设备支持。并不是所有的摄像头都支持所有功能,例如部分设备前置摄像头就不支持对焦操作,因为它和目标距离一般在一臂之长的距离。但大部分后置摄像头是可以支持全尺寸对焦。其次会话设置的分辨率摄像头不支持也是无法添加的,尝试应用一个不被支持的动作,会导致异常崩溃。所以修改摄像头设备前,需要判断是否支持。
#pragma mark - Public Func 对焦&曝光
// 设置对焦点
- (void)focusAtPoint:(CGPoint)point {
AVCaptureDevice *device = [self getActiveCamera];
// 摄像头是否支持兴趣点对焦 & 是否支持自动对焦模式 ,不支持不操作,玩手动对焦的需求另说
if (!device.isFocusPointOfInterestSupported || ![device isFocusModeSupported:AVCaptureFocusModeAutoFocus]) return;
NSError *error;
// 锁定设备准备配置,因为配置时不能让多个地方对同一个设备更改,所以需要加锁
if ([device lockForConfiguration:&error]) {
// 设置对焦点
device.focusPointOfInterest = point;
// 对焦模式设置为自动对焦
device.focusMode = AVCaptureFocusModeAutoFocus;
// 释放锁定
[device unlockForConfiguration];
} else {
// 锁定错误时,回调错误
if (self.delegate && [self.delegate respondsToSelector:@selector(deviceConfigurationFailedWithError:)]) {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
// 设置曝光点
- (void)exposeAtPoint:(CGPoint)point {
AVCaptureDevice *device = [self getActiveCamera];
// 摄像头是否支持兴趣点曝光 & 是否支持自动曝光模式 ,不支持不操作,玩手动曝光的需求另说
if (!device.isExposurePointOfInterestSupported || ![device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) return;
NSError *error;
// 锁定设备准备配置
if ([device lockForConfiguration:&error]) {
// 设置曝光点,针对该点进行自动曝光
device.exposurePointOfInterest = point;
// 曝光模式设置为自动曝光
device.exposureMode = AVCaptureExposureModeContinuousAutoExposure;
// 判断设备是否支持锁定曝光的模式。
if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) {
// 支持,则使用kvo监听设备的曝光调节状态。
[device addObserver:self forKeyPath:@"adjustingExposure" options:NSKeyValueObservingOptionNew context:&CameraAdjustingExposureContext];
}
// 释放锁定
[device unlockForConfiguration];
} else {
// 锁定错误时,回调错误处理代理
if (self.delegate && [self.delegate respondsToSelector:@selector(deviceConfigurationFailedWithError:)]) {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == &CameraAdjustingExposureContext) {
//获取device
AVCaptureDevice *device = (AVCaptureDevice *)object;
// 设备不再调整曝光等级,说明自动调节曝光结束,并且支持设置为AVCaptureExposureModeLocked
// TODO: -测试监听次数
if(!device.isAdjustingExposure && [device isExposureModeSupported:AVCaptureExposureModeLocked] && device.isExposurePointOfInterestSupported) {
// 使用一次监听,立即移除通知
[object removeObserver:self forKeyPath:@"adjustingExposure" context:&CameraAdjustingExposureContext];
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error;
if ([device lockForConfiguration:&error]) {
// 锁定曝光
device.exposureMode = AVCaptureExposureModeLocked;
[device unlockForConfiguration];
} else {
if (self.delegate && [self.delegate respondsToSelector:@selector(deviceConfigurationFailedWithError:)]) {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
});
}
}
}
// 重置对焦和曝光
- (void)resetFocusAndExposureModes {
AVCaptureDevice *device = [self getActiveCamera];
// 摄像头是否支持兴趣点对焦 & 是否支持自动对焦模式 ,不支持不操作,玩手动对焦的需求另说
BOOL canResetFocus = device.isFocusPointOfInterestSupported && [device isFocusModeSupported:AVCaptureFocusModeAutoFocus];
// 摄像头是否支持兴趣点曝光 & 是否支持自动曝光模式 ,不支持不操作,玩手动曝光的需求另说
BOOL canResetExposure = device.isExposurePointOfInterestSupported && [device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure];
// 捕捉设备空间左上角(0,0),右下角(1,1) 中心点则(0.5,0.5)
CGPoint centerPoint = CGPointMake(0.5f, 0.5f);
NSError *error;
//锁定设备,准备配置
if ([device lockForConfiguration:&error]) {
// 将对焦点和曝光点设为中心,并将对焦和曝光模式设为自动
if (canResetFocus) {
device.focusMode = AVCaptureFocusModeContinuousAutoFocus;
device.focusPointOfInterest = centerPoint;
}
if (canResetExposure) {
device.exposureMode = AVCaptureExposureModeContinuousAutoExposure;
device.exposurePointOfInterest = centerPoint;
}
//释放锁定
[device unlockForConfiguration];
} else {
if (self.delegate && [self.delegate respondsToSelector:@selector(deviceConfigurationFailedWithError:)]) {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
一些常用的get set函数
#pragma mark - Getter
- (NSUInteger)cameraCount {
return [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo] count];
}
- (BOOL)isHasFlash {
return [[self getActiveCamera] hasFlash];
}
- (BOOL)isHasTorch {
return [[self getActiveCamera] hasTorch];
}
- (BOOL)isSupportTapFocus {
return [self getActiveCamera].isFocusPointOfInterestSupported;
}
- (BOOL)isSupportTapExpose {
return [self getActiveCamera].isExposurePointOfInterestSupported;
}
- (AVCaptureFlashMode)flashMode {
return [[self getActiveCamera] flashMode];
}
- (AVCaptureTorchMode)torchMode {
return [[self getActiveCamera] torchMode];
}
#pragma mark - Setter
- (void)setFlashMode:(AVCaptureFlashMode)flashMode {
AVCaptureDevice *device = [self getActiveCamera];
if (![device isFlashModeSupported:flashMode]) return;
NSError *error;
if ([device lockForConfiguration:&error]) {
device.flashMode = flashMode;
[device unlockForConfiguration];
} else {
if (self.delegate && [self.delegate respondsToSelector:@selector(deviceConfigurationFailedWithError:)]) {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}
- (void)setTorchMode:(AVCaptureTorchMode)torchMode {
AVCaptureDevice *device = [self getActiveCamera];
if (![device isTorchModeSupported:torchMode]) return;
NSError *error;
if ([device lockForConfiguration:&error]) {
device.torchMode = torchMode;
[device unlockForConfiguration];
} else {
if (self.delegate && [self.delegate respondsToSelector:@selector(deviceConfigurationFailedWithError:)]) {
[self.delegate deviceConfigurationFailedWithError:error];
}
}
}