张科的技术博客

折腾是一种态度

嗨,我是张科(@GarfieldLover),搜狐视频iOS开发者。


这是我用来记录实践和填坑经验心得的地方,欢迎您的访问。


iOS直播app个人实践--采集美颜

如果不了解原理,可以看这篇文章iOS直播app个人实践–原理

开发一款直播app,首先需要采集主播的视频和音频(包括美颜),然后传入流媒体服务器,iOS端用AVFoundation实现音视频的采集,下边介绍常用功能。

一、AVFoundation采集音视频

1.1、常用捕获功能类

  • AVFoundation: 音视频数据采集需要用AVFoundation框架.
  • AVCaptureDevice:硬件设备,包括麦克风、摄像头,通过该对象可以设置物理设备的一些属性(例如相机聚焦、白平衡等)
  • AVCaptureDeviceInput:硬件输入对象,可以根据AVCaptureDevice创建对应的AVCaptureDeviceInput对象,用于管理硬件输入数据。
  • AVCaptureOutput:硬件输出对象,用于接收各类输出数据,通常使用对应的子类AVCaptureAudioDataOutput(声音数据输出对象)、AVCaptureVideoDataOutput(视频数据输出对象)
  • AVCaptionConnection:当把一个输入和输出添加到AVCaptureSession之后,AVCaptureSession就会在输入、输出设备之间建立连接,而且通过AVCaptureOutput可以获取这个连接对象。
  • AVCaptureVideoPreviewLayer:相机拍摄预览图层,能实时查看拍照或视频录制效果,创建该对象需要指定对应的AVCaptureSession对象,因为AVCaptureSession包含视频输入数据,有视频数据才能展示。
  • AVCaptureSession: 协调输入与输出之间传输数据 系统作用:可以操作硬件设备 工作原理:让App与系统之间产生一个捕获会话,相当于App与硬件设备有联系了, 我们只需要把硬件输入对象和输出对象添加到会话中,会话就会自动把硬件输入对象和输出产生连接,这样硬件输入与输出设备就能传输音视频数据。 现实生活场景:租客(输入钱),中介(会话),房东(输出房),租客和房东都在中介登记,中介就会让租客与房东之间产生联系,以后租客就能直接和房东联系了。

对于一个session, 可以配置多个inputs和outputs, 如图所示:

采集流程

对于大部分的应用而言, 这已经足够了. 但是有些情况下, 会涉及到如何表示一个inputs的多个端口(ports), 以及这些ports如何连接到outputs.

如下图所示, 当在session中添加一个input或output时, session会为所有可匹配的inputs和outputs之前生成connections(AVCaptureConnection).

采集流程

1.2、捕获音视频步骤

  1. 创建AVCaptureSession对象
  2. 获取AVCaptureDevicel录像设备(摄像头),录音设备(麦克风),注意不具备输入数据功能,只是用来调节硬件设备的配置。
  3. 根据音频/视频硬件设备(AVCaptureDevice)创建音频/视频硬件输入数据对象(AVCaptureDeviceInput),专门管理数据输入。
  4. 创建视频输出数据管理对象(AVCaptureVideoDataOutput),并且设置样品缓存代理(setSampleBufferDelegate)就可以通过它拿到采集到的视频数据
  5. 创建音频输出数据管理对象(AVCaptureAudioDataOutput),并且设置样品缓存代理(setSampleBufferDelegate)就可以通过它拿到采集到的音频数据
  6. 将数据输入对象AVCaptureDeviceInput、数据输出对象AVCaptureOutput添加到媒体会话管理对象AVCaptureSession中,就会自动让音频输入与输出和视频输入与输出产生连接.
  7. 创建视频预览图层AVCaptureVideoPreviewLayer并指定媒体会话,添加图层到显示容器layer中
  8. 启动AVCaptureSession,只有开启,才会开始输入到输出数据流传输。
