Metal(7)——渲染视频文件

1,776 阅读5分钟

使用Metal渲染视频文件,效果图如下: 大概思路如下:

  • 利用工具类解码视频 拿到视频轨道数据输出对象 readerVideoTrackOutput
  • draw 方法每次调用时,拿到视频下一帧的采样数据CMSampleBuffer
  • 根据CMSampleBuffer生成两个纹理对象 并传入片元着色函数
  • 在片元着色函数中,读取纹理的颜色值 YUV格式
  • 根据定义的转换矩阵,将YUV转成RGB 然后显示

1. 准备

1.1 类型定义头文件

相较于之前的头文件,增加了ZConvertMatrix结构体,在进行YUV->RGB时使用。
ZFragmentBufferIndex片元函数缓冲区索引,利用MTLBufferZConvertMatrix结构体的值传到片元函数,定义索引值,进行对应的取值。
ZFragmentTextureIndex,本案例中需要用到两个纹理,所以定义了两个纹理索引,进行对应取值。

//顶点数据结构
typedef struct {
    //顶点坐标(x,y,z,w)
    vector_float4 position;
    //纹理坐标(s,t)
    vector_float2 textureCoordinate;
}ZVertex;

//转换矩阵
typedef struct {
    //三维矩阵
    matrix_float3x3 matrix;
    //偏移量
    vector_float3 offset;
} ZConvertMatrix;

//顶点函数输入索引
typedef enum ZVertexInputIndex {
    ZVertexInputIndexVertices = 0,
} ZVertexInputIndex;

//片元函数缓存区索引
typedef enum ZFragmentBufferIndex {
    ZFragmentBufferIndexMatrix = 0,
} ZFragmentBufferIndex;

//片元函数纹理索引
typedef enum ZFragmentTextureIndex {
    ZFragmentTextureIndexTextureY = 0,
    ZFragmentTextureIndexTextureUV = 1,
} ZFragmentTextureIndex;

1.2 MTKView

MTKView的创建流程已经很熟悉了,不再赘述,直接上代码:

//MTKView 设置
func setupMTKView() {
    //1.初始化mtkView
    mtkView = MTKView(frame: view.bounds)
    // 获取默认的device
    mtkView.device = MTLCreateSystemDefaultDevice()
    //设置self.view = self.mtkView;
    view = mtkView
    //设置代理
    mtkView.delegate = self
    //获取视口size
    viewportSize = vector_int2(Int32(mtkView.drawableSize.width), Int32(mtkView.drawableSize.height))
    mtkView.preferredFramesPerSecond = 24
}

1.3 setupPipeline

设置渲染管道

func setupPipeline() {
    //1 获取.metal
    /*
     newDefaultLibrary: 默认一个metal 文件时,推荐使用
     newLibraryWithFile:error: 从Library 指定读取metal 文件
     newLibraryWithData:error: 从Data 中获取metal 文件
     */
    let defaultLibrary = mtkView.device?.makeDefaultLibrary()
    let vertexFunction = defaultLibrary?.makeFunction(name: "vertexShader")
    let fragmentFunction = defaultLibrary?.makeFunction(name: "fragmentShader")
        
    //2.渲染管道描述信息类
    let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
    pipelineStateDescriptor.vertexFunction = vertexFunction
    pipelineStateDescriptor.fragmentFunction = fragmentFunction
    // 设置颜色格式
    pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
    
    //3.初始化渲染管道根据渲染管道描述信息
    // 创建图形渲染管道,耗性能操作不宜频繁调用
    pipelineState = try! mtkView.device?.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
        
    //4.CommandQueue是渲染指令队列,保证渲染指令有序地提交到GPU
    commandQueue = mtkView.device?.makeCommandQueue()
}

1.4 顶点数据

