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

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

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


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

image.png


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


  • 1、通过AVFoundation进行视频数据的采集,并将采集到的原始数据存储到CMSampleBufferRef中,即视频帧数据(视频帧其实本质也是一张图片)
  • 2、通过CoreVideoCMSampleBufferRef中存储的图像数据,转换为Metal可以直接使用的纹理
  • 3、将Metal纹理进行渲染,并即刻显示到屏幕上

在实际的开发应用中,AVFoundation提供了一个layer,即AVCaptureVideoPreviewLayer 预览层,我们可以使用预览层直接预览视频采集后的即可渲染,用于替代思路中2、3步。

根据官方文档-AVCaptureVideoPreviewLayer说明,AVCaptureVideoPreviewLayerCALayer 的子类,用于在输入设备捕获视频时显示视频,此预览图层与捕获会话结合使用,主要有以下三步:


  • 创建预览层对象
  • 将预览层与captureSession链接
  • 将预览层加到view的子layer中
//创建一个预览层。
let previewLayer = AVCaptureVideoPreviewLayer()
//将预览层与捕获会话连接。
previewLayer.session = captureSession
//将预览图层添加到视图的图层层次结构中
view.layer.addSublayer(previewLayer)

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

image.png

主要分为3部分


  • viewDidLoad函数:初始化Metal和视频采集的准备工作
  • MTKViewDelegate协议方法:视频采集数据转换为纹理
  • AVCaptureVideoDataOutputSampleBufferDelegate协议方法:将采集转换后的纹理渲染到屏幕上


viewDidLoad函数


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


image.png

分为以下两部分


  • 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是如何管理设备的输入 & 输出,以及与主队列之间的关系

image.png

image.png


 self.mProcessQueue = dispatch_queue_create("mProcessQueue", DISPATCH_QUEUE_SERIAL);


设置输入设备


  • 获取后置摄像头设备AVCaptureDevice
    通过获取设备数组,循环判断找到后置摄像头,将后置摄像头设备为当前的输入设备
  • 通过摄像头设备创建AVCaptureDeviceInput
    将AVCaptureDevice 转换为 AVCaptureDeviceInput,主要是因为 AVCaptureSession 无法直接使用 AVCaptureDevice,所以需要将device转换为deviceInput
  • 输入设备添加到captureSession中
    在添加之前,需要通过captureSessioncanAddInput函数判断是否可以添加输入设备,如果可以,则通过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纹理,函数流程如下

image.png

didOutputSampleBuffer代理方法流程


主要分为以下几步:


  • sampleBuffer中获取位图
    通过CMSampleBufferGetImageBuffer函数从sampleBuffer形参中获取视频像素缓存区对象,即视频帧数据,平常所说的位图
//    1、从sampleBuffer 获取视频像素缓存区对象,即获取位图
    CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
  • 获取捕捉视频帧的宽高
    通过CoreVideo中的CVPixelBufferGetWidthCVPixelBufferGetHeight函数获取宽高
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默认的帧速率与屏幕刷新频率一致,所以每当屏幕刷新时,都会回调 视频采集方法 和 视图渲染方法,以下是视图渲染方法执行流程

image.pngdrawInMTKView代理方法流程



主要有以下几步


  • 判断纹理是否获取成功
    即纹理不为空,如果纹理为空,则没必要执行视图渲染流程
  • 通过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 类型则是视频,反之,是音频