// 捕获音视频
- (void)setupCaputureVideo
{
    // 1.创建捕获会话,必须要强引用,否则会被释放
    AVCaptureSession *captureSession = [[AVCaptureSession alloc] init];
    _captureSession = captureSession;

    // 2.获取摄像头设备,默认是后置摄像头
    AVCaptureDevice *videoDevice = [self getVideoDevice:AVCaptureDevicePositionFront];

    // 3.获取声音设备
    AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];

    // 4.创建对应视频设备输入对象
    AVCaptureDeviceInput *videoDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:nil];
    _currentVideoDeviceInput = videoDeviceInput;

    // 5.创建对应音频设备输入对象
    AVCaptureDeviceInput *audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:nil];

    // 6.添加到会话中
    // 注意“最好要判断是否能添加输入,会话不能添加空的
    // 6.1 添加视频
    if ([captureSession canAddInput:videoDeviceInput]) {
        [captureSession addInput:videoDeviceInput];
    }
    // 6.2 添加音频
    if ([captureSession canAddInput:audioDeviceInput]) {
        [captureSession addInput:audioDeviceInput];
    }

    // 7.获取视频数据输出设备
    AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
    // 7.1 设置代理,捕获视频样品数据
    // 注意:队列必须是串行队列,才能获取到数据,而且不能为空
    dispatch_queue_t videoQueue = dispatch_queue_create("Video Capture Queue", DISPATCH_QUEUE_SERIAL);
    [videoOutput setSampleBufferDelegate:self queue:videoQueue];
    if ([captureSession canAddOutput:videoOutput]) {
        [captureSession addOutput:videoOutput];
    }

    // 8.获取音频数据输出设备
    AVCaptureAudioDataOutput *audioOutput = [[AVCaptureAudioDataOutput alloc] init];
    // 8.2 设置代理,捕获视频样品数据
    // 注意:队列必须是串行队列,才能获取到数据,而且不能为空
    dispatch_queue_t audioQueue = dispatch_queue_create("Audio Capture Queue", DISPATCH_QUEUE_SERIAL);
    [audioOutput setSampleBufferDelegate:self queue:audioQueue];
    if ([captureSession canAddOutput:audioOutput]) {
        [captureSession addOutput:audioOutput];
    }

    // 9.获取视频输入与输出连接,用于分辨音视频数据
    _videoConnection = [videoOutput connectionWithMediaType:AVMediaTypeVideo];

    // 10.添加视频预览图层
    AVCaptureVideoPreviewLayer *previedLayer = [AVCaptureVideoPreviewLayer layerWithSession:captureSession];
    previedLayer.frame = [UIScreen mainScreen].bounds;
    [self.view.layer insertSublayer:previedLayer atIndex:0];
    _previedLayer = previedLayer;

    // 11.启动会话
    [captureSession startRunning];
}

// 指定摄像头方向获取摄像头
- (AVCaptureDevice *)getVideoDevice:(AVCaptureDevicePosition)position
{
    NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
    for (AVCaptureDevice *device in devices) {
        if (device.position == position) {
            return device;
        }
    }
    return nil;
}

#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
// 获取输入设备数据,有可能是音频有可能是视频
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
    if (_videoConnection == connection) {
        NSLog(@"采集到视频数据");
    } else {
        NSLog(@"采集到音频数据");
    }
}

1.3、捕获设置

1.3.1、切换摄像头

切换摄像头步骤

  1. 获取当前视频设备输入对象
  2. 判断当前视频设备是前置还是后置
  3. 确定切换摄像头的方向
  4. 根据摄像头方向获取对应的摄像头设备
  5. 创建对应的摄像头输入对象
  6. 从会话中移除之前的视频输入对象
  7. 添加新的视频输入对象到会话中
// 切换摄像头
- (IBAction)toggleCapture:(id)sender {

    // 获取当前设备方向
    AVCaptureDevicePosition curPosition = _currentVideoDeviceInput.device.position;

    // 获取需要改变的方向
    AVCaptureDevicePosition togglePosition = curPosition == AVCaptureDevicePositionFront?AVCaptureDevicePositionBack:AVCaptureDevicePositionFront;

    // 获取改变的摄像头设备
    AVCaptureDevice *toggleDevice = [self getVideoDevice:togglePosition];

    // 获取改变的摄像头输入设备
    AVCaptureDeviceInput *toggleDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:toggleDevice error:nil];

    // 移除之前摄像头输入设备
    [_captureSession removeInput:_currentVideoDeviceInput];

    // 添加新的摄像头输入设备
    [_captureSession addInput:toggleDeviceInput];

    // 记录当前摄像头输入设备
    _currentVideoDeviceInput = toggleDeviceInput;

}
1.3.2、对焦模式

