【AVFoundation】Video/AudioDataOutput视频音频流捕捉

506 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

AVFoundation 是Apple iOS和OS X系统中用于处理基于时间的媒体数据的高级框架,通过开发所需的工具提供了强大的功能集,让开发者能够基于苹果平台创建当下最先进的媒体应用程序,其针对64位处理器设计,充分利用了多核硬件优势,会自动提供硬件加速操作,确保大部分设备能以最佳性能运行,是iOS开发接触音视频开发必学的框架之一

参与掘金日新计划,持续记录AVFoundation学习,Demo学习地址,面封装了一些工具类,可以直接使用,这篇文章主要讲述AVCaptureVideoDataOutput视频流、AVCaptureAudioDataOutput音频流数据输出的配置,其他类的相关用法可查看我的其他文章。

AVCaptureVideoDataOutput/AVCaptureAudioDataOutput

之前讲述了使用AVCaptureStillImageOutputAVCaptureMovieFileOutput实现静态图片和视频的捕捉,但是想要在拍摄预览时加入滤镜、变声等效果,捕捉到的视频也有这个效果,那么这两个类是做不到的,这时就需要使用实时视频流拿到输出的Buffer,对Buffer进行添加滤镜等操作,通过OpenGlGLKView等方式预览Buffer,最后通过AVAssetWriterVideoToolBox、AudioToolBox封装成视频文件。

配置视频数据输出

  • 配置输出前需要创建会话,给会话配置输入,这个之前的文章有写,这里不再赘述。
  • 封装视频文件需要将视频Buffer、音频Buffer都封装,否则没有声音。
  • 视频流、音频流输出可以都在一个队列,也可以创建两个。
  • setAlwaysDiscardsLateVideoFrames,如果将这个属性为NO会给回调函数一些额外的时间来处理样本buffer,但是会增加内存消耗,所以设置为NO时要尽量保证在回调中的处理足够高效。
  • kCVPixelBufferPixelFormatTypeKey这个地方指定格式非常重要,因为我外界使用CoreImage做滤镜,所以写了kCVPixelFormatType_32BGRA,具体根据自己的需求,如果弄错,会出现无法预览、或者封装文件错误等。
#pragma mark - Func 视频数据输出配置
/// 配置视频数据输出
- (void)configVideoDataOutput {
    // 视频Buffer输出
    self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
    [self.videoDataOutput setSampleBufferDelegate:self queue:self.captureVideoQueue];
    [self.videoDataOutput setAlwaysDiscardsLateVideoFrames:YES];
    //kCVPixelBufferPixelFormatTypeKey它指定像素的输出格式,这个参数直接影响输出的buffer到生成图像的成功与否,需要与外界指定相应的格式
   // kCVPixelFormatType_420YpCbCr8BiPlanarFullRange  YUV420格式.
//    self.videoDataOutput.videoSettings = @{(__bridge NSString *)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)};
    // CUBE Demo 用这个设置 当结合OpenGLES和CoreImage时,kCVPixelFormatType_32BGRA非常适合
    self.videoDataOutput.videoSettings = @{(__bridge NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA)};
    // 委托回调会有额外的时间处理buffer,但会增加内存,回调中应尽量高效,才能保证实时性能
    self.videoDataOutput.alwaysDiscardsLateVideoFrames = NO;
    [self.captureSession beginConfiguration];
    if ([self.captureSession canAddOutput:self.videoDataOutput]) {
        [self.captureSession addOutput:self.videoDataOutput];
    }
    [self.captureSession commitConfiguration];
}

/// 移除视频数据输出
- (void)removeVideoDataOutput {
    if (self.videoDataOutput) [self.captureSession removeOutput:self.videoDataOutput];
}

配置音频数据输出

#pragma mark - Func 音频数据输出配置
/// 配置音频数据输出
- (BOOL)configAudioDataOutput {
    self.audioDataOutput = [[AVCaptureAudioDataOutput alloc] init];
    [self.audioDataOutput setSampleBufferDelegate:self queue:self.captureAudioQueue];
    if([self.captureSession canAddOutput:self.audioDataOutput]){
        [self.captureSession beginConfiguration];
        [self.captureSession addOutput:self.audioDataOutput];
        [self.captureSession commitConfiguration];
        return YES;
    } else {
        return NO;
    }
}

/// 移除音频数据输出
- (void)removeAudioDataOutput {
    if (self.audioDataOutput) [self.captureSession removeOutput:self.audioDataOutput];
}

在代理回调中拿到捕捉的Buffer

  • 音频流视频流都是走同一个回调,可以用output类型区分是音频流还是视频流
#pragma mark AVCaptureVideo/AudioDataOutputSampleBufferDelegate
/**
 每当有一个新的视频帧写入时该方法就会被调用,数据会基于视频数据输出的videoSettings属性进行解码或重新编码
 */
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    // 注意,视频/音频通过AV采集,都会走这里,需要对音频/视频做区分
    // 直接判断output 是videoDataOutput/Audio
    if ([captureOutput isKindOfClass:AVCaptureVideoDataOutput.class]) {
        if (self.delegate && [self.delegate respondsToSelector:@selector(captureVideoSampleBuffer:)]) {
            [_delegate captureVideoSampleBuffer:sampleBuffer];
        }
    }
    if ([captureOutput isKindOfClass:AVCaptureAudioDataOutput.class]) {
        if (self.delegate && [self.delegate respondsToSelector:@selector(captureAudioSampleBuffer:)]) {
            [_delegate captureAudioSampleBuffer:sampleBuffer];
        }
    }
}