相关文章
|
开发工具 Windows
Windows平台RTMP推送|轻量级RTSP服务实现本地摄像头|屏幕|叠加数据预览
大家在做Windows平台RTMP推送或轻量级RTSP服务的时候,不管是采集屏幕还是采集摄像头,亦或屏幕摄像头的叠加模式,总会有这样的诉求,采集到的数据,希望能本地看看具体采集的数据或者图像实际效果,也就是本次介绍的“预览”功能。
246 0
|
编解码 开发工具 Android开发
Android平台RTSP轻量级服务|RTMP推送摄像头或屏幕之音频接口设计
好多开发者在做Android平台录像或者RTSP轻量级服务、RTMP推送相关模块时,对需要设计哪些常用接口会心存疑惑,本文主要以大牛直播SDK(官方)为例,简单介绍下Android平台直播推送SDK所有音频相关的接口,感兴趣的开发者可以看看。
108 0
|
编解码 Android开发 数据安全/隐私保护
Android平台外部编码数据(H264/H265/AAC/PCMA/PCMU)实时预览播放技术实现
好多开发者可能疑惑,外部数据实时预览播放,到底有什么用? 是的,一般场景是用不到的,我们在开发这块前几年已经开发了非常稳定的RTMP、RTSP直播播放模块,不过也遇到这样的场景,部分设备输出编码后(视频:H.264/H.265,音频:AAC/PCMA/PCMU)的数据,比如无人机或部分智能硬件设备,回调出来的H.264/H.265数据,除了想转推到RTMP、轻量级RTSP服务或GB28181外,还需要本地预览甚至对数据做二次处理(视频分析、实时水印字符叠加等,然后二次编码),基于这样的场景诉求,我们开发了Android平台外部编码数据实时预览播放模块。
161 0
|
Web App开发 编解码 前端开发
VUE网页实时播放海康、大华摄像头RTSP视频流完全方案,300毫秒延迟,支持H.265、可多路同时播放
在遍地都是摄像头的今天,往往需要在各种信息化、数字化、可视化B/S系统中集成实时视频流播放等功能,海康、大华、华为等厂家摄像头或录像机等设备一般也都遵循监控行业标准,支持国际标准的主流传输协议RTSP输出,而Chrome、Firefox、Edge等新一代浏览器从2015年开始取消了NPAPI插件技术支持导致RTSP流无法直接原生播放了
3142 0
|
1月前
|
UED
鸿蒙next版开发:相机开发-适配不同折叠状态的摄像头变更(ArkTS)
在HarmonyOS 5.0中,ArkTS提供了强大的相机开发能力,特别是针对折叠屏设备的摄像头适配。本文详细介绍了如何在ArkTS中检测和适配不同折叠状态下的摄像头变更,确保相机应用在不同设备状态下的稳定性和用户体验。通过代码示例展示了具体的实现步骤。
81 8
|
1月前
|
编解码 vr&ar 图形学
Unity下如何实现低延迟的全景RTMP|RTSP流渲染
随着虚拟现实技术的发展,全景视频逐渐成为新的媒体形式。本文详细介绍了如何在Unity中实现低延迟的全景RTMP或RTSP流渲染,包括环境准备、引入依赖、初始化客户端、解码与渲染、优化低延迟等步骤,并提供了具体的代码示例。适用于远程教育、虚拟旅游等实时交互场景。
34 2
|
3月前
|
编解码 开发工具 Android开发
Android平台实现屏幕录制(屏幕投影)|音频播放采集|麦克风采集并推送RTMP或轻量级RTSP服务
Android平台屏幕采集、音频播放声音采集、麦克风采集编码打包推送到RTMP和轻量级RTSP服务的相关技术实现,做成高稳定低延迟的同屏系统,还需要有配套好的RTMP、RTSP直播播放器
|
4月前
|
编解码 vr&ar 图形学
惊世骇俗!Unity下如何实现低至毫秒级的全景RTMP|RTSP流渲染,颠覆你的视觉体验!
【8月更文挑战第14天】随着虚拟现实技术的进步,全景视频作为一种新兴媒体形式,在Unity中实现低延迟的RTMP/RTSP流渲染变得至关重要。这不仅能够改善用户体验,还能广泛应用于远程教育、虚拟旅游等实时交互场景。本文介绍如何在Unity中实现全景视频流的低延迟渲染,并提供代码示例。首先确保Unity开发环境及所需插件已就绪,然后利用`unity-rtsp-rtmp-client`插件初始化客户端并设置回调。通过FFmpeg等工具解码视频数据并更新至全景纹理,同时采用硬件加速、调整缓冲区大小等策略进一步降低延迟。此方案需考虑网络状况与异常处理,确保应用程序的稳定性和可靠性。
108 1
|
6月前
|
自然语言处理 程序员 Windows
[UE虚幻引擎] DTSpeechVoice 文字转语音播放 插件说明
这个插件用于在虚幻引擎(UE)中通过蓝图将文本转化为语音播放,利用Windows内置的语音引擎,支持Win10和Win11。确保电脑已安装语音系统,可能需要额外下载语言包以支持多语言播放。蓝图操作包括添加Speech Voice Component到Actor,使用Speak节点播放文本,Set Volume调整音量,Set Rate改变播放速度,Pause和Resume控制播放状态,Stop则停止播放且无法恢复。此外,Get Tokens和Set Token用于管理语音类型。更多详情可访问[80后程序员](https://dt.cq.cn/archives/1008?from=aliyun)
99 5
|
6月前
|
图形学 异构计算
蓝易云 - Unity下如何实现低延迟的全景RTMP|RTSP流渲染
以上就是在Unity中实现低延迟的全景RTMP/RTSP流渲染的基本步骤。具体的实现可能会根据你的具体需求和所使用的库有所不同。
119 0