有三种对焦模式:

  • AVCaptureFocusModeLocked: 固定焦点
  • AVCaptureFocusModeAutoFocus: 自动对焦然后锁定焦点
  • AVCaptureFocusModeContinuousAutoFocus: 连续自动对焦
if ([currentDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
    CGPoint autofocusPoint = CGPointMake(0.5f, 0.5f);
    [currentDevice setFocusPointOfInterest:autofocusPoint];
    [currentDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus];
}
1.3.3、曝光模式

有两种曝光模式:

  • AVCaptureExposureModeContinuousAutoExposure: 自动调整曝光等级
  • AVCaptureExposureModeLocked: 固定曝光等级
if ([currentDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) {
    CGPoint exposurePoint = CGPointMake(0.5f, 0.5f);
    [currentDevice setExposurePointOfInterest:exposurePoint];
    [currentDevice setExposureMode:AVCaptureExposureModeContinuousAutoExposure];
}
1.3.4、闪光模式

有三种闪光模式:

  • AVCaptureFlashModeOff: 关闭
  • AVCaptureFlashModeOn: 打开
  • AVCaptureFlashModeAuto: 根据环境亮度自动开启或关闭
1.3.5、手电筒模式

手电筒模式下, 闪光灯会一直处于开启状态, 用于视频捕捉. 有三种手电筒模式:

  • AVCaptureTorchModeOff: 关闭
  • AVCaptureTorchModeOn: 打开
  • AVCaptureTorchModeAuto: 根据需要自动开启或关闭
1.3.6、白平衡

有两种白平衡模式:

  • AVCaptureWhiteBalanceModeLocked: 固定参数的白平衡
  • AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance: 由相机自动调整白平衡参数

二、GPUImage美颜

2.1、美颜流程

美颜流程

2.2、GPUImage处理图像

  • GPU:(Graphic Processor Unit图形处理单元)手机或者电脑用于图像处理和渲染的硬件
  • OpenGL ES:(Open Graphics Library For Embedded(嵌入的) Systems 开源嵌入式系统图形处理框架),一套图形与硬件接口,用于把处理好的图片显示到屏幕上。
  • GPUImage:是一个基于OpenGL ES 2.0图像和视频处理的开源iOS框架,提供各种各样的图像处理滤镜,并且支持照相机和摄像机的实时滤镜,内置120多种滤镜效果,并且能够自定义图像滤镜。
  • 滤镜原理:就是把静态图片或者视频的每一帧进行图形变换再显示出来。它的本质就是像素点的坐标和颜色变化
  • GPUImage采用链式方式来处理画面,通过addTarget:方法为链条添加每个环节的对象,处理完一个target,就会把上一个环节处理好的图像数据传递下一个target去处理,称为GPUImage处理链。
    • 中间环节的target, 一般是各种filter, 是GPUImageFilter或者是子类.
    • 最终环节的target, GPUImageView:用于显示到屏幕上, 或者GPUImageMovieWriter:写成视频文件。
  • GPUImage处理主要分为3个环节: source(视频、图片源) -> filter(滤镜) -> final target (处理后视频、图片)
    • GPUImaged的Source:都继承GPUImageOutput的子类
    • GPUImageVideoCamera:用于实时拍摄视频
    • GPUImageStillCamera:用于实时拍摄照片
    • GPUImagePicture:用于处理已经拍摄好的图片,比如png,jpg图片
    • GPUImageMovie:用于处理已经拍摄好的视频,比如mp4文件
  • GPUImage的filter:GPUimageFilter类或者子类,这个类继承自GPUImageOutput,并且遵守GPUImageInput协议,
  • GPUImage的final target:GPUImageView,GPUImageMovieWriter

GPUImage处理原理

2.3、GPUImage美颜实现

2.3.1、组合滤镜
  1. 创建视频源GPUImageVideoCamera
  2. 创建最终目的源:GPUImageView
  3. 创建滤镜组(GPUImageFilterGroup),需要组合亮度(GPUImageBrightnessFilter)和双边滤波(GPUImageBilateralFilter)这两个滤镜达到美颜效果.
  4. 设置滤镜组链
  5. 设置GPUImage处理链,从数据源 => 滤镜 => 最终界面效果
  6. 开始采集视频
- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建视频源
    // SessionPreset:屏幕分辨率,AVCaptureSessionPresetHigh会自适应高分辨率
    // cameraPosition:摄像头方向
    GPUImageVideoCamera *videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPresetHigh cameraPosition:AVCaptureDevicePositionFront];
     videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
    _videoCamera = videoCamera;

    // 创建最终预览View
    GPUImageView *captureVideoPreview = [[GPUImageView alloc] initWithFrame:self.view.bounds];
    [self.view insertSubview:captureVideoPreview atIndex:0];

    // 创建滤镜:磨皮,美白,组合滤镜
    GPUImageFilterGroup *groupFilter = [[GPUImageFilterGroup alloc] init];

    // 磨皮滤镜
    GPUImageBilateralFilter *bilateralFilter = [[GPUImageBilateralFilter alloc] init];
    [groupFilter addTarget:bilateralFilter];
    _bilateralFilter = bilateralFilter;

    // 美白滤镜
    GPUImageBrightnessFilter *brightnessFilter = [[GPUImageBrightnessFilter alloc] init];
    [groupFilter addTarget:brightnessFilter];
    _brightnessFilter = brightnessFilter;

    // 设置滤镜组链
    [bilateralFilter addTarget:brightnessFilter];
    [groupFilter setInitialFilters:@[bilateralFilter]];
    groupFilter.terminalFilter = brightnessFilter;

    // 设置GPUImage响应链,从数据源 => 滤镜 => 最终界面效果
    [videoCamera addTarget:groupFilter];
    [groupFilter addTarget:captureVideoPreview];

    // 必须调用startCameraCapture,底层才会把采集到的视频源,渲染到GPUImageView中,就能显示了。
    // 开始采集视频
    [videoCamera startCameraCapture];
}