/**
 每当一个迟到的视频帧被丢弃时调用该方法,通常是因为在didOutputSampleBuffer调用中消耗了太多的处理时间就会调用该方法,应尽量提高处理效率,否则将收不到缓存数据
 */
- (void)captureOutput:(AVCaptureOutput *)output didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    
}

给视频Buffer添加滤镜

  • 这里我使用CoreImage添加滤镜,封装了一个工具类将CVPixelBuffer转成CIImage,具体实际项目中,会用到三方滤镜SDK等,大同小异。
final class FilterManager {
    static let filters: [CIFilter] = [CIFilter(name: "CIPhotoEffectChrome")!,
                                      CIFilter(name: "CIPhotoEffectFade")!,
                                      CIFilter(name: "CIPhotoEffectInstant")!,
                                      CIFilter(name: "CIPhotoEffectMono")!,
                                      CIFilter(name: "CIPhotoEffectNoir")!,
                                      CIFilter(name: "CIPhotoEffectProcess")!,
                                      CIFilter(name: "CIPhotoEffectTonal")!,
                                      CIFilter(name: "CIPhotoEffectTransfer")!]
    
    
    static func filterImage(for filter: CIFilter, pixelBuffer: CVPixelBuffer) -> CIImage {
        // 原始Image
        let sourceImage: CIImage = CIImage(cvPixelBuffer: pixelBuffer)
        // 添加了滤镜Image
        filter.setValue(sourceImage, forKey: kCIInputImageKey)
        var filteredImage: CIImage = filter.outputImage ?? sourceImage
//        filter.setValue(nil, forKey: kCIInputImageKey)
        return filteredImage
    }
    
}
  • 从回调的CMSampleBuffer获取CVPixelBuffer,然后用CoreImage添加滤镜
extension L_CameraWrite: CQCaptureManagerDelegate {
    func captureVideoSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
        /**
         为了达到实时滤镜的效果,需要在每一帧的回调数据中,都对每一帧的图像数据都应用当前滤镜的效果,从而用户可以在拍摄过程中不断切换各种滤镜。
         */
        
        /**
         数据加工阶段可以基于CMSampleBuffer进行各种处理,包括加滤镜等,都在这个阶段。sampleBuffer会包含一个CVPixelBuffer,它是一个带有单个视频帧原始像素数据的Core Video对象,据此我们可以进行像素级别的加工。
         */
        
        guard let pixelBuffer: CVPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        let filteredImage: CIImage = FilterManager.filterImage(for: currentFilter, pixelBuffer: pixelBuffer)

        // 展示
//        preview.image = filteredImage
        Asyncs.asyncMain {
            self.preview2.image = UIImage(ciImage: filteredImage)
        }
        
    }

}

展示

  • 已经是CIImage了,这里展示就很容易了,可以使用GLKView或我们熟悉的UIImageView,当然在复杂的项目中,可能更多会使用OpenGL渲染Buffer,这里我贴下GLKViewUIImageView的展示。
/**
 使用GLKView 达到预览的效果
 */
class ImageBufferPreview: GLKView {
    /// 需要绘制的image
    var image: CIImage! {
        didSet {
            bindDrawable()
            let cropRect: CGRect = WriteUtil.centerCropImageRect(sourceRect: image.extent, previewRect: drawbleBounds)
            coreImageContext.draw(image, in: drawbleBounds, from: cropRect)
            self.display()
        }
    }
    
    /// 绘制image上下文
    private(set) var coreImageContext: CIContext
    
    /// 绘制范围
    private var drawbleBounds: CGRect = .zero
    
    override init(frame: CGRect) {
        let eaglContext = EAGLContext(api: .openGLES2)!
        coreImageContext = CIContext(eaglContext: eaglContext, options: [CIContextOption.workingColorSpace: nil])
        super.init(frame: frame, context: eaglContext)
        enableSetNeedsDisplay = false
        isOpaque = true
        backgroundColor = .black
        transform = CGAffineTransform(rotationAngle: Double.pi/2)
        self.frame = frame
        bindDrawable()
        drawbleBounds = bounds
        drawbleBounds.size.width = CGFloat(drawableWidth)
        drawbleBounds.size.height = CGFloat(drawableHeight)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        CQLog("ImageBufferPreview-deinit")
    }

}
/// 利用UIImageView预览
class ImagePreview: UIImageView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentMode = .scaleAspectFill
        transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / 2))
    }
    
    override init(image: UIImage?) {
        super.init(image: image)
        contentMode = .scaleAspectFill
        transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / 2))
    }
    
    override init(image: UIImage?, highlightedImage: UIImage?) {
        super.init(image: image, highlightedImage: highlightedImage)
        contentMode = .scaleAspectFill
        transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / 2))
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}