iOS图像:Metal 实现视频采集和渲染

原创:知识探索型文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、使用Metal实现视频采集和预览(可省略)
    • 1、准备工作
    • 2、设置Metal
    • 3、设置AVFoundation进行视频采集
    • 4、实现视频采集回调方法
    • 5、实现视图渲染的方法
  • 二、使用Metal实现视频渲染
    • 1、颜色编码格式
    • 2、AssetReader 工具类
    • 3、RendererCamera 视图控制器
    • 4、ShaderTypes 桥接文件
    • 5、Shaders.metal 顶点片源函数文件
  • Demo
  • 参考文献

一、使用Metal实现视频采集和预览(可省略)

这里使用MTKView来进行视频预览,但AVFoundation自身也提供预览图层AVCaptureVideoPreviewLayer,所以在实际开发过程中直接使用AVFoundation提供的预览图层即可,没必要多此一举使用MTKView来进行视频预览,本Demo仅是教学目的而用此方式。

1、准备工作

导入框架
#import 
#import 
#import 
#import 
// 有一些滤镜处理的Metal实现
#import 
代理方法
@interface ViewController ()

@end
私有属性
@property (nonatomic, strong) MTKView *mtkView;// 展示视图
@property (nonatomic, strong) AVCaptureSession *captureSession;// 负责输入和输出设备之间的数据传递
@property (nonatomic, strong) AVCaptureDeviceInput *captureDeviceInput;// 负责从AVCaptureDevice获得输入数据
@property (nonatomic, strong) AVCaptureVideoDataOutput *captureDeviceOutput;// 输出设备
@property (nonatomic, strong) dispatch_queue_t processQueue;// 处理队列
@property (nonatomic, assign) CVMetalTextureCacheRef textureCache;// 纹理缓存区
@property (nonatomic, strong) id commandQueue;// 命令队列
@property (nonatomic, strong) id texture;// 纹理
整体流程
- (void)viewDidLoad
{
    [super viewDidLoad];

    // 设置Metal
    [self setupMetal];
    // 设置AVFoundation
    [self setupCaptureSession];
}

2、设置Metal

- (void)setupMetal
{
}
❶ 获取MTKView
self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
self.mtkView.device = MTLCreateSystemDefaultDevice();
[self.view insertSubview:self.mtkView atIndex:0];
self.mtkView.delegate = self;
// 
self.mtkView.framebufferOnly = NO;
❷ 设置MTKView的drawable纹理是可读写的(默认是只读)
self.mtkView.framebufferOnly = NO;
❸ 创建命令队列
self.commandQueue = [self.mtkView.device newCommandQueue];
❹ 创建Core Video的Metal纹理缓存区
  • allocator:内存分配器,使用默认选项NULL
  • cacheAttributes:缓存区,默认为NULL
  • metalDevice:
  • textureAttributes:创建纹理选项的字典,使用默认选项NULL
  • cacheOut:返回时,包含新创建的纹理缓存
CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);

3、设置AVFoundation进行视频采集

- (void)setupCaptureSession
{    
}
❶ 创建captureSession
// 1.创建captureSession
self.captureSession = [[AVCaptureSession alloc] init];
// 设置视频采集的分辨率
self.captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
❷ 创建串行队列(视频是有序的)
self.processQueue = dispatch_queue_create("processQueue", DISPATCH_QUEUE_SERIAL);
❸ 获取摄像头设备(前置/后置摄像头设备)
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
AVCaptureDevice *inputCamera = nil;
// 循环设备数组,找到后置摄像头,设置为当前inputCamera
for (AVCaptureDevice *device in devices)
{
    if ([device position] == AVCaptureDevicePositionBack)
    {
        inputCamera = device;
    }
}
❹ 将AVCaptureDevice转换为AVCaptureDeviceInput
self.captureDeviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:inputCamera error:nil];
// 将设备添加到captureSession中
if ([self.captureSession canAddInput:self.captureDeviceInput])
{
    [self.captureSession addInput:self.captureDeviceInput];
} 
❺ 创建AVCaptureVideoDataOutput对象
self.captureDeviceOutput = [[AVCaptureVideoDataOutput alloc] init];
// 设置视频帧延迟时是否丢弃数据
[self.captureDeviceOutput setAlwaysDiscardsLateVideoFrames:NO];

