Metal 案例05:视频采集 & 实时渲染

2021-04-23  本文已影响0人  辉辉岁月

本案例主要是利用Metal实现摄像头采集内容的即刻渲染处理,理解视频采集、处理及渲染的流程

视频实时采集并渲染的效果图如下,以下效果是由于设置了高斯模糊滤镜,其中高斯模糊滤镜的sigma参数值越高,图像越模糊

视频渲染的实现思路主要有以下三步

在实际的开发应用中,AVFoundation提供了一个layer,即AVCaptureVideoPreviewLayer 预览层,我们可以使用预览层直接预览视频采集后的即可渲染,用于替代思路中2、3步。
根据官方文档-AVCaptureVideoPreviewLayer说明,AVCaptureVideoPreviewLayerCALayer 的子类,用于在输入设备捕获视频时显示视频,此预览图层与捕获会话结合使用,主要有以下三步:

//创建一个预览层。
let previewLayer = AVCaptureVideoPreviewLayer()

//将预览层与捕获会话连接。
previewLayer.session = captureSession

//将预览图层添加到视图的图层层次结构中
view.layer.addSublayer(previewLayer)

下面来说下视频渲染的实现,其整体的实现流程如图所示

主要分为3部分

viewDidLoad函数

该函数中主要是设置Metal的相关初始化操作,以及视频采集前的准备工作,函数的流程如图所示

分为以下两部分

setupMetal函数
Metal的准备工作,主要需要初始化以下3部分

self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds device:MTLCreateSystemDefaultDevice()];
[self.view insertSubview:self.mtkView atIndex:0];
self.mtkView.delegate = self;

self.commandQueue = [self.mtkView.device newCommandQueue];

//注意: 在初始化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函数
初始化视频采集的准备工作,以及开始视频采集,主要分为以下几步:

1、设置AVCaptureSession & 视频采集的分辨率
self.mCaptureSession = [[AVCaptureSession alloc] init];
self.mCaptureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
image
 self.mProcessQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL);

//    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];
    }

//    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];
    }

//    8、输入与输出链接
    AVCaptureConnection *connection = [self.mCaptureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];

//    9、设置视频输出方向
    //注意: 一定要设置视频方向.否则视频会是朝向异常的.
    [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];

//    10、开始捕捉
    [self.mCaptureSession startRunning];

AVCaptureVideoDataOutputSampleBufferDelegate协议方法

在视频采集的同时,采集到的视频数据,即视频帧会自动回调视频采集回调方法captureOutput:didOutputSampleBuffer:fromConnection:,在该方法中处理采集到的原始视频数据,将其转换为metal纹理

didOutputSampleBuffer代理方法
主要是获取视频的帧数据,将其转换为metal纹理,函数流程如下

主要分为以下几步:

//    1、从sampleBuffer 获取视频像素缓存区对象,即获取位图
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);

size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);

//    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默认的帧速率与屏幕刷新频率一致,所以每当屏幕刷新时,都会回调 视频采集方法 和 视图渲染方法,以下是视图渲染方法执行流程

主要有以下几步

//        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];

//        6、展示显示的内容
        [commandBuffer presentDrawable:view.currentDrawable];

//        7、提交命令
        [commandBuffer commit];

//        8、清空当前纹理,准备下一次的纹理数据读取,
        //如果不清空,也是可以的,下一次的纹理数据会将上次的数据覆盖
        self.texture = NULL;

总结

视频采集流程总结
根据上述流程的解析,视频的采集主要有以下几步:

如何判断采集的数据是音频还是视频?
主要有以下两种判断方式:

如果需要判断当前采集的输出是视频还是音频,需要将connect对象设置为全局变量,然后在采集回调方法captureOutput:didOutputSampleBuffer:fromConnection:中判断全局的connection 是否等于 代理方法参数中的coneection ,如果相等,就是视频,反之是音频

在采集回调方法captureOutput:didOutputSampleBuffer:fromConnection:中判断output形参的类型,如果是AVCaptureVideoDataOutput 类型则是视频,反之,是音频

完整的代码见Github :21_21_Metal_视频渲染_OC

上一篇下一篇

猜你喜欢

热点阅读