// 设置顶点
func setupVertex() {
    //1.顶点坐标(x,y,z,w);纹理坐标(x,y)
    //注意: 为了让视频全屏铺满,所以顶点大小均设置[-1,1]
    let quadVertices = [        ZVertex(position: [1.0, -1.0, 0.0, 1.0], textureCoordinate: [1.0, 1.0]),
        ZVertex(position: [-1.0, -1.0, 0.0, 1.0], textureCoordinate: [0.0, 1.0]),
        ZVertex(position: [-1.0, 1.0, 0.0, 1.0], textureCoordinate: [0.0, 0.0]),
            
        ZVertex(position: [1.0, -1.0, 0.0, 1.0], textureCoordinate: [1.0, 1.0]),
        ZVertex(position: [-1.0, 1.0, 0.0, 1.0], textureCoordinate: [0.0, 0.0]),
        ZVertex(position: [1.0, 1.0, 0.0, 1.0], textureCoordinate: [1.0, 0.0])]
        
    //2.创建顶点缓存区
    vertices = mtkView.device?.makeBuffer(bytes: quadVertices, length: MemoryLayout<ZVertex>.size*quadVertices.count, options: .storageModeShared)
        
    //3.计算顶点个数
    numVertices = quadVertices.count
        
}

1.5 转换矩阵

在上篇文章已经介绍了YUV->RGB的计算公式。下面定义三种转换矩阵,在进行转换时,YUV的颜色值与矩阵相乘,就相当于矩阵的值成为了计算公式的固定系数,与不同的YUV分量相乘,得到最终的RGB的值。
定义矩阵:

//1.转化矩阵
// BT.601, which is the standard for SDTV.
let kColorConversion601DefaultMatrix = matrix_float3x3(columns: (
    simd_float3(1.164,  1.164, 1.164),
    simd_float3(0.0, -0.392, 2.017),
    simd_float3(1.596, -0.813, 0.0)))

// BT.601 full range
let kColorConversion601FullRangeMatrix = matrix_float3x3(columns: (
    simd_float3(1.0,    1.0,    1.0),
    simd_float3(0.0,    -0.343, 1.765),
    simd_float3(1.4,    -0.711, 0.0)))
        
// BT.709, which is the standard for HDTV.
let kColorConversion709DefaultMatrix = matrix_float3x3(columns: (
    simd_float3(1.164,  1.164, 1.164),
    simd_float3(0.0, -0.213, 2.112),
    simd_float3(1.793, -0.533,   0.0)))

定义偏移量。偏移量为固定值。

YUV细分的话有Y'UV,YUV,YCbCr,YPbPr等格式,目前在计算机上使用的主要是YCbCr,因此说起YUV时主要指的是YCbCr(本文后续均称YUV),Cb表示蓝色浓度偏移量,Cr表示红色浓度偏移量。

//2.偏移量
let kColorConversion601FullRangeOffset = vector_float3(-(16.0/255.0), -0.5, -0.5)

创建转换矩阵结构体,并将数据存入转换矩阵缓冲区

//3.创建转化矩阵结构体.
var matrix = ZConvertMatrix()
//设置转化矩阵
/*
 kColorConversion601DefaultMatrix;
 kColorConversion601FullRangeMatrix;
 kColorConversion709DefaultMatrix;
 */
matrix.matrix = kColorConversion601FullRangeMatrix
matrix.offset = kColorConversion601FullRangeOffset

//4.创建转换矩阵缓存区.
convertMatrix = mtkView.device?.makeBuffer(bytes: &matrix, length: MemoryLayout<ZConvertMatrix>.size, options: .storageModeShared)

2. 工具类

工具类负责根据视频路径加载视频资源,并在需要时提供视频帧的图像采样数据。

  • 根据路径创建AVAssetReaderAVAssetReader可以从原始数据里获取解码后的音视频数据

  • AVAssetReaderTrackOutput 通过使用AVAssetReaderaddOutput:方法将AVAssetReaderTrackOutput实例添加到AVAssetReader,客户端可以读取资源轨道的媒体数据。 AVAssetReaderOutPut包含三种类型的输出:

    • AVAssetReaderTrackOutput:用于从AVAssetReader存储中读取单个轨道的媒体样本
    • AVAssetReaderAudioMixOutput:用于读取音频样本
    • AVAssetReaderVideoCompositionOutput:用于读取一个或多个轨道中的帧合成的视频帧
  • 通过AVAssetReaderTrackOutput可以获取视频帧的图像采样数据CMSampleBuffer