// 这里设置格式为BGRA,而不用YUV的颜色空间,避免使用Shader转换
// 这里必须和后面CVMetalTextureCacheCreateTextureFromImage保存图像像素存储格式保持一致,否则视频会出现异常现象
[self.captureDeviceOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];

// 设置视频捕捉输出的代理方法
[self.captureDeviceOutput setSampleBufferDelegate:self queue:self.processQueue];

// 添加输出
if ([self.captureSession canAddOutput:self.captureDeviceOutput])
{
    [self.captureSession addOutput:self.captureDeviceOutput];
}
❻ 将输入与输出连接
AVCaptureConnection *connection = [self.captureDeviceOutput connectionWithMediaType:AVMediaTypeVideo];
// 一定要设置视频方向,否则视频方向是异常的
[connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
❼ 开始捕捉
[self.captureSession startRunning];

4、实现视频采集回调方法

这里只进行视频的预览,不需要进行压缩和编码等操作,所以当拿到原始数据后就可以直接进行渲染。

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
}
❶ 从sampleBuffer获取视频像素缓存区对象
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
❷ 获取捕捉视频的宽和高
size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer);
❸ 根据视频像素缓存区创建Metal纹理缓存区
  • allocator:内存分配器,默认kCFAllocatorDefault
  • textureCache:纹理缓存区对象
  • sourceImage:视频图像缓冲区
  • textureAttributes:纹理参数字典,默认为NULL
  • pixelFormat:图像缓存区数据的Metal像素格式常量。注意如果MTLPixelFormatBGRA8Unorm和摄像头采集时设置的颜色格式不一致,则会出现图像异常的情况
  • width:纹理图像的宽度(像素)
  • height:纹理图像的高度(像素)
  • planeIndex:如果图像缓冲区是平面的,则为映射纹理数据的平面索引。对于非平面图像缓冲区忽略
  • textureOut:返回时,返回创建的Metal纹理缓冲区
CVMetalTextureRef tmpTexture = NULL;
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, self.textureCache, pixelBuffer, NULL, MTLPixelFormatBGRA8Unorm, width, height, 0, &tmpTexture);

// 判断纹理缓冲区是否创建成功
if(status == kCVReturnSuccess)
{
    ...
}
❹ 设置可绘制纹理的当前大小
self.mtkView.drawableSize = CGSizeMake(width, height);
❺ 返回纹理缓冲区的Metal纹理对象
self.texture = CVMetalTextureGetTexture(tmpTexture);
❻ 使用完毕,则释放纹理缓冲区
CFRelease(tmpTexture);

5、实现视图渲染的方法

- (void)drawInMTKView:(MTKView *)view
{
    // 判断是否获取了AVFoundation采集的纹理数据
    if (self.texture)
    {
        ...
    }
}
❶ 创建指令缓冲
id commandBuffer = [self.commandQueue commandBuffer];
❷ 将MTKView作为目标渲染纹理
id drawingTexture = view.currentDrawable.texture;
❸ 创建高斯滤镜,sigma值越高图像越模糊
MPSImageGaussianBlur *filter = [[MPSImageGaussianBlur alloc] initWithDevice:self.mtkView.device sigma:1];
❹ 高斯滤镜以Metal纹理作为输入和输出
// 输入:摄像头采集的图像 self.texture
// 输出:创建的纹理 drawingTexture(其实就是view.currentDrawable.texture)
[filter encodeToCommandBuffer:commandBuffer sourceTexture:self.texture destinationTexture:drawingTexture];
❺ 展示显示的内容并提交命令
[commandBuffer presentDrawable:view.currentDrawable];
[commandBuffer commit];
❻ 清空当前纹理,准备下一次的纹理数据读取
self.texture = NULL;

