Metal(6)——渲染实时视频

1,723 阅读5分钟

本文为案例讲解,实现利用metal来将摄像头采集到的视频内容添加高斯模糊的滤镜,并实时渲染到屏幕上的功能。 主要结构如下:

  • setupMetal:设置与Metal相关的对象
  • setupCaptureSession: 视频采集相关,设置输入、输出
  • 视频采集回调方法: 根据采集到的视频图像生成纹理
  • draw: Metal绘制 添加滤镜

1. setupMetal

除了熟悉的MTKView, commandBuffer的创建以外。还需要额外做以下两点操作:

  1. 设置MTKViewdrawable 纹理是可读写的(默认是只读);
  2. 创建CVMetalTextureCache; 这是Core VideoMetal纹理缓存

由于需要不断的将采集到的图像生成纹理对象,输出到 view.currentDrawabletexture上,所以要设置纹理为可读可写的。

//允许读写操作
mtkView.framebufferOnly = false

创建纹理缓冲区textureCache,因为采集的视频数据是通过CoreVideo转换为metal纹理的,主要的用于存储转换后的metal纹理。

/*
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(nil, nil, mtkView.device!, nil, &textureCache)

2. setupCaptureSession

AVCaptureSession,用来管理与实时采集有关的操作,比如添加输入和输出,开始和停止捕捉,设置分辨率等。

//1.创建mCaptureSession
mCaptureSession = AVCaptureSession()
//设置视频采集的分辨率
mCaptureSession.sessionPreset = .hd1920x1080

创建一个单独的串行队列,处理视频采集时的回调处理。因为是实时的,有大量的数据需要处理,用单独的线程来处理,避免对主线程造成卡顿。

//2.创建串行队列
mProcessQueue = DispatchQueue(label: "mProcessQueue")

获取摄像头设备,这里选用后置摄像头

//3.获取摄像头设备(前置/后置摄像头设备)
let devices = AVCaptureDevice.devices(for: .video)
var inputCamera: AVCaptureDevice!
for device in devices {
    if device.position == .back {
        inputCamera = device
    }
}

设置输入设备,并将其添加到mCaptureSession

//4.将AVCaptureDevice 转换为AVCaptureDeviceInput
        mCaptureDeviceInput = try! AVCaptureDeviceInput(device: inputCamera)
        
//5. 将设备添加到mCaptureSession中
if mCaptureSession.canAddInput(mCaptureDeviceInput) {
    mCaptureSession.addInput(mCaptureDeviceInput)
}

设置数据输出,并将其添加到mCaptureSession中。

        //6.创建AVCaptureVideoDataOutput 对象
        mCaptureDeviceOutput = AVCaptureVideoDataOutput()
        
        /*设置视频帧延迟到底时是否丢弃数据.
            YES: 处理现有帧的调度队列在captureOutput:didOutputSampleBuffer:FromConnection:Delegate方法中被阻止时,对象会立即丢弃捕获的帧。
            NO: 在丢弃新帧之前,允许委托有更多的时间处理旧帧,但这样可能会内存增加.
            */
        mCaptureDeviceOutput.alwaysDiscardsLateVideoFrames = false
        
        //这里设置格式为BGRA,而不用YUV的颜色空间,避免使用Shader转换
        //注意:这里必须和后面CVMetalTextureCacheCreateTextureFromImage 保存图像像素存储格式保持一致.否则视频会出现异常现象.
        mCaptureDeviceOutput.videoSettings = [String(kCVPixelBufferPixelFormatTypeKey): kCVPixelFormatType_32BGRA]
        
         //设置视频捕捉输出的代理方法
        mCaptureDeviceOutput.setSampleBufferDelegate(self, queue: mProcessQueue)
        
        
         //7.添加输出
        if mCaptureSession.canAddOutput(mCaptureDeviceOutput) {
            mCaptureSession.addOutput(mCaptureDeviceOutput)
        }

将输入与输出链接,设置视频方向

let connection = mCaptureDeviceOutput.connection(with: .video)
        
connection?.videoOrientation = .portrait

注意: 一定要设置视频方向.否则视频会是朝向异常的.

最后开始视频捕捉

  mCaptureSession.startRunning()

3.处理捕获的视频

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

通过CMSampleBufferGetImageBuffer函数从sampleBuffer中获取视频像素缓存区对象,即视频帧数据,平常所说的位图

let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)

获取捕捉视频帧的宽和高

 let width = CVPixelBufferGetWidth(pixelBuffer!)
let height = CVPixelBufferGetHeight(pixelBuffer!)

CVMetalTextureCacheCreateTextureFromImage方法,创建一个CVMetalTexture对象,它与前面生成的pixelBuffer建立实时绑定。用这个方法,根据设置的参数,生成一个临时的纹理对象,但是这个对象并不是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纹理缓冲区。
var tmpTexture: CVMetalTexture?
CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer!, nil, .bgra8Unorm, width, height, 0, &tmpTexture)

将前面生成的tmpTexture对象,转换成Metal可以使用的MTLTexture类型的纹理对象。将这个纹理对象保存,以供绘制是使用。

texture = CVMetalTextureGetTexture(tmpTexture)

4.draw 绘制

  • 先判断当前是有纹理需要绘制,如果没有就不往下执行了
  • 创建指令缓冲器,已经很熟悉了
  • 拿到当前渲染mtkView目标的的currentDrawable?.texture作为目标渲染纹理
  • 创建一个高斯模糊滤镜,Metal提供了一个MetalPerformanceShaders库,里面有一些滤镜效果,MPSImageGaussianBlur 高斯模糊是其中之一
  • 已当前纹理作为滤镜的输入,已目标纹理作为滤镜的输出
  • 展示显示的内容
  • 提交命令
  • 清空纹理
func draw(in view: MTKView) {
        //1.判断是否获取了AVFoundation 采集的纹理数据
        if texture != nil {
            //2.创建指令缓冲
            let commandBuffer = commandQueue.makeCommandBuffer()
            
             //3.将MTKView 作为目标渲染纹理
            let drawingTexture = view.currentDrawable?.texture
            
            //4.设置滤镜
             /*
              MetalPerformanceShaders是Metal的一个集成库,有一些滤镜处理的Metal实现;
              MPSImageGaussianBlur 高斯模糊处理;
              */
            
             //创建高斯滤镜处理filter
             //注意:sigma值可以修改,sigma值越高图像越模糊;
            let filter = MPSImageGaussianBlur(device: mtkView.device!, sigma: 20)
            
            //5.MPSImageGaussianBlur以一个Metal纹理作为输入,以一个Metal纹理作为输出;
            //输入:摄像头采集的图像 self.texture
            //输出:创建的纹理 drawingTexture(其实就是view.currentDrawable.texture)
            filter.encode(commandBuffer: commandBuffer!, sourceTexture: texture, destinationTexture: drawingTexture!)
            
            //6.展示显示的内容
            commandBuffer?.present(view.currentDrawable!)
            
            commandBuffer?.commit()
            
            texture = nil
        }
    }

完整demo