一、视频渲染实现思路
① 思路说明
- 通过AVFoundation进行视频数据的采集,并将采集到的原始数据存储到CMSampleBufferRef中,即视频帧数据(视频帧其实本质也是一张图片)。
- 通过CoreVideo将CMSampleBufferRef中存储的图像数据,转换为Metal可以直接使用的纹理。
- 将Metal纹理进行渲染,并即刻显示到屏幕上。
② 思路实现
- 在实际的开发应用中,AVFoundation 提供了一个 layer,即AVCaptureVideoPreviewLayer 预览层,我们可以使用预览层直接预览视频采集后的即可渲染,用于直接实现上面思路中的第二步和第三步。
- 根据官方文档之AVCaptureVideoPreviewLayer说明,AVCaptureVideoPreviewLayer 是 CALayer 的子类,用于在输入设备捕获视频时显示视频,此预览图层与捕获会话结合使用,主要有以下三步:
- 创建预览层对象;
- 将预览层与captureSession链接;
- 将预览层加到view的子layer中。
let previewLayer = AVCaptureVideoPreviewLayer()
previewLayer.session = captureSession
view.layer.addSublayer(previewLayer)
③ 整体流程
- viewDidLoad函数:初始化Metal和视频采集的准备工作;
- MTKViewDelegate协议方法:视频采集数据转换为纹理;
- AVCaptureVideoDataOutputSampleBufferDelegate协议方法:将采集转换后的纹理渲染到屏幕上。
二、初始化 Metal 和视频采集的准备工作
① 整体流程如下:
② setupMetal函数
- 初始化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纹理;
self.mtkView.framebufferOnly = NO;
CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
③ setupCaptureSession函数
- 设置AVCaptureSession & 视频采集的分辨率;
self.mCaptureSession = [[AVCaptureSession alloc] init];
self.mCaptureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
- 创建串行队列:串行队列创建的目的在于处理captureSession的交互时,不会影响主队列,在苹果官方文档中有如下图示,表示captureSession是如何管理设备的输入 & 输出,以及与主队列之间的关系,session管理输入和输出图示如下:
self.mProcessQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL);
- 设置输入设备
- 获取后置摄像头设备AVCaptureDevice:通过获取设备数组,循环判断找到后置摄像头,将后置摄像头设备为当前的输入设备;
- 通过摄像头设备创建AVCaptureDeviceInput:将AVCaptureDevice 转换为 AVCaptureDeviceInput,主要是因为 AVCaptureSession 无法直接使用 AVCaptureDevice,所以需要将device转换为deviceInput;
- 输入设备添加到captureSession中:在添加之前,需要通过captureSession的canAddInput函数判断是否可以添加输入设备,如果可以,则通过session的addInput函数添加输入设备;
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
AVCaptureDevice *inputCamera = nil;
for (AVCaptureDevice *device in devices) {
if ([device position] == AVCaptureDevicePositionBack) {
inputCamera = device;
}
}
self.mCaptureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputCamera error:nil];
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中;
self.mCaptureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init];
[self.mCaptureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO];
[self.mCaptureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
[self.mCaptureDeviceOutput setSampleBufferDelegate:self queue:self.mProcessQueue];
if ([self.mCaptureSession canAddOutput:self.mCaptureDeviceOutput]) {
[self.mCaptureSession addOutput:self.mCaptureDeviceOutput];
}
- 输入与输出链接 & 设置视频输出方向:通过AVCaptureConnection链接输入和输出,并设置connect的视频输出方向,即设置videoOrientation属性;
AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
- 输入与输出链接 & 设置视频输出方向:通过AVCaptureConnection链接输入和输出,并设置connect的视频输出方向,即设置videoOrientation属性;
AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
- 开始捕捉,即开始视频采集,也可以通过一个按钮来控制视频采集的开始与停止
- startRunning:开启捕捉
- stopRunning:停止捕捉
[self.mCaptureSession startRunning];
三 、AVCaptureVideoDataOutputSampleBufferDelegate协议
① 整体流程
- 在视频采集的同时,采集到的视频数据,即视频帧会自动回调视频采集回调方法captureOutput:didOutputSampleBuffer:fromConnection:,在该方法中处理采集到的原始视频数据,将其转换为metal纹理;
- didOutputSampleBuffer代理方法:主要是获取视频的帧数据,将其转换为metal纹理,函数流程如下:
② 流程分解说明
- 从sampleBuffer中获取位图:通过CMSampleBufferGetImageBuffer函数从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纹理对象;
- 释放临时纹理;
CVMetalTextureRef tmpTexture = NULL;
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);
if (status == kCVReturnSuccess) {
self.mtkView.drawableSize = CGSizeMake(width, height);
self.texture = CVMetalTextureGetTexture(tmpTexture);
CFRelease(tmpTexture);
}
四、MTKViewDelegate协议
① 说明
- 将获取的metal纹理即刻渲染并显示到屏幕上,这里是通过MTKViewDelegate协议的drawInMTKView代理方法渲染并显示。
- drawInMTKView代理方法
MTKView默认的帧速率与屏幕刷新频率一致,所以每当屏幕刷新时,都会回调 视频采集方法和视图渲染方法,以下是视图渲染方法执行流程:
② 具体步骤
- 判断纹理是否获取成功:即纹理不为空,如果纹理为空,则没必要执行视图渲染流程;
- 通过commandQueue创建commandBuffer命令缓存区;
- 将MTKView的纹理作为目标渲染纹理,即获取view中纹理对象
- 设置高斯模糊滤镜:
- MetalPerformanceShaders是Metal的一个集成库,有一些滤镜处理的Metal实现;
- 此时的滤镜就等价于Metal中的MTLRenderCommandEncoder渲染命令编码器,类似于GLSL中program;
- 高斯模糊滤镜在渲染时,会触发离屏渲染,且其中的sigma值设置的越高,图像越模糊;
MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:5];
[filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
- 将获取的纹理显示到屏幕上;
- 将commandBuffer通过commit提交给GPU;
- 清空当前纹理,为下一次纹理数据读取做准备;
如果不清空,也是可以的,下一次的纹理数据会将上次的数据覆盖;
[commandBuffer presentDrawable:view.currentDrawable];
[commandBuffer commit];
self.texture = NULL;
五、总结
① 视频采集流程
- 设置session;
- 创建串行队列;
- 设置输入设备;
- 设置输出设备;
- 输入与输出链接;
- 设置视频输出方向;
- 开始捕捉,即开始视频采集;
- AVCaptureVideoDataOutputSampleBufferDelegate协议处理采集后的视频数据;
② 如何判断采集的数据是音频还是视频
- 通过AVCaptureConnection判断
- 视频:包含视频输入设备 & 视频输出设备,通过AVCaptureConnection链接起来
- 音频:包含音频输入设备 & 音频输出设备,同样通过AVCaptureConnection链接起来
- 如果需要判断当前采集的输出是视频还是音频,需要将connect对象设置为全局变量,然后在采集回调方法captureOutput:didOutputSampleBuffer:fromConnection:中判断全局的connection 是否等于代理方法参数中的coneection ,如果相等,就是视频,反之是音频;
- 通过AVCaptureOutput判断
在采集回调方法captureOutput:didOutputSampleBuffer:fromConnection:中判断output形参的类型,如果是AVCaptureVideoDataOutput 类型则是视频,反之,是音频。