二、使用Metal实现视频渲染

1、颜色编码格式

RGB 颜色编码

RGB 三个字母分别代表了 红(Red)、绿(Green)、蓝(Blue),这三种颜色称为 三原色,将它们以不同的比例相加,可以产生多种多样的颜色。

在图像显示中,一张 1280 * 720 大小的图片,就代表着它有 1280 * 720 个像素点。其中每一个像素点的颜色显示都采用 RGB 编码方法,将 RGB 分别取不同的值,就会展示不同的颜色。

RGB 图像中,每个像素点都有红、绿、蓝三个原色,其中每种原色都占用 8 bit,也就是一个字节,那么一个像素点也就占用 24 bit,也就是三个字节。

一张 1280 * 720 大小的图片,就占用 1280 * 720 * 3 / 1024 / 1024 = 2.63 MB 存储空间。

YUV 颜色编码

YUV 颜色编码采用的是 明亮度 和 色度 来指定像素的颜色。其中,Y 表示明亮度,而 U 和 V 表示色度。而色度又定义了颜色的两个方面:色调和饱和度。

和 RGB 表示图像类似,每个像素点都包含 Y、U、V 分量。但是它的 Y 和 UV 分量是可以分离的,如果没有 UV 分量一样可以显示完整的图像,只不过是黑白的。

对于 YUV 图像来说,并不是每个像素点都需要包含了 Y、U、V 三个分量,根据不同的采样格式,可以每个 Y 分量都对应自己的 UV 分量,也可以几个 Y 分量共用 UV 分量。

RGB 到 YUV 的转换

对于图像显示器来说,它是通过 RGB 模型来显示图像的,而在传输图像数据时又是使用 YUV 模型,这是因为 YUV 模型可以节省带宽。因此就需要采集图像时将 RGB 模型转换到 YUV 模型,显示时再将 YUV 模型转换为 RGB 模型。RGB 到 YUV 的转换,就是将图像所有像素点的 R、G、B 分量转换到 Y、U、V 分量。

YUV 4:4:4 采样

YUV 4:4:4 采样,意味着 Y、U、V 三个分量的采样比例相同,因此在生成的图像里,每个像素的三个分量信息完整,都是 8 bit,也就是一个字节。

Y 分量用叉表示,UV 分量用圆圈表示

可以看到这种采样方式的图像和 RGB 颜色模型的图像大小是一样,并没有达到节省带宽的目的,当将 RGB 图像转换为 YUV 图像时,也是先转换为 YUV 4:4:4 采样的图像。

假如图像像素为:[Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]

那么采样的码流为:Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3 

最后映射出的像素点依旧为 [Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3] 

可以看到这种采样方式的图像和 RGB 颜色模型的图像大小是一样,并没有达到节省带宽的目的,当将 RGB 图像转换为 YUV 图像时,也是先转换为 YUV 4:4:4 采样的图像。

YUV 4:2:2 采样

YUV 4:2:2 采样,意味着 UV 分量是 Y 分量采样的一半,Y 分量和 UV 分量按照 2 : 1 的比例采样。如果水平方向有 10 个像素点,那么采样了 10 个 Y 分量,而只采样了 5 个 UV 分量。

Y 分量用叉表示,UV 分量用圆圈表示
 假如图像像素为:[Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]

 那么采样的码流为:Y0 U0 Y1 V1 Y2 U2 Y3 V3 

 其中,每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一个采集一个

 最后映射出的像素点为 [Y0 U0 V1]、[Y1 U0 V1]、[Y2 U2 V3]、[Y3 U2 V3]

采样的码流映射为像素点,还是要满足每个像素点有 Y、U、V 三个分量。但是可以看到,第一和第二像素点公用了 U0、V1 分量,第三和第四个像素点公用了 U2、V3 分量,这样就节省了图像空间。

一张 1280 * 720 大小的图片,在 YUV 4:2:2 采样时的大小为:(1280 * 720 * 8 + 1280 * 720 * 0.5 * 8 * 2)/ 8 / 1024 / 1024 = 1.76 MB 