3. 绘制

在draw里主要做两件事:

  • 从reader中获取图像数据samperBuffer
  • 根据samperBuffer生成两个纹理,textureY和textureUV

3.1 readerBuffer

  • 判断readerVideoTrackOutput 是否创建成功,即是否可出输出图像
  • 输出下一帧图像
  • 判断读取完毕,重新初始化,准备下一次的读取
  • 返回sampleBuffer
//读取Buffer 数据
func readBuffer() -> CMSampleBuffer? {
   //锁定
   lock.lock()
   
   var sampleBuffer: CMSampleBuffer?
   //1.判断readerVideoTrackOutput 是否创建成功.
   if readerVideoTrackOutput != nil {
       
       //复制下一个缓存区的内容到sampleBufferRef
       sampleBuffer = readerVideoTrackOutput.copyNextSampleBuffer()
   } else {
       lock.unlock()
       return nil
   }
   
    //2.判断assetReader 并且status 是已经完成读取 则重新清空readerVideoTrackOutput/assetReader.并重新初始化它们
    if assetReader != nil && assetReader.status == AVAssetReader.Status.completed {
       print(Date().timeIntervalSince1970)
       print("customInit")
       readerVideoTrackOutput = nil
       assetReader = nil
       setupAsset()
    }
        
    lock.unlock()
        
    //3.返回读取到的sampleBufferRef 数据
    return sampleBuffer
}

3.2 生成纹理

  • 首先要创建好CVMetalTextureCache, CoreVideo提供的可供CPU和GPU共享的,高速缓存通道。
  • CMSampleBuffer->CVPixelBuffer
  • 利用CVMetalTextureCache,将位于GPU缓冲区中的CVPixelBuffer对象,生成CVMetalTexture对象
  • 利用CVMetalTexture对象生成可供metal使用的MTLTexture纹理对象
  • 将生成的MTLTexture纹理对象传入片元着色函数,以供使用

具体代码,详见demo.

4. 颜色转化

  • 设置纹理采样器
  • 对纹理进行采样,得要YUV格式的颜色值
  • YUV->GRB
  • 创建RGBA, 最终返回
// stage_in表示这个数据来自光栅化。(光栅化是顶点处理之后的步骤,业务层无法修改)
// texture表明是纹理数据,CCFragmentTextureIndexTextureY是索引
// texture表明是纹理数据,CCFragmentTextureIndexTextureUV是索引
// buffer表明是缓存数据, CCFragmentInputIndexMatrix是索引
fragment float4 fragmentShader(RasterizerData input [[stage_in]],
                               texture2d<float> textureY [[texture(ZFragmentTextureIndexTextureY)]],
                               texture2d<float> textureUV [[texture(ZFragmentTextureIndexTextureUV)]],
                               constant ZConvertMatrix *convertMatrix [[buffer(ZFragmentBufferIndexMatrix)]]){
    
    //1.获取纹理采样器
    constexpr sampler textureSampler(mag_filter::linear,
                                     min_filter::linear);
    
    /*
    2. 读取YUV 颜色值
       textureY.sample(textureSampler, input.textureCoordinate).r
       从textureY中的纹理采集器中读取,纹理坐标对应上的R值.(Y)
       textureUV.sample(textureSampler, input.textureCoordinate).rg
       从textureUV中的纹理采集器中读取,纹理坐标对应上的RG值.(UV)
    */
    float3 yuv = float3(textureY.sample(textureSampler, input.textureCoordinate).r,
                        textureUV.sample(textureSampler, input.textureCoordinate).rg);
    
     //3.将YUV 转化为 RGB值.convertMatrix->matrix * (YUV + convertMatrix->offset)
    float3 rgb = convertMatrix->matrix * (yuv + convertMatrix->offset);
    
    //4.返回颜色值(RGBA)
    return float4(rgb, 1.0);
}

完整demo