本案例主要是利用Metal实现摄像头采集内容的即刻渲染处理,理解视频采集、处理及渲染的流程
视频实时采集并渲染的效果图如下,以下效果是由于设置了高斯模糊滤镜,其中高斯模糊滤镜的sigma
参数值越高,图像越模糊
视频渲染的实现思路主要有以下三步
- 1、通过
AVFoundation
进行视频数据的采集,并将采集到的原始数据存储到CMSampleBufferRef
中,即视频帧数据(视频帧其实本质也是一张图片) - 2、通过
CoreVideo
将CMSampleBufferRef
中存储的图像数据,转换为Metal
可以直接使用的纹理
- 3、将Metal
纹理
进行渲染
,并即刻显示到屏幕上
在实际的开发应用中,
AVFoundation
提供了一个layer,即AVCaptureVideoPreviewLayer
预览层,我们可以使用预览层直接预览视频采集后的即可渲染,用于替代思路中2、3步。根据官方文档-AVCaptureVideoPreviewLayer说明,
AVCaptureVideoPreviewLayer
是CALayer
的子类,用于在输入设备捕获视频时显示视频
,此预览图层与捕获会话
结合使用,主要有以下三步:
- 创建预览层对象
- 将预览层与captureSession链接
- 将预览层加到view的子layer中
//创建一个预览层。 let previewLayer = AVCaptureVideoPreviewLayer() //将预览层与捕获会话连接。 previewLayer.session = captureSession //将预览图层添加到视图的图层层次结构中 view.layer.addSublayer(previewLayer)
下面来说下视频渲染的实现,其整体的实现流程如图所示
主要分为3部分
- viewDidLoad函数:初始化Metal和视频采集的准备工作
- MTKViewDelegate协议方法:视频采集数据转换为纹理
- AVCaptureVideoDataOutputSampleBufferDelegate协议方法:将采集转换后的纹理渲染到屏幕上
viewDidLoad函数
该函数中主要是设置Metal的相关初始化操作,以及视频采集前的准备工作,函数的流程如图所示
分为以下两部分
- setupMetal函数
- setupCaptureSession函数
setupMetal函数
Metal的准备工作,主要需要初始化以下3部分
- 初始化MTKView,用于显示视频采集数据转换后的纹理
self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds device:MTLCreateSystemDefaultDevice()]; [self.view insertSubview:self.mtkView atIndex:0]; self.mtkView.delegate = self;
- 创建命令队列:通过MTKView中的device创建
self.commandQueue = [self.mtkView.device newCommandQueue];
设置MTKView的读写操作 & 创建纹理缓冲区
- MTKView中的
framebufferOnly
属性,默认的帧缓存是只读
的即YES
,由于view需要显示纹理,所以需要该属性改为可读写
即NO
- 通过
CVMetalTextureCacheCreate
方法创建CoreVideo
中的metal纹理缓存区
,因为采集的视频数据是通过CoreVideo转换为metal纹理的,主要的用于存储转换后的metal纹理
//注意: 在初始化MTKView 的基本操作以外. 还需要多下面2行代码. /* 1. 设置MTKView 的drawable 纹理是可读写的(默认是只读); 2. 创建CVMetalTextureCacheRef _textureCache; 这是Core Video的Metal纹理缓存 */ //允许读写操作 self.mtkView.framebufferOnly = NO; /* CVMetalTextureCacheCreate(CFAllocatorRef allocator, CFDictionaryRef cacheAttributes, id <MTLDevice> metalDevice, CFDictionaryRef textureAttributes, CVMetalTextureCacheRef * CV_NONNULL cacheOut ) 功能: 创建纹理缓存区 参数1: allocator 内存分配器.默认即可.NULL 参数2: cacheAttributes 缓存区行为字典.默认为NULL 参数3: metalDevice 参数4: textureAttributes 缓存创建纹理选项的字典. 使用默认选项NULL 参数5: cacheOut 返回时,包含新创建的纹理缓存。 */ CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
setupCaptureSession函数
初始化视频采集的准备工作,以及开始视频采集,主要分为以下几步:
- 设置
AVCaptureSession
& 视频采集的分辨率
1、设置AVCaptureSession & 视频采集的分辨率 self.mCaptureSession = [[AVCaptureSession alloc] init]; self.mCaptureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
- 创建串行队列
串行队列创建的目的在于处理captureSession的交互时,不会影响主队列,在苹果官方文档中有如下图示,表示captureSession是如何管理设备的输入 & 输出,以及与主队列之间的关系
self.mProcessQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL);
设置输入设备
- 获取后置摄像头设备
AVCaptureDevice
通过获取设备数组,循环判断找到后置摄像头,将后置摄像头设备为当前的输入设备 - 通过摄像头设备创建
AVCaptureDeviceInput
将AVCaptureDevice 转换为 AVCaptureDeviceInput,主要是因为AVCaptureSession
无法直接使用AVCaptureDevice
,所以需要将device转换为deviceInput - 输入设备添加到captureSession中
在添加之前,需要通过captureSession
的canAddInput
函数判断是否可以添加输入设备,如果可以,则通过session的addInput
函数添加输入设备
// 3、获取摄像头设备(前置/后置摄像头设备) NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; AVCaptureDevice *inputCamera = nil; //循环设备数组,找到后置摄像头.设置为当前inputCamera for (AVCaptureDevice *device in devices) { if ([device position] == AVCaptureDevicePositionBack) { //拿到后置摄像头 inputCamera = device; } } // 4、将AVCaptureDevice 转换为 AVCaptureDeviceInput,即输入 // AVCaptureSession 无法直接使用 AVCaptureDevice,所哟需要将device转换为deviceInput self.mCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputCamera error:nil]; // 5、将设备添加到captureSession中,需要先判断能否添加输入 if ([self.mCaptureSession canAddInput:self.mCaptureDeviceInput]) { [self.mCaptureSession addInput:self.mCaptureDeviceInput]; }
- 设置输出设备
- 创建AVCaptureVideoDataOutput对象,即输出设备
- 设置输出设备的
setAlwaysDiscardsLateVideoFrames
属性(表示视频帧延时使是否丢弃数据)为NO
YES
:处理现有帧的调度队列在captureOutput:didOutputSampleBuffer:FromConnection:
Delegate方法中被阻止时,对象会立即丢弃
捕获的帧NO
:在丢弃新帧之前,允许委托有更多的时间处理旧帧,但这样可能会内存增加
- 设置输出设备的
setVideoSettings
属性(即像素格式),表示每一个像素点颜色保存的格式,且设置的格式是BGRA
,而不是YUV
,主要是为了避免Shader转换
,如果使用了YUV
格式,就需要编写shader来进行颜色格式转换
- 设置输出设备的视频捕捉输出的
delegate
- 将输出设备添加到captureSession中
// 6、创建AVCaptureVideoDataOutput对象,即输出 & 设置输出相关属性 self.mCaptureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init]; /*设置视频帧延迟到底时是否丢弃数据. YES: 处理现有帧的调度队列在captureOutput:didOutputSampleBuffer:FromConnection:Delegate方法中被阻止时,对象会立即丢弃捕获的帧。 NO: 在丢弃新帧之前,允许委托有更多的时间处理旧帧,但这样可能会内存增加. */ //视频帧延迟是否需要丢帧 [self.mCaptureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO]; //设置像素格式:每一个像素点颜色保存的格式 //这里设置格式为BGRA,而不用YUV的颜色空间,避免使用Shader转换,如果使用YUV格式,需要编写shade来进行颜色格式转换 //注意:这里必须和后面CVMetalTextureCacheCreateTextureFromImage 保存图像像素存储格式保持一致.否则视频会出现异常现象. [self.mCaptureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]]; //设置视频捕捉输出的代理方法:将采集的视频数据输出 [self.mCaptureDeviceOutput setSampleBufferDelegate:self queue:self.mProcessQueue]; // 7、添加输出,即添加到captureSession中 if ([self.mCaptureSession canAddOutput:self.mCaptureDeviceOutput]) { [self.mCaptureSession addOutput:self.mCaptureDeviceOutput]; }
- 输入与输出链接 & 设置视频输出方向
通过AVCaptureConnection
链接输入和输出,并设置connect的视频输出方向,即设置videoOrientation
属性
// 8、输入与输出链接 AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo]; // 9、设置视频输出方向 //注意: 一定要设置视频方向.否则视频会是朝向异常的. [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
- 开始捕捉,即开始视频采集也可以通过一个按钮来控制视频采集的开始与停止
startRunning
:开启捕捉stopRunning
:停止捕捉
// 10、开始捕捉 [self.mCaptureSession startRunning];
AVCaptureVideoDataOutputSampleBufferDelegate协议方法
在视频采集的同时,采集到的视频数据,即视频帧会自动回调视频采集回调方法captureOutput:didOutputSampleBuffer:fromConnection:
,在该方法中处理采集到的原始视频数据,将其转换为metal纹理
didOutputSampleBuffer代理方法
主要是获取视频的帧数据,将其转换为metal纹理,函数流程如下
didOutputSampleBuffer代理方法流程
主要分为以下几步:
- 从
sampleBuffer
中获取位图
通过CMSampleBufferGetImageBuffer
函数从sampleBuffer
形参中获取视频像素缓存区对象,即视频帧数据,平常所说的位图
// 1、从sampleBuffer 获取视频像素缓存区对象,即获取位图 CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
- 获取捕捉视频帧的宽高
通过CoreVideo中的CVPixelBufferGetWidth
和CVPixelBufferGetHeight
函数获取宽高
size_t width = CVPixelBufferGetWidth(pixelBuffer); size_t height = CVPixelBufferGetHeight(pixelBuffer);
将位图转换为metal纹理
- 通过
CVMetalTextureRef
创建临时纹理 - 通过
CVMetalTextureCacheCreateTextureFromImage
函数创建metal纹理缓冲区,赋值给临时纹理 - 判断临时纹理是否创建成功,如果临时纹理创建成功,则继续往下执行
- 设置MTKView中的
drawableSize
属性,即表示可绘制纹理的大小 - 通过
CVMetalTextureGetTexture
函数,获取纹理缓冲区的metal纹理对象 - 释放临时纹理
// 3、将位图转换为纹理 //方法来自CoreVideo /*3. 根据视频像素缓存区 创建 Metal 纹理缓存区 CVReturn CVMetalTextureCacheCreateTextureFromImage(CFAllocatorRef allocator, CVMetalTextureCacheRef textureCache, CVImageBufferRef sourceImage, CFDictionaryRef textureAttributes, MTLPixelFormat pixelFormat, size_t width, size_t height, size_t planeIndex, CVMetalTextureRef *textureOut); 功能: 从现有图像缓冲区创建核心视频Metal纹理缓冲区。 参数1: allocator 内存分配器,默认kCFAllocatorDefault 参数2: textureCache 纹理缓存区对象 参数3: sourceImage 视频图像缓冲区 参数4: textureAttributes 纹理参数字典.默认为NULL 参数5: pixelFormat 图像缓存区数据的Metal 像素格式常量.注意如果MTLPixelFormatBGRA8Unorm和摄像头采集时设置的颜色格式不一致,则会出现图像异常的情况; 参数6: width,纹理图像的宽度(像素) 参数7: height,纹理图像的高度(像素) 参数8: planeIndex 颜色通道.如果图像缓冲区是平面的,则为映射纹理数据的平面索引。对于非平面图像缓冲区忽略。 参数9: textureOut,返回时,返回创建的Metal纹理缓冲区。 */ //创建临时纹理 CVMetalTextureRef tmpTexture = NULL; CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture); // 4、判断tmpTexture 是否创建成功 if (status == kCVReturnSuccess) {//创建成功 // 5、设置可绘制纹理的大小 self.mtkView.drawableSize = CGSizeMake(width, height); // 6、返回纹理缓冲区的metal纹理对象 self.texture = CVMetalTextureGetTexture(tmpTexture); // 7、使用完毕,释放tmptexture CFRelease(tmpTexture); }
MTKViewDelegate协议方法
接下来就是将获取的metal纹理即刻渲染并显示到屏幕上,这里是通过MTKViewDelegate
协议的drawInMTKView
代理方法渲染并显示
drawInMTKView代理方法
MTKView默认的帧速率与屏幕刷新频率一致,所以每当屏幕刷新时,都会回调 视频采集方法 和 视图渲染方法,以下是视图渲染方法执行流程
drawInMTKView代理方法流程
主要有以下几步
- 判断纹理是否获取成功
即纹理不为空,如果纹理为空,则没必要执行视图渲染流程 - 通过commandQueue创建commandBuffer命令缓存区
- 将MTKView的纹理作为目标渲染纹理
即获取view中纹理对象 - 设置高斯模糊滤镜
MetalPerformanceShaders
是Metal的一个集成库,有一些滤镜处理的Metal实现,- 此时的滤镜就等价于Metal中的
MTLRenderCommandEncoder
渲染命令编码器,类似于GLSL中program
- 高斯模糊滤镜在渲染时,会触发离屏渲染,且其中的sigma值设置的越高,图像越模糊,就如文章开头展示的效果图
// 4、设置滤镜(Metal封装了一些滤镜) //高斯模糊 渲染时,会触发 离屏渲染 /* MetalPerformanceShaders是Metal的一个集成库,有一些滤镜处理的Metal实现; MPSImageGaussianBlur 高斯模糊处理; */ //创建高斯滤镜处理filter //注意:sigma值可以修改,sigma值越高图像越模糊; MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:5]; // 5、MPSImageGaussianBlur以一个Metal纹理作为输入,以一个Metal纹理作为输出; //输入:摄像头采集的图像 self.texture //输出:创建的纹理 drawingTexture(其实就是view.currentDrawable.texture) //filter等价于Metal中的MTLRenderCommandEncoder 渲染命令编码器,类似于GLSL中的program [filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
- 将获取的纹理显示到屏幕上
- 将commandBuffer通过
commit
提交给GPU - 清空当前纹理,为下一次纹理数据读取做准备
如果不清空,也是可以的,下一次的纹理数据会将上次的数据覆盖
// 6、展示显示的内容 [commandBuffer presentDrawable:view.currentDrawable]; // 7、提交命令 [commandBuffer commit]; // 8、清空当前纹理,准备下一次的纹理数据读取, //如果不清空,也是可以的,下一次的纹理数据会将上次的数据覆盖 self.texture = NULL;
总结
视频采集流程总结
根据上述流程的解析,视频的采集主要有以下几步:
- 1、设置session
- 2、创建串行队列
- 3、设置输入设备
- 4、设置输出设备
- 5、输入与输出链接
- 6、设置视频输出方向
- 7、开始捕捉,即开始视频采集
- 8、
AVCaptureVideoDataOutputSampleBufferDelegate
协议处理采集后的视频数据
如何判断采集的数据是音频还是视频?
主要有以下两种判断方式:
- 1、通过
AVCaptureConnection
判断
视频
:包含视频输入设备 & 视频输出设备,通过AVCaptureConnection
链接起来音频
:包含音频输入设备 & 音频输出设备,同样通过AVCaptureConnection
链接起来
如果需要判断当前采集的输出是视频还是音频,需要将connect对象设置为全局变量,然后在采集回调方法captureOutput:didOutputSampleBuffer:fromConnection:
中判断全局的connection 是否等于 代理方法参数中的coneection ,如果相等,就是视频,反之是音频
- 2、通过
AVCaptureOutput
判断
在采集回调方法captureOutput:didOutputSampleBuffer:fromConnection:
中判断output
形参的类型,如果是AVCaptureVideoDataOutput
类型则是视频,反之,是音频