Metal 渲染视频

在阅读该文章前请先阅读 Metal 渲染摄像机内容RGB和YUV颜色编码

先来介绍一下该案例需要实现的功能:
利用AVAssetReader加载视频内容,并且一帧一帧的渲染到MTKView上。

下面我们来看下核心代码。

一、.metal 文件的代码:

#include <metal_stdlib>
using namespace metal;

#import "CQVideo.h"

typedef struct {
    float4 clipSpacePosition [[position]]; // position的修饰符表示这个是顶点
    float2 textureCoordinate; // 纹理坐标
} RasterizerData;

vertex RasterizerData vertexShaderVideo(uint vertexIndex [[vertex_id]],
                                        constant CQVideoVertex *vertexArray [[buffer(CQVVertexInputIndexVertices)]]) {
    RasterizerData out;
    out.clipSpacePosition = vertexArray[vertexIndex].position;
    out.textureCoordinate = vertexArray[vertexIndex].textureCoordinate;
    return out;
}

fragment float4 fragmentShaderVideo(RasterizerData in [[stage_in]],
                                    texture2d<float> textureY [[texture(CQVFragmentTextureIndexTextureY)]],
                                    texture2d<float> textureUV [[texture(CQVFragmentTextureIndexTextureUV)]],
                                    constant CQConvertMatrix *convertMatrix [[buffer(CQVFragmentBufferIndexMatrix)]]) {
    
    constexpr sampler textureSampler (filter::linear);
    float y = textureY.sample(textureSampler, in.textureCoordinate).r;
    float2 uv = textureUV.sample(textureSampler, in.textureCoordinate).rg;
    float3 yuv = float3(y, uv);
    //将YUV 转化为 RGB值
    float3 rgb = convertMatrix->matrix * (yuv + convertMatrix->offset);
    return float4(rgb, 1.0);
}

这里是 .metal 文件中的所有代码。来简单介绍下片元函数的相关代码:

  • textureSampler:纹理采样器。
  • textureY:我们从OC代码传过来的是一个只包含一个(R)8位规范化的无符号整数组件(MTLPixelFormatR8Unorm)的纹理。所以我们只需要取分量r上的值,就是分量Y对应的纹理的颜色值。
  • textureUV:我们从OC代码传过来的是一个包含两个(RG)8位规范化的无符号整数组件(MTLPixelFormatRG8Unorm)的纹理。所以我们需要取分量rg上的值,就是分量UV对应的纹理的颜色值。
  • convertMatrix:将 YUV 转化为 RGB 值的 转换矩阵

这里有些数据类型是我们自定义在CQVideo.h文件中的,方便metalOC共用。看代码:

#ifndef CQVideo_h
#define CQVideo_h
#include <simd/simd.h>

typedef struct {
    vector_float4 position;
    vector_float2 textureCoordinate;
} CQVideoVertex;

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

typedef enum CQVVertexInputIndex {
    CQVVertexInputIndexVertices = 0,
} CQVVertexInputIndex;//顶点索引

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

typedef enum CCFragmentTextureIndex {
    CQVFragmentTextureIndexTextureY     = 0,//Y纹理
    CQVFragmentTextureIndexTextureUV    = 1,//UV纹理
} CQVFragmentTextureIndex;//片元函数纹理索引

#endif /* CQVideo_h */

二、CQMetalVideoVC.m中的代码

先来了解下整体流程,有个大概的思路:

  • 1、setupMTKView:设置MTKView
  • 2、setupAsset:设置视频资源。
  • 3、setupPineline:设置管线。
  • 4、setupVertex:设置顶点数据。
  • 5、setupMatrix:设置转换矩阵。

这五步是笔者划分的步骤,其实就是绘制的准备工作。
还有最后一步:

  • 6、绘制。这一步肯定是在MTKView代理方法drawInMTKView:中进行了。

下面看下每一步的具体操作。

2.1 setupMTKView
    self.mtkView = [[MTKView alloc] initWithFrame:self.view.bounds];
    self.mtkView.device = MTLCreateSystemDefaultDevice();
    if (!self.mtkView.device) {
        NSLog(@"Metal is not supported on this device");
        return;
    }
    self.view = self.mtkView;
    self.mtkView.delegate = self;
    self.viewportSize = (vector_uint2){self.mtkView.drawableSize.width, self.mtkView.drawableSize.height};

这一步就不再赘述了,很简单的操作。

2.2 setupAsset 设置视频资源
NSURL *url = [[NSBundle mainBundle] URLForResource:@"recoder" withExtension:@"mp4"];
self.reader = [[CQAssetReader alloc] initWithUrl:url];
    
CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
  • CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);_textureCache(高速纹理读取缓存区CVMetalTextureCacheRef)的创建。通过CoreVideo提供给CPU/GPU高速缓存通道读取纹理数据。

这里用到了自定义的类CQAssetReader。我们来具体看下:

#pragma mark - readBuffer
- (CMSampleBufferRef)readBuffer {
    [lock lock];
    CMSampleBufferRef sampleBufferRef = nil;
    if (assetReaderTrackOutput) {
        //复制下一个缓存区的内容到sampleBufferRef
        sampleBufferRef = [assetReaderTrackOutput copyNextSampleBuffer];
    }

    if (assetReader && assetReader.status == AVAssetReaderStatusCompleted) {
        assetReaderTrackOutput = nil;
        assetReader = nil;
        [self setupAsset];
    }
    [lock unlock];
    return sampleBufferRef;
}
- (void)setupAsset {
    //默认为NO,YES表示提供精确的时长
    NSDictionary *options = @{AVURLAssetPreferPreciseDurationAndTimingKey:@(YES)};
    AVURLAsset *inputAsset = [[AVURLAsset alloc] initWithURL:videoUrl options:options];
    
    __weak typeof(self) weakSelf = self;
    NSString *key = @"tracks";
    //管理目标 加载尚未加载的任何指定键的值。
    //对资源所需的键执行标准的异步载入操作,这样就可以访问资源的tracks属性时,就不会受到阻碍.
    [inputAsset loadValuesAsynchronouslyForKeys:@[key] completionHandler:^{
        __strong typeof(self) strongSelf = self;
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSError *error;
            AVKeyValueStatus status = [inputAsset statusOfValueForKey:key error:&error];
            if (status != AVKeyValueStatusLoaded) {
                NSLog(@"Asset error: %@", error);
                return;
            }
            [weakSelf processAsset:inputAsset];
        });
    }];
}

- (void)processAsset:(AVAsset *)asset {
    [lock lock];
    NSError *error;
    assetReader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
    if (error != nil) {
        NSLog(@"Reader error: %@", error);
    }
    //kCVPixelBufferPixelFormatTypeKey 像素格式.
    //kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange : 420v
    //kCVPixelFormatType_32BGRA : iOS在内部进行YUV至BGRA格式转换
    NSDictionary *options = @{(id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)};
    AVAssetTrack *assetTrack = [asset tracksWithMediaType:AVMediaTypeVideo].firstObject;
    assetReaderTrackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:assetTrack outputSettings:options];
    //表示缓存区的数据输出之前是否会被复制.
    //YES:输出总是从缓存区提供复制的数据,你可以自由的修改这些缓存区数据
    assetReaderTrackOutput.alwaysCopiesSampleData = NO;
    
    [assetReader addOutput:assetReaderTrackOutput];
    
    BOOL start = [assetReader startReading];
    if (start == NO) {
        NSLog(@"Error reading from file at URL: %@", asset);
    }
    [lock unlock];
}

对这段代码笔者画了张图,大家可以参考理解一下:

2.3 setupPineline 设置管线
    id<MTLLibrary> defaultLibrary = [self.mtkView.device newDefaultLibrary];
    id<MTLFunction> vertexFunc = [defaultLibrary newFunctionWithName:@"vertexShaderVideo"];
    id<MTLFunction> fragmentFunc = [defaultLibrary newFunctionWithName:@"fragmentShaderVideo"];
    
    MTLRenderPipelineDescriptor *renderPipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
    renderPipelineDescriptor.vertexFunction = vertexFunc;
    renderPipelineDescriptor.fragmentFunction = fragmentFunc;
    renderPipelineDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
    
    self.renderPipelineState = [self.mtkView.device newRenderPipelineStateWithDescriptor:renderPipelineDescriptor error:NULL];
    self.commandQueue = [self.mtkView.device newCommandQueue];
2.4 setupVertex 设置顶点数据
    //注意: 为了让视频全屏铺满,所以顶点大小均设置[-1,1]
    static const CQVideoVertex 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.verticesNum = sizeof(quadVertices) / sizeof(CQVideoVertex);
2.5 setupMatrix设置转换矩阵
    //1.转化矩阵
     // BT.601, which is the standard for SDTV.
     matrix_float3x3 kColorConversion601DefaultMatrix = (matrix_float3x3){
         (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
     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},
     };
    
     // BT.709, which is the standard for HDTV.
     matrix_float3x3 kColorConversion709DefaultMatrix[] = {
         (simd_float3){1.164,  1.164, 1.164},
         (simd_float3){0.0, -0.213, 2.112},
         (simd_float3){1.793, -0.533,   0.0},
     };
     
     //2.偏移量
     vector_float3 kColorConversion601FullRangeOffset = (vector_float3){ -(16.0/255.0), -0.5, -0.5};
     
     //3.创建转化矩阵结构体.
     CQConvertMatrix matrix;
     //设置转化矩阵
     matrix.matrix = kColorConversion601FullRangeMatrix;
     matrix.offset = kColorConversion601FullRangeOffset;
     
     //创建转换矩阵缓存区.
     self.convertMatrix = [self.mtkView.device newBufferWithBytes:&matrix
                                                         length:sizeof(CQConvertMatrix)
                                                 options:MTLResourceStorageModeShared];

所有的准备工作已结束,下面我们开始绘制。

2.6 绘制
- (void)drawInMTKView:(nonnull MTKView *)view {
    id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
    MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
    CMSampleBufferRef sampleBufferRef = [self.reader readBuffer];
    if (renderPassDescriptor && sampleBufferRef) {
        //设置MTLRenderPassDescriptor中颜色附着(默认背景色)
        renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.5, 0.5, 1.0);
        id<MTLRenderCommandEncoder> commandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
        MTLViewport viewport = {0.0, 0.0, _viewportSize.x, _viewportSize.y, -1.0, 1.0};
        [commandEncoder setViewport:viewport];
        [commandEncoder setRenderPipelineState:self.renderPipelineState];
        [commandEncoder setVertexBuffer:self.vertices offset:0 atIndex:CQVVertexInputIndexVertices];
        [self setupTextureWithEncoder:commandEncoder buffer:sampleBufferRef];
        
        [commandEncoder setFragmentBuffer:self.convertMatrix offset:0 atIndex:CQVFragmentBufferIndexMatrix];
        [commandEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:self.verticesNum];
        [commandEncoder endEncoding];
        [commandBuffer presentDrawable:view.currentDrawable];
    }
    [commandBuffer commit];
}
  • CMSampleBufferRef sampleBufferRef = [self.reader readBuffer];
    这一步我们每次读取视频的下一个CMSampleBufferRef对象数据。
  • 然后我们设置了视口、渲染管线状态、顶点数据、纹理数据、转换矩阵。
    这一系列设置后绘制、结束编码、呈现、提交命令。

我们来看下纹理的设置,如何设置Y纹理以及UV纹理:

- (void)setupTextureWithEncoder:(id<MTLRenderCommandEncoder>)encoder buffer:(CMSampleBufferRef)sampleBuffer {
    //从CMSampleBuffer读取CVPixelBuffer,
    CVPixelBufferRef pixelBufferRef = CMSampleBufferGetImageBuffer(sampleBuffer);
    id<MTLTexture> textureY = nil;
    id<MTLTexture> textureUV = nil;
    
    {//textureY 设置
        size_t width = CVPixelBufferGetWidthOfPlane(pixelBufferRef, 0);
        size_t height = CVPixelBufferGetHeightOfPlane(pixelBufferRef, 0);
        //像素格式:普通格式,包含一个8位规范化的无符号整数组件。
        MTLPixelFormat pixelFormat = MTLPixelFormatR8Unorm;
        
        //创建CoreVideo的Metal纹理
        CVMetalTextureRef texture = NULL;
        CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, self.textureCache, pixelBufferRef, NULL, pixelFormat, width, height, 0, &texture);
        if (status == kCVReturnSuccess) {
            textureY = CVMetalTextureGetTexture(texture);
            CFRelease(texture);
        }
    }
    
    {//textureUV 设置
        size_t width = CVPixelBufferGetWidthOfPlane(pixelBufferRef, 1);
        size_t height = CVPixelBufferGetHeightOfPlane(pixelBufferRef, 1);
        MTLPixelFormat pixelFormat = MTLPixelFormatRG8Unorm;
        CVMetalTextureRef texture = NULL;
        CVReturn status = CVMetalTextureCacheCreateTextureFromImage(NULL, self.textureCache, pixelBufferRef, NULL, pixelFormat, width, height, 1, &texture);
        if (status == kCVReturnSuccess) {
            textureUV = CVMetalTextureGetTexture(texture);
            CFRelease(texture);
        }
    }
    
    if(textureY != nil && textureUV != nil) {
        [encoder setFragmentTexture:textureY atIndex:CQVFragmentTextureIndexTextureY];
        [encoder setFragmentTexture:textureUV atIndex:CQVFragmentTextureIndexTextureUV];
    }
    CFRelease(sampleBuffer);
}
  • 利用CMSampleBufferRef获取像素缓存区对象CVPixelBufferRef
  • 获取像素缓存区中平面位置索引处平面的 widthheight
  • 指定像素格式纹理YMTLPixelFormatR8Unorm、 纹理UVMTLPixelFormatRG8Unorm
  • 创建CoreVideoMetal纹理。
  • 生成MTLTexture类型纹理。

该段代码使用到了CoreVideo以及CoreMedia的相关代码,看下流程图:

CoreVideo里使用到的两个函数
CVMetalTextureCacheCreate()
CVMetalTextureCacheCreateTextureFromImage()
具体参数解释可以在 Metal 渲染摄像机内容 文章中看下。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342