【iOS音视频学习】ACFoundation捕捉

367 阅读9分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

  • 突然发现很久没有写文章记录了,2021的flag凉凉,去年换到了新的公司,开始大量接触iOS音视频相关,之前对音视频的接触并不多,所以除了上班就是学习了,赶在更文挑战即将结束前,总结下最近的学习。

  • 先贴Demo学习地址

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];
        }
    }
}