- (IBAction)brightnessFilter:(UISlider *)sender {
    _brightnessFilter.brightness = sender.value;
}

- (IBAction)bilateralFilter:(UISlider *)sender {
    // 值越小,磨皮效果越好
    CGFloat maxValue = 10;
    [_bilateralFilter setDistanceNormalizationFactor:(maxValue - sender.value)];
}
2.3.2、美颜滤镜
  1. 创建视频源GPUImageVideoCamera
  2. 创建最终目的源:GPUImageView
  3. 创建最终美颜滤镜:GPUImageBeautifyFilter
  4. 设置GPUImage处理链,从数据源 => 滤镜 => 最终界面效果
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    // 创建视频源
    // SessionPreset:屏幕分辨率,AVCaptureSessionPresetHigh会自适应高分辨率
    // cameraPosition:摄像头方向
    GPUImageVideoCamera *videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPresetHigh cameraPosition:AVCaptureDevicePositionFront];
    videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
    _videoCamera = videoCamera;

    // 创建最终预览View
    GPUImageView *captureVideoPreview = [[GPUImageView alloc] initWithFrame:self.view.bounds];
    [self.view insertSubview:captureVideoPreview atIndex:0];
    _captureVideoPreview = captureVideoPreview;

    // 设置处理链
    [_videoCamera addTarget:_captureVideoPreview];

    // 必须调用startCameraCapture,底层才会把采集到的视频源,渲染到GPUImageView中,就能显示了。
    // 开始采集视频
    [videoCamera startCameraCapture];

}

- (IBAction)openBeautifyFilter:(UISwitch *)sender {

    // 切换美颜效果原理:移除之前所有处理链,重新设置处理链
    if (sender.on) {

        // 移除之前所有处理链
        [_videoCamera removeAllTargets];

        // 创建美颜滤镜
        GPUImageBeautifyFilter *beautifyFilter = [[GPUImageBeautifyFilter alloc] init];

        // 设置GPUImage处理链,从数据源 => 滤镜 => 最终界面效果
        [_videoCamera addTarget:beautifyFilter];
        [beautifyFilter addTarget:_captureVideoPreview];

    } else {

        // 移除之前所有处理链
        [_videoCamera removeAllTargets];
        [_videoCamera addTarget:_captureVideoPreview];
    }
}