可以看到 YUV 4:2:2 采样的图像比 RGB 模型图像节省了三分之一的存储空间,在传输时占用的带宽也会随之减少。

YUV 4:2:0 采样

YUV 4:2:0 采样,并不是指只采样 U 分量而不采样 V 分量。而是指,在每一行扫描时,只扫描一种色度分量(U 或者 V),和 Y 分量按照 2 : 1 的方式采样。比如,第一行扫描时,YU 按照 2 : 1 的方式采样,那么第二行扫描时,YV 分量按照 2:1 的方式采样。对于每个色度分量来说,它的水平方向和竖直方向的采样和 Y 分量相比都是 2:1 。

Y 分量用叉表示,UV 分量用圆圈表示

假设第一行扫描了 U 分量,第二行扫描了 V 分量,那么需要扫描两行才能够组成完整的 UV 分量。

假设图像像素为:

[Y0 U0 V0]、[Y1 U1 V1]、 [Y2 U2 V2]、 [Y3 U3 V3]
[Y5 U5 V5]、[Y6 U6 V6]、 [Y7 U7 V7] 、[Y8 U8 V8]

那么采样的码流为:Y0 U0 Y1 Y2 U2 Y3 Y5 V5 Y6 Y7 V7 Y8

其中,每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。

最后映射出的像素点为:

[Y0 U0 V5]、[Y1 U0 V5]、[Y2 U2 V7]、[Y3 U2 V7]
[Y5 U0 V5]、[Y6 U0 V5]、[Y7 U2 V7]、[Y8 U2 V7]

从映射出的像素点中可以看到,四个 Y 分量是共用了一套 UV 分量,而且是按照 2*2 的小方格的形式分布的,相比 YUV 4:2:2 采样中两个 Y 分量共用一套 UV 分量,这样更能够节省空间。

一张 1280 * 720 大小的图片,在 YUV 4:2:0 采样时的大小为:(1280 * 720 * 8 + 1280 * 720 * 0.25 * 8 * 2)/ 8 / 1024 / 1024 = 1.32 MB 

2、AssetReader 工具类

a、准备工作
导入的框架
#import 
提供的方法
// 初始化 AssetReader
- (instancetype)initWithUrl:(NSURL *)url;

// 从mov文件读取CMSampleBufferRef数据
- (CMSampleBufferRef)readBuffer;
成员变量
@implementation AssetReader
{
    AVAssetReaderTrackOutput *readerVideoTrackOutput;// 视频输出轨道
    AVAssetReader *assetReader;// 可以从原始数据里获取解码后的音视频数据
    NSURL *videoUrl;// 视频地址
    NSLock *lock;// 锁
}
实现初始化方法
- (instancetype)initWithUrl:(NSURL *)url
{
    self = [super init];
    if(self != nil)
    {
        videoUrl = url;
        lock = [[NSLock alloc] init];
        [self setUpAsset];
    }
    return self;
}

b、设置Asset
- (void)setUpAsset
{
}
❶ 创建AVURLAsset

AVURLAssetAVAsset的子类,用于从本地或者远程URL初始化资源。

// 默认为NO,设置为YES则可获取精确的时长
NSDictionary *inputOptions = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:AVURLAssetPreferPreciseDurationAndTimingKey];
AVURLAsset *inputAsset = [[AVURLAsset alloc] initWithURL:videoUrl options:inputOptions];
❷ 异步加载资源

对资源所需的键执行标准的异步载入操作,这样就可以访问资源的tracks视频轨道属性时不会受到阻碍。

