持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
AVFoundation 是Apple iOS和OS X系统中用于处理基于时间的媒体数据的高级框架,通过开发所需的工具提供了强大的功能集,让开发者能够基于苹果平台创建当下最先进的媒体应用程序,其针对64位处理器设计,充分利用了多核硬件优势,会自动提供硬件加速操作,确保大部分设备能以最佳性能运行,是iOS开发接触音视频开发必学的框架之一。
参与掘金日新计划,持续记录AVFoundation学习,Demo学习地址,面封装了一些工具类,可以直接使用,这篇文章主要讲述AVCaptureVideoDataOutput视频流、AVCaptureAudioDataOutput音频流数据输出的配置,其他类的相关用法可查看我的其他文章。
AVCaptureVideoDataOutput/AVCaptureAudioDataOutput
之前讲述了使用AVCaptureStillImageOutput、AVCaptureMovieFileOutput实现静态图片和视频的捕捉,但是想要在拍摄预览时加入滤镜、变声等效果,捕捉到的视频也有这个效果,那么这两个类是做不到的,这时就需要使用实时视频流拿到输出的Buffer,对Buffer进行添加滤镜等操作,通过OpenGl、GLKView等方式预览Buffer,最后通过AVAssetWriter或VideoToolBox、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,这里我贴下GLKView和UIImageView的展示。
/**
使用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")
}
}