使用Metal渲染视频文件,效果图如下: 大概思路如下:
- 利用工具类解码视频 拿到视频轨道数据输出对象 readerVideoTrackOutput
- draw 方法每次调用时,拿到视频下一帧的采样数据CMSampleBuffer
- 根据CMSampleBuffer生成两个纹理对象 并传入片元着色函数
- 在片元着色函数中,读取纹理的颜色值 YUV格式
- 根据定义的转换矩阵,将YUV转成RGB 然后显示
1. 准备
1.1 类型定义头文件
相较于之前的头文件,增加了ZConvertMatrix
结构体,在进行YUV
->RGB
时使用。
ZFragmentBufferIndex
片元函数缓冲区索引,利用MTLBuffer
将ZConvertMatrix
结构体的值传到片元函数,定义索引值,进行对应的取值。
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. 工具类
工具类负责根据视频路径加载视频资源,并在需要时提供视频帧的图像采样数据。
-
根据路径创建
AVAssetReader
,AVAssetReader
可以从原始数据里获取解码后的音视频数据 -
AVAssetReaderTrackOutput
通过使用AVAssetReader
的addOutput:
方法将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);
}