__weak typeof(self) weakSelf = self;// 解决循环引用
NSString *tracks = @"tracks";// 键名称
[inputAsset loadValuesAsynchronouslyForKeys:@[tracks] completionHandler: ^{
        __strong typeof(self) strongSelf = weakSelf;// 延长self生命周期
        ...
    });
}];
❸ 开辟子线程并发队列异步函数来处理读取的inputAsset
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      NSError *error = nil;

      // 获取视频轨道状态码
      AVKeyValueStatus tracksStatus = [inputAsset statusOfValueForKey:@"tracks" error:&error];
      // 如果状态不等于成功加载则返回并打印错误信息
      if (tracksStatus != AVKeyValueStatusLoaded)
      {
          NSLog(@"视频轨道状态为失败,错误信息为:%@", error);
          return;
      }
      // 处理读取的inputAsset
      [strongSelf processWithAsset:inputAsset];
});

c、处理获取到的asset
- (void)processWithAsset:(AVAsset *)asset
{
    [lock lock];// 锁定
    ...
    [lock unlock];// 取消锁
}
❶ 创建AVAssetReader
NSError *error = nil;
assetReader = [AVAssetReader assetReaderWithAsset:asset error:&error];
❷ 设置像素格式为YUV 4:2:0
NSMutableDictionary *outputSettings = [NSMutableDictionary dictionary];
[outputSettings setObject:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) forKey:(id)kCVPixelBufferPixelFormatTypeKey]; 
❸ 读取资源中的视频信息
readerVideoTrackOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:[[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0] outputSettings:outputSettings];
❹ 缓存区的数据输出之前是否会被复制

默认为NO,设置为YES则表示输出的是从缓存区复制的数据,所以你可以自由的修改这些数据。

readerVideoTrackOutput.alwaysCopiesSampleData = NO;
❺ 为assetReader填充输出
[assetReader addOutput:readerVideoTrackOutput];
❻ assetReader开始读取,倘若URL错误导致无法读取则进行提示
if ([assetReader startReading] == NO)
{
    NSLog(@"URL错误导致无法读取,错误资源为:%@", asset);
}

d、读取Buffer数据
- (CMSampleBufferRef)readBuffer
{
    [lock lock];// 锁定
    ...
    [lock unlock];// 取消锁
    return sampleBufferRef;// 返回读取到的sampleBufferRef数据
}
❶ 读取缓存区的内容
CMSampleBufferRef sampleBufferRef = nil;
if (readerVideoTrackOutput)// 判断视频输出轨道是否创建成功
{
    // 复制下一个缓存区的内容到sampleBufferRef
    sampleBufferRef = [readerVideoTrackOutput copyNextSampleBuffer];
}
❷ 内容读取完成后的操作

因为是逐帧读取,所以需要重复清空再读取直到视频内容全部读取完成。

// 判断资源读取器是否存在并且处于已经完成读取的状态
if (assetReader && assetReader.status == AVAssetReaderStatusCompleted)
{
    // 清空视频输出轨道
    readerVideoTrackOutput = nil;
    // 清空资源读取器
    assetReader = nil;
    // 重新初始化二者
    [self setUpAsset];
}

3、RendererCamera 视图控制器

a、准备工作
导入框架
#import 
#import 
私有属性
@interface RendererCameraViewController ()

@property (nonatomic, strong) MTKView *mtkView;
// 用来读取mov或者mp4文件中的视频数据
@property (nonatomic, strong) AssetReader *reader;
// 由CoreVideo框架提供的高速纹理读取缓存区,用来迅速读取纹理到GPU
@property (nonatomic, assign) CVMetalTextureCacheRef textureCache;
@property (nonatomic, assign) vector_uint2 viewportSize;// 视口大小
@property (nonatomic, strong) id pipelineState;// 渲染管道
@property (nonatomic, strong) id commandQueue;// 命令队列
@property (nonatomic, strong) id texture;// 纹理
@property (nonatomic, strong) id vertices;// 顶点缓存区
@property (nonatomic, strong) id convertMatrix;// YUV->RGB 转换矩阵
@property (nonatomic, assign) NSUInteger numVertices;// 顶点个数

@end
整体流程
- (void)viewDidLoad
{
    [super viewDidLoad];

    // 1.设置MTKView
    [self setupMTKView];
    // 2.设置AssetReader
    [self setupAsset];
    // 3.设置渲染管道
    [self setupPipeline];
    // 4.设置顶点数据
    [self setupVertex];
    // 5.设置转换矩阵
    [self setupMatrix];
}

b、设置MTKView
- (void)setupMTKView
{
    // 初始化mtkView
    self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
    // 获取默认的device
    self.mtkView.device = MTLCreateSystemDefaultDevice();
    // 设置代理
    self.mtkView.delegate = self;
    // 获取视口大小
    self.viewportSize = (vector_uint2){self.mtkView.drawableSize.width, self.mtkView.drawableSize.height};
    
    self.view = self.mtkView;
}

c、设置AssetReader
- (void)setupAsset
{
    // 视频文件路径
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"kun2" withExtension:@"mp4"];
    
    // 初始化AssetReader
    self.reader = [[AssetReader alloc] initWithUrl:url];
    
    // 通过CoreVideo提供给CPU/GPU高速缓存通道读取纹理数据
    CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
}

d、设置渲染管道
- (void)setupPipeline
{
}
❶ 获取metal文件
  • newDefaultLibrary:一个metal文件时推荐使用
  • newLibraryWithFile:Library读取指定metal文件
  • newLibraryWithData:Data中获取metal文件
id defaultLibrary = [self.mtkView.device newDefaultLibrary];
// 顶点shader,vertexShader是函数名
id vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
// 片元shader,samplingShader是函数名
id fragmentFunction = [defaultLibrary newFunctionWithName:@"samplingShader"];
❷ 渲染管道描述信息类
MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
// 设置vertexFunction
pipelineStateDescriptor.vertexFunction = vertexFunction;
// 设置fragmentFunction
pipelineStateDescriptor.fragmentFunction = fragmentFunction;
// 设置颜色格式
pipelineStateDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
❸ 根据渲染管道描述信息初始化渲染管道
self.pipelineState = [self.mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:NULL];
❹ 初始化渲染指令队列,保证渲染指令有序地提交到GPU
self.commandQueue = [self.mtkView.device newCommandQueue];

e、设置顶点
- (void)setupVertex
{
}
❶ 创建顶点坐标(x,y,z,w) 纹理坐标(x,y)
  • 为了让视频全屏铺满,所有顶点大小均设置[-1,1]
  • 图像由上三角和下三角组成
static const Vertex quadVertices[] =
{   // 顶点坐标分别是x、y、z、w  纹理坐标为x、y
    { {  1.0, -1.0, 0.0, 1.0 },  { 1.f, 1.f } },
    { { -1.0, -1.0, 0.0, 1.0 },  { 0.f, 1.f } },
    { { -1.0,  1.0, 0.0, 1.0 },  { 0.f, 0.f } },
    
    { {  1.0, -1.0, 0.0, 1.0 },  { 1.f, 1.f } },
    { { -1.0,  1.0, 0.0, 1.0 },  { 0.f, 0.f } },
    { {  1.0,  1.0, 0.0, 1.0 },  { 1.f, 0.f } },
};
❷ 创建顶点缓存区
self.vertices = [self.mtkView.device newBufferWithBytes:quadVertices
                                                 length:sizeof(quadVertices)
                                                options:MTLResourceStorageModeShared];
❸ 计算顶点个数
self.numVertices = sizeof(quadVertices) / sizeof(Vertex);

f、设置YUV->RGB转换的矩阵
- (void)setupMatrix
{
}
❶ 转化矩阵(共有三种,区别在于清晰度)
// 不要求掌握
matrix_float3x3 kColorConversion601FullRangeMatrix = (matrix_float3x3)
{
    (simd_float3){1.0,    1.0,    1.0},
    (simd_float3){0.0,    -0.343, 1.765},
    (simd_float3){1.4,    -0.711, 0.0},
};
❷ 设置偏移量
// 不要求掌握
vector_float3 kColorConversion601FullRangeOffset = (vector_float3){ -(16.0/255.0), -0.5, -0.5};
❸ 创建转化矩阵结构体
ConvertMatrix matrix;
matrix.matrix = kColorConversion601FullRangeMatrix;// 设置转化矩阵
matrix.offset = kColorConversion601FullRangeOffset;// 设置offset偏移量
❹ 创建转换矩阵缓存区
self.convertMatrix = [self.mtkView.device newBufferWithBytes:&matrix
                                                    length:sizeof(ConvertMatrix)
                                            options:MTLResourceStorageModeShared];

g、设置纹理
- (void)setupTextureWithEncoder:(id)encoder buffer:(CMSampleBufferRef)sampleBuffer
{    
}
❶ 从CMSampleBuffer读取CVPixelBuffer
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
❷ 创建临时纹理。由于这个方法会多次调用,所以在每次创建新纹理之前需要清空之前的旧纹理
id textureY = nil;
id textureUV = nil;
❸ 获取Y纹理的宽高
size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
❹ 设置像素格式为普通格式,即一个8位规范化的无符号整数组件
MTLPixelFormat pixelFormat = MTLPixelFormatR8Unorm;
❺ 创建CoreVideo的Metal纹理
CVMetalTextureRef texture = NULL;
❻ 根据视频像素缓存区创建Metal纹理缓存区
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, self.textureCache, pixelBuffer, NULL, pixelFormat, width, height, 0, &texture);

// 判断Metal纹理缓存区是否创建成功
if(status == kCVReturnSuccess)
{
}
❼ 转成Metal用的纹理
textureY = CVMetalTextureGetTexture(texture);

// 使用完毕释放
CFRelease(texture);
❽ 同理设置纹理UV
size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
MTLPixelFormat pixelFormat = MTLPixelFormatRG8Unorm;
CVMetalTextureRef texture = NULL;
CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, self.textureCache, pixelBuffer, NULL, pixelFormat, width, height, 1, &texture);
if(status == kCVReturnSuccess)
{
    textureUV = CVMetalTextureGetTexture(texture);
    CFRelease(texture);
}
❾ 向片元函数设置textureY / textureUV纹理

不能在客户端中完成像素点的颜色YUG到RGB的转化,因为视频有无数帧图片,每张图片有无数个像素点,造成计算量非常庞大,CPU不足以支撑起如此庞大的计算量,所以需要将Y / UV纹理传递到片源函数中去。

if(textureY != nil && textureUV != nil)
{
    // 向片元函数设置textureY纹理
    [encoder setFragmentTexture:textureY atIndex:FragmentTextureIndexTextureY];
    // 向片元函数设置textureUV纹理
    [encoder setFragmentTexture:textureUV atIndex:FragmentTextureIndexTextureUV];
}
❿ 使用完毕则将sampleBuffer释放
CFRelease(sampleBuffer);

h、绘制视图
- (void)drawInMTKView:(MTKView *)view
{
}
❶ 每次渲染都要单独创建一个命令缓冲区
id commandBuffer = [self.commandQueue commandBuffer];
❷ 获取渲染描述信息
MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
❸ 从AssetReader中读取图像数据
CMSampleBufferRef sampleBuffer = [self.reader readBuffer];
❹ 判断renderPassDescriptor和sampleBuffer是否已经获取到了
if(renderPassDescriptor && sampleBuffer)
{
}
❺ 设置渲染描述信息中的颜色附着(默认背景色)
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.5, 0.5, 1.0f);
❻ 根据渲染描述信息创建渲染命令编码器
id renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
❼ 设置视口大小(显示区域)
[renderEncoder setViewport:(MTLViewport){0.0, 0.0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0 }];
❽ 为渲染编码器设置渲染管道
[renderEncoder setRenderPipelineState:self.pipelineState];
❾ 设置顶点缓存区
[renderEncoder setVertexBuffer:self.vertices
                        offset:0
                       atIndex:VertexInputIndexVertices];
❿ 设置纹理(将sampleBuffer数据设置到renderEncoder中)
[self setupTextureWithEncoder:renderEncoder buffer:sampleBuffer];
设置片元函数转化矩阵
[renderEncoder setFragmentBuffer:self.convertMatrix
                          offset:0
                         atIndex:FragmentInputIndexMatrix];
开始绘制
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
                  vertexStart:0
                  vertexCount:self.numVertices];
显示并结束编码
[commandBuffer presentDrawable:view.currentDrawable];
[renderEncoder endEncoding];
提交命令
[commandBuffer commit];

4、ShaderTypes 桥接文件

顶点数据结构
typedef struct
{
    vector_float4 position;// 顶点坐标(x,y,z,w)
    vector_float2 textureCoordinate;// 纹理坐标(s,t)
} Vertex;
转换矩阵
typedef struct
{
    matrix_float3x3 matrix;// 三维矩阵
    vector_float3 offset;// 偏移量
} ConvertMatrix;
顶点函数输入索引
typedef enum VertexInputIndex
{
    VertexInputIndexVertices     = 0,
} VertexInputIndex;
片元函数缓存区索引
typedef enum FragmentBufferIndex
{
    FragmentInputIndexMatrix     = 0,
} FragmentBufferIndex;
片元函数纹理索引
typedef enum FragmentTextureIndex
{
    FragmentTextureIndexTextureY     = 0,// Y纹理
    FragmentTextureIndexTextureUV     = 1,// UV纹理
} FragmentTextureIndex;

5、Shaders.metal 顶点片源函数文件

结构体(用于顶点函数输出/片元函数输入)
typedef struct
{
    float4 clipSpacePosition [[position]];// position修饰符表示这个是顶点
    float2 textureCoordinate;// 纹理坐标
} RasterizerData;
顶点函数
  • RasterizerData:返回数据类型
  • vertex_id:是顶点shader每次处理的index,用于定位当前的顶点
  • buffer:表明是缓存数据
  • VertexInputIndexVertices:是索引0
vertex RasterizerData
vertexShader(uint vertexID [[ vertex_id ]],
             constant Vertex *vertexArray [[ buffer(VertexInputIndexVertices) ]])
{
    RasterizerData out;
    out.clipSpacePosition = vertexArray[vertexID].position;// 顶点坐标
    out.textureCoordinate = vertexArray[vertexID].textureCoordinate;// 纹理坐标
    return out;
}
片源函数
  • float4:返回每个像素点的颜色值
  • stage_in:表示这个数据来自光栅化(光栅化是顶点处理之后的步骤,无法修改)
  • textureY:表明是纹理数据,FragmentTextureIndexTextureY是索引
  • textureUV:表明是纹理数据,FragmentTextureIndexTextureUV是索引
  • buffer:表明是缓存数据, FragmentInputIndexMatrix是索引
  • textureY的纹理采集器读取纹理坐标对应的R值(Y)
  • textureUV的纹理采集器读取纹理坐标对应的RG值(UV)
fragment float4
samplingShader(RasterizerData input [[stage_in]],
               texture2d textureY [[ texture(FragmentTextureIndexTextureY) ]],
               texture2d textureUV [[ texture(FragmentTextureIndexTextureUV) ]],
               constant ConvertMatrix *convertMatrix [[ buffer(FragmentInputIndexMatrix) ]])
{
    // 获取纹理采样器
    constexpr sampler textureSampler (mag_filter::linear,
                                      min_filter::linear);
    // 读取YUV颜色值
    float3 yuv = float3(textureY.sample(textureSampler, input.textureCoordinate).r,
                        textureUV.sample(textureSampler, input.textureCoordinate).rg);
    
    // 将YUV颜色值转化为RGB颜色值
    float3 rgb = convertMatrix->matrix * (yuv + convertMatrix->offset);
    
    // 返回RGBA颜色值
    return float4(rgb, 1.0);
}

Demo

Demo在我的Github上,欢迎下载。
Multi-MediaDemo

参考文献

  • 一文读懂 YUV 的采样与格式

你可能感兴趣的:(iOS图像:Metal 实现视频采集和渲染)