iOS AVDemo(13):視訊渲染,用 Metal 渲染丨音視訊工程示例

語言: CN / TW / HK

莫奈《貝勒島風景》

這個公眾號會 路線圖 式的遍歷分享音視訊技術 音視訊基礎(完成)  →  音視訊工具(完成)  →  音視訊工程示例(進行中)  →  音視訊工業實戰(準備) 關注一下成本不高,錯過乾貨損失不小 ↓↓↓

iOS/Android 客戶端開發同學如果想要開始學習音視訊開發,最絲滑的方式是對音視訊基礎概念知識有一定了解後,再借助 iOS/Android 平臺的音視訊能力上手去實踐音視訊的 採集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染 過程,並藉助音視訊工具來分析和理解對應的音視訊資料。

在音視訊工程示例這個欄目,我們將通過拆解 採集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染 流程並實現 Demo 來向大家介紹如何在 iOS/Android 平臺上手音視訊開發。

這裡是第十三篇: iOS 視訊渲染 Demo 。這個 Demo 裡包含以下內容:

  • 1)實現一個視訊採集裝模組;

  • 2)實現一個視訊渲染模組;

  • 3)串聯視訊採集和渲染模組,將採集的視訊資料輸入給渲染模組進行渲染;

  • 4)詳盡的程式碼註釋,幫你理解程式碼邏輯和原理。

在本文中,我們將詳解一下 Demo 的具體實現和原始碼。讀完本文內容相信就能幫你掌握相關知識。

不過,如果你的需求是:1)直接獲得全部工程原始碼;2)想進一步諮詢音視訊技術問題;3)諮詢音視訊職業發展問題。可以根據自己的需要考慮是否加入『關鍵幀的音視訊開發圈』,這是一個收費的社群服務,目前還有少量優惠券可用。

1、視訊採集模組

在這個 Demo 中,視訊採集模組 KFVideoCapture 的實現與《iOS 視訊採集 Demo》 中一樣,這裡就不再重複介紹了,其介面如下:

KFVideoCapture.h

#import <Foundation/Foundation.h>
#import "KFVideoCaptureConfig.h"

NS_ASSUME_NONNULL_BEGIN

@interface KFVideoCapture : NSObject
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(KFVideoCaptureConfig *)config;

@property (nonatomic, strong, readonly) KFVideoCaptureConfig *config;
@property (nonatomic, strong, readonly) AVCaptureVideoPreviewLayer *previewLayer; // 視訊預覽渲染 layer。
@property (nonatomic, copy) void (^sampleBufferOutputCallBack)(CMSampleBufferRef sample); // 視訊採集資料回撥。
@property (nonatomic, copy) void (^sessionErrorCallBack)(NSError *error); // 視訊採集會話錯誤回撥。
@property (nonatomic, copy) void (^sessionInitSuccessCallBack)(void); // 視訊採集會話初始化成功回撥。

- (void)startRunning; // 開始採集。
- (void)stopRunning; // 停止採集。
- (void)changeDevicePosition:(AVCaptureDevicePosition)position; // 切換攝像頭。
@end

NS_ASSUME_NONNULL_END

2、視訊渲染模組

在之前的《iOS 視訊採集 Demo》那篇中,我們採集後的視訊資料是通過系統封裝好的 AVCaptureVideoPreviewLayer 來做預覽渲染的。這篇我們來介紹一下使用 MetalKit 來實現渲染。

首先,我們在 KFShaderType.h 中定義一些渲染過程需要用到的資料結構。

KFShaderType.h

#ifndef KFShaderType_h
#define KFShaderType_h

#include <simd/simd.h>

// 儲存資料的自定義結構,用於橋接 OC 和 Metal 程式碼(頂點)。
typedef struct {
    // 頂點座標,4 維向量。
    vector_float4 position;
    // 紋理座標。
    vector_float2 textureCoordinate;
} KFVertex;

// 儲存資料的自定義結構,用於橋接 OC 和 Metal 程式碼(頂點)。
typedef struct {
    // YUV 矩陣。
    matrix_float3x3 matrix;
    // 是否為 full range。
    bool fullRange;
} KFConvertMatrix;

// 自定義列舉,用於橋接 OC 和 Metal 程式碼(頂點)。
// 頂點的橋接列舉值 KFVertexInputIndexVertices。
typedef enum KFVertexInputIndex {
    KFVertexInputIndexVertices = 0,
} KFVertexInputIndex;

// 自定義列舉,用於橋接 OC 和 Metal 程式碼(片元)。
// YUV 矩陣的橋接列舉值 KFFragmentInputIndexMatrix。
typedef enum KFFragmentBufferIndex {
    KFFragmentInputIndexMatrix = 0,
} KFMetalFragmentBufferIndex;

// 自定義列舉,用於橋接 OC 和 Metal 程式碼(片元)。
// YUV 資料的橋接列舉值 KFFragmentTextureIndexTextureY、KFFragmentTextureIndexTextureUV。
typedef enum KFFragmentYUVTextureIndex {
    KFFragmentTextureIndexTextureY = 0,
    KFFragmentTextureIndexTextureUV = 1,
} KFFragmentYUVTextureIndex;

// 自定義列舉,用於橋接 OC 和 Metal 程式碼(片元)。
// RGBA 資料的橋接列舉值 KFFragmentTextureIndexTextureRGB。
typedef enum KFFragmentRGBTextureIndex {
    KFFragmentTextureIndexTextureRGB = 0,
} KFFragmentRGBTextureIndex;

#endif /* KFMetalShaderType_h */

然後,我們在 render.metal 中寫 Metal 渲染程式碼。它類似 OpenGL 的 shader。

render.metal

#include <metal_stdlib>
#include "KFShaderType.h"

using namespace metal;

// 定義了一個型別為 RasterizerData 的結構體,裡面有一個 float4 向量和 float2 向量。
typedef struct {
    // float4:4 維向量;
    // clipSpacePosition:引數名,表示頂點;
    // [[position]]:position 是頂點修飾符,這是蘋果內建的語法,不能改變,表示頂點資訊。
    float4 clipSpacePosition [[position]];
    // float2:2 維向量;
    // textureCoordinate:引數名,這裡表示紋理。
    float2 textureCoordinate;
} RasterizerData;

// 頂點函式通過一個自定義的結構體,返回對應的資料;頂點函式的輸入引數也可以是自定義結構體。

// 頂點函式
// vertex:函式修飾符,表示頂點函式;
// RasterizerData:返回值型別;
// vertexShader:函式名;
// [[vertex_id]]:vertex_id 是頂點 id 修飾符,蘋果內建的語法不可改變;
// [[buffer(YYImageVertexInputIndexVertexs)]]:buffer 是快取資料修飾符,蘋果內建的語法不可改變,YYImageVertexInputIndexVertexs 是索引;
// constant:是變數型別修飾符,表示儲存在 device 區域。
vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],
                                   constant KFVertex *vertexArray [[buffer(KFVertexInputIndexVertices)]]) {
    RasterizerData out;
    out.clipSpacePosition = vertexArray[vertexID].position;
    out.textureCoordinate = vertexArray[vertexID].textureCoordinate;
    
    return out;
}

// 片元函式
// fragment:函式修飾符,表示片元函式;
// float4:返回值型別,返回 RGBA;
// fragmentImageShader:函式名;
// RasterizerData:引數型別;
// input:變數名;
// [[stage_in]:stage_in 表示這個資料來自光柵化,光柵化是頂點處理之後的步驟,業務層無法修改。
// texture2d:型別表示紋理;
// textureY:表示 Y 通道;
// textureUV:表示 UV 通道;
// [[texture(index)]]:紋理修飾符;可以加索引:[[texture(0)]] 對應紋理 0,[[texture(1)]] 對應紋理 1;
// KFFragmentTextureIndexTextureY、KFFragmentTextureIndexTextureUV:表示紋理索引。
fragment float4 yuvSamplingShader(RasterizerData input [[stage_in]],
                                  texture2d<float> textureY [[texture(KFFragmentTextureIndexTextureY)]],
                                  texture2d<float> textureUV [[texture(KFFragmentTextureIndexTextureUV)]],
                                  constant KFConvertMatrix *convertMatrix [[buffer(KFFragmentInputIndexMatrix)]]) {
    constexpr sampler textureSampler (mag_filter::linear, min_filter::linear);
    float3 yuv = float3(textureY.sample(textureSampler, input.textureCoordinate).r, textureUV.sample(textureSampler, input.textureCoordinate).rg);
    
    if (convertMatrix->fullRange) { // full range.
        yuv.x = textureY.sample(textureSampler, input.textureCoordinate).r;
    } else { // video range.
        yuv.x = textureY.sample(textureSampler, input.textureCoordinate).r - (16.0 / 255.0);
    }
    yuv.yz = textureUV.sample(textureSampler, input.textureCoordinate).rg - 0.5;
    
    float3 rgb = convertMatrix->matrix * yuv;
  
    return float4(rgb,1.0);
}

// 片元函式
// fragment:函式修飾符,表示片元函式;
// float4:返回值型別,返回 RGBA;
// fragmentImageShader:函式名;
// RasterizerData:引數型別;
// input:變數名;
// [[stage_in]:stage_in 表示這個資料來自光柵化,光柵化是頂點處理之後的步驟,業務層無法修改。
// texture2d:型別表示紋理;
// colorTexture:代表 RGBA 資料;
// [[texture(index)]]:紋理修飾符;可以加索引:[[texture(0)]] 對應紋理 0,[[texture(1)]] 對應紋理 1;
// KFFragmentTextureIndexTextureRGB:表示紋理索引。
fragment float4 rgbSamplingShader(RasterizerData input [[stage_in]],
                                  texture2d<half> colorTexture [[texture(KFFragmentTextureIndexTextureRGB)]]) {
    constexpr sampler textureSampler (mag_filter::linear, min_filter::linear);
    
    half4 colorSample = colorTexture.sample(textureSampler, input.textureCoordinate);
  
    return float4(colorSample);
}

接下來,就是封裝渲染 Metal 渲染檢視 KFMetalView 了,它接受 CVPixelBufferRef 作為引數來進行渲染。

KFMetalView.h

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

// 渲染畫面填充模式。
typedef NS_ENUM(NSInteger, KFMetalViewContentMode) {
    // 自動填充滿,可能會變形。
    KFMetalViewContentModeStretch = 0,
    // 按比例適配,可能會有黑邊。
    KFMetalViewContentModeFit = 1,
    // 根據比例裁剪後填充滿。
    KFMetalViewContentModeFill = 2
};

@interface KFMetalView : UIView
@property (nonatomic, assign) KFMetalViewContentMode fillMode; // 畫面填充模式。
- (void)renderPixelBuffer:(CVPixelBufferRef)pixelBuffer; // 渲染。
@end

NS_ASSUME_NONNULL_END

KFMetalView.m

#import "KFMetalView.h"
#import <MetalKit/MetalKit.h>
#import <AVFoundation/AVFoundation.h>
#import <MetalPerformanceShaders/MetalPerformanceShaders.h>
#import "KFShaderType.h"

// 顏色空間轉換矩陣,BT.601 Video Range。
static const matrix_float3x3 kFColorMatrix601VideoRange = (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。
static const matrix_float3x3 kFColorMatrix601FullRange = (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 Video Range。
static const matrix_float3x3 kFColorMatrix709VideoRange = (matrix_float3x3) {
    (simd_float3) {1.164,  1.164, 1.164},
    (simd_float3) {0.0,   -0.213, 2.112},
    (simd_float3) {1.793, -0.533,   0.0},
};

// 顏色空間轉換矩陣,BT.709 Full Range。
static const matrix_float3x3 kFColorMatrix709FullRange = (matrix_float3x3) {
    (simd_float3) { 1.0,    1.0,    1.0},
    (simd_float3) {0.0,    -0.187, 1.856},
    (simd_float3) {1.575,    -0.468, 0.0},
};

@interface KFMetalView () <MTKViewDelegate>
@property (nonatomic, assign) CVPixelBufferRef pixelBuffer; // 外層輸入的最後一幀資料。
@property (nonatomic, strong) dispatch_semaphore_t semaphore; // 處理 PixelBuffer 鎖,防止外層輸入執行緒與渲染執行緒同時操作 Crash。
@property (nonatomic, assign) CVMetalTextureCacheRef textureCache; // 紋理快取,根據 pixelbuffer 獲取紋理。
@property (nonatomic, strong) MTKView *mtkView; // Metal 渲染的 view。
@property (nonatomic, assign) vector_uint2 viewportSize; // 視口大小。
@property (nonatomic, strong) id<MTLRenderPipelineState> pipelineState; // 渲染管道,管理頂點函式和片元函式。
@property (nonatomic, strong) id<MTLCommandQueue> commandQueue; // 渲染指令佇列。
@property (nonatomic, strong) id<MTLBuffer> vertices; // 頂點快取物件。
@property (nonatomic, assign) NSUInteger numVertices; // 頂點數量。
@property (nonatomic, strong) id<MTLBuffer> yuvMatrix; // YUV 資料矩陣物件。
@property (nonatomic, assign) BOOL updateFillMode; // 填充模式變更標記。
@property (nonatomic, assign) CGSize pixelBufferSize; // pixelBuffer 資料尺寸。
@property (nonatomic, assign) CGSize currentViewSize; // 當前檢視大小。
@property (nonatomic, strong) dispatch_queue_t renderQueue; // 渲染執行緒。
@end

@implementation KFMetalView
#pragma mark - LifeCycle
- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        _currentViewSize = frame.size;
        _fillMode = KFMetalViewContentModeFit;
        _updateFillMode = YES;
        //  建立 Metal 渲染檢視且新增到當前檢視。
        self.mtkView = [[MTKView alloc] initWithFrame:self.bounds];
        self.mtkView.device = MTLCreateSystemDefaultDevice();
        self.mtkView.backgroundColor = [UIColor clearColor];
        [self addSubview:self.mtkView];
        self.mtkView.delegate = self;
        self.mtkView.framebufferOnly = YES;
        self.viewportSize = (vector_uint2) {self.mtkView.drawableSize.width, self.mtkView.drawableSize.height};
        
        // 建立渲染執行緒。
        _semaphore = dispatch_semaphore_create(1);
        _renderQueue = dispatch_queue_create("com.KeyFrameKit.metalView.renderQueue", DISPATCH_QUEUE_SERIAL);
        
        // 建立紋理快取。
        CVMetalTextureCacheCreate(NULL, NULL, self.mtkView.device, NULL, &_textureCache);
    }
    
    return self;
}

- (void)layoutSubviews {
    // 檢視自動調整佈局,同步至 Metal 檢視。
    [super layoutSubviews];
    self.mtkView.frame = self.bounds;
    _currentViewSize = self.bounds.size;
}

- (void)dealloc {
    // 釋放最後一幀資料、紋理快取。
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    if (_pixelBuffer) {
        CFRelease(_pixelBuffer);
        _pixelBuffer = NULL;
    }
    
    if (_textureCache) {
        CVMetalTextureCacheFlush(_textureCache, 0);
        CFRelease(_textureCache);
        _textureCache = NULL;
    }
    dispatch_semaphore_signal(_semaphore);
    [self.mtkView releaseDrawables];
}

#pragma mark - Public Method
- (void)renderPixelBuffer:(CVPixelBufferRef)pixelBuffer {
    if (!pixelBuffer) {
        return;
    }
    // 外層輸入 BGRA、YUV 資料。
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    if (_pixelBuffer) {
        CFRelease(_pixelBuffer);
        _pixelBuffer = NULL;
    }
    _pixelBuffer = pixelBuffer;
    _pixelBufferSize = CGSizeMake(CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer));
    CFRetain(pixelBuffer);
    dispatch_semaphore_signal(_semaphore);
}

- (void)setFillMode:(KFMetalViewContentMode)fillMode {
    // 更改檢視填充模式。
    _fillMode = fillMode;
    _updateFillMode = YES;
}

#pragma mark - Private Method
-(void)_setupPipeline:(BOOL)isYUV {
    // 根據本地 shader 檔案初始化渲染管道與渲染指令佇列。
    id<MTLLibrary> defaultLibrary = [self.mtkView.device newDefaultLibrary];
    id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
    id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:isYUV ? @"yuvSamplingShader" : @"rgbSamplingShader"];
    
    MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
    pipelineStateDescriptor.vertexFunction = vertexFunction;
    pipelineStateDescriptor.fragmentFunction = fragmentFunction;
    pipelineStateDescriptor.colorAttachments[0].pixelFormat = self.mtkView.colorPixelFormat;
    self.pipelineState = [self.mtkView.device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:NULL];
    self.commandQueue = [self.mtkView.device newCommandQueue];
}

- (void)_setupYUVMatrix:(BOOL)isFullRange colorSpace:(CFTypeRef)colorSpace{
    // 初始化 YUV 矩陣,判斷 pixelBuffer 的顏色格式是 601 還是 709,建立對應的矩陣。
    KFConvertMatrix matrix;
    if (colorSpace == kCVImageBufferYCbCrMatrix_ITU_R_601_4) {
        matrix.matrix = isFullRange ? kFColorMatrix601FullRange : kFColorMatrix601VideoRange;
    }else if (colorSpace == kCVImageBufferYCbCrMatrix_ITU_R_709_2) {
        matrix.matrix = isFullRange ? kFColorMatrix709FullRange : kFColorMatrix709VideoRange;
    }
    matrix.fullRange = isFullRange;
    self.yuvMatrix = [self.mtkView.device newBufferWithBytes:&matrix
                                                          length:sizeof(KFConvertMatrix)
                                                         options:MTLResourceStorageModeShared];
}

- (void)_updaterVertices {
    // 根據填充模式計算頂點資料。
    float heightScaling = 1.0;
    float widthScaling = 1.0;
    
    if (!CGSizeEqualToSize(_currentViewSize, CGSizeZero) && !CGSizeEqualToSize(_pixelBufferSize, CGSizeZero)) {
        CGRect insetRect = AVMakeRectWithAspectRatioInsideRect(_pixelBufferSize, CGRectMake(0, 0, _currentViewSize.width, _currentViewSize.height));
        
        switch (_fillMode) {
            case KFMetalViewContentModeStretch: {
                widthScaling = 1.0;
                heightScaling = 1.0;
                break;
            }
            case KFMetalViewContentModeFit: {
                widthScaling = insetRect.size.width / _currentViewSize.width;
                heightScaling = insetRect.size.height / _currentViewSize.height;
                break;
            }
            case KFMetalViewContentModeFill: {
                widthScaling = _currentViewSize.height / insetRect.size.height;
                heightScaling = _currentViewSize.width / insetRect.size.width;
                break;
            }
        }
    }
    
    KFVertex quadVertices[] =
    {
        { { -widthScaling, -heightScaling, 0.0, 1.0 },  { 0.f, 1.f } },
        { { widthScaling,  -heightScaling, 0.0, 1.0 },  { 1.f, 1.f } },
        { { -widthScaling, heightScaling,  0.0, 1.0 },  { 0.f, 0.f } },
        { {  widthScaling, heightScaling,  0.0, 1.0 },  { 1.f, 0.f } },
    };
    // MTLResourceStorageModeShared 屬性可共享的,表示可以被頂點或者片元函式或者其他函式使用。
    self.vertices = [self.mtkView.device newBufferWithBytes:quadVertices
                                                 length:sizeof(quadVertices)
                                                options:MTLResourceStorageModeShared];
    // 獲取頂點數量。
    self.numVertices = sizeof(quadVertices) / sizeof(KFVertex);
}

- (BOOL)_pixelBufferIsFullRange:(CVPixelBufferRef)pixelBuffer {
    // 判斷 YUV 資料是否為 full range。
    CFDictionaryRef cfDicAttributes = CVPixelBufferCopyCreationAttributes(pixelBuffer);
    NSDictionary *dicAttributes = (__bridge_transfer NSDictionary*)cfDicAttributes;
    if (dicAttributes && [dicAttributes objectForKey:@"PixelFormatDescription"]) {
        NSDictionary *pixelFormatDescription = [dicAttributes objectForKey:@"PixelFormatDescription"];
        if (pixelFormatDescription && [pixelFormatDescription objectForKey:(__bridge NSString*)kCVPixelFormatComponentRange]) {
            NSString *componentRange = [pixelFormatDescription objectForKey:(__bridge NSString*)kCVPixelFormatComponentRange];
            return [componentRange isEqualToString:(__bridge NSString*)kCVPixelFormatComponentRange_FullRange];
        }
    }
    return NO;
}

- (void)_drawInMTKView:(MTKView*)view {
    // 渲染資料。
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
    if (_pixelBuffer) {
        // 為當前渲染的每個渲染傳遞建立一個新的命令緩衝區。
        id<MTLCommandBuffer> commandBuffer = [self.commandQueue commandBuffer];
        // 獲取渲染命令編碼器 MTLRenderCommandEncoder 的描述符。
        // currentRenderPassDescriptor 描述符包含 currentDrawable 的紋理、檢視的深度、模板和 sample 緩衝區和清晰的值。
        // MTLRenderPassDescriptor 描述一系列 attachments 的值,類似 OpenGL 的 FrameBuffer;同時也用來建立 MTLRenderCommandEncoder。
        MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
        if (renderPassDescriptor) {
            // 根據描述建立 x 渲染命令編碼器。
            id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
            // 設定繪製區域。
            [renderEncoder setViewport:(MTLViewport) {0.0, 0.0, self.viewportSize.x, self.viewportSize.y, -1.0, 1.0 }];
            BOOL isRenderYUV = CVPixelBufferGetPlaneCount(_pixelBuffer) > 1;
            
            // 根據是否為 YUV 初始化渲染管道。
            if (!self.pipelineState) {
                [self _setupPipeline:isRenderYUV];
            }
            // 設定渲染管道。
            [renderEncoder setRenderPipelineState:self.pipelineState];
            
            // 更新填充模式。
            if (_updateFillMode) {
                [self _updaterVertices];
                _updateFillMode = NO;
            }
            // 傳遞頂點快取。
            [renderEncoder setVertexBuffer:self.vertices
                                    offset:0
                                   atIndex:KFVertexInputIndexVertices];
            CVPixelBufferRef pixelBuffer = _pixelBuffer;
            
            if (isRenderYUV) {
                // 獲取 y、uv 紋理。
                id<MTLTexture> textureY = nil;
                id<MTLTexture> textureUV = nil;
                {
                    size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 0);
                    size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 0);
                    MTLPixelFormat pixelFormat = MTLPixelFormatR8Unorm;
                    
                    CVMetalTextureRef texture = NULL;
                    CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, pixelFormat, width, height, 0, &texture);
                    if (status == kCVReturnSuccess) {
                        textureY = CVMetalTextureGetTexture(texture);
                        CFRelease(texture);
                    }
                }
                
                {
                    size_t width = CVPixelBufferGetWidthOfPlane(pixelBuffer, 1);
                    size_t height = CVPixelBufferGetHeightOfPlane(pixelBuffer, 1);
                    MTLPixelFormat pixelFormat = MTLPixelFormatRG8Unorm;
                    
                    CVMetalTextureRef texture = NULL;
                    CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, pixelFormat, width, height, 1, &texture);
                    if (status == kCVReturnSuccess) {
                        textureUV = CVMetalTextureGetTexture(texture);
                        CFRelease(texture);
                    }
                }
                
                // 傳遞紋理。
                if (textureY != nil && textureUV != nil) {
                    [renderEncoder setFragmentTexture:textureY
                                              atIndex:KFFragmentTextureIndexTextureY];
                    [renderEncoder setFragmentTexture:textureUV
                                              atIndex:KFFragmentTextureIndexTextureUV];
                }
                
                // 初始化 YUV 矩陣。
                if (!self.yuvMatrix) {
                    CFTypeRef matrixKey = CVBufferCopyAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, NULL);
                    [self _setupYUVMatrix:[self _pixelBufferIsFullRange:pixelBuffer] colorSpace:matrixKey];
                    CFRelease(matrixKey);
                }
                // 傳遞 YUV 矩陣。
                [renderEncoder setFragmentBuffer:self.yuvMatrix
                                          offset:0
                                         atIndex:KFFragmentInputIndexMatrix];
            } else {
                // 生成 rgba 紋理。
                id<MTLTexture> textureRGB = nil;
                size_t width = CVPixelBufferGetWidth(pixelBuffer);
                size_t height = CVPixelBufferGetHeight(pixelBuffer);
                MTLPixelFormat pixelFormat = MTLPixelFormatBGRA8Unorm;
                
                CVMetalTextureRef texture = NULL;
                CVReturn status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCache, pixelBuffer, NULL, pixelFormat, width, height, 0, &texture);
                if (status == kCVReturnSuccess) {
                    textureRGB = CVMetalTextureGetTexture(texture);
                    CFRelease(texture);
                }
                
                // 傳遞紋理。
                if (textureRGB) {
                    [renderEncoder setFragmentTexture:textureRGB
                                              atIndex:KFFragmentTextureIndexTextureRGB];
                }
            }
            
            // 繪製。
            [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStrip
                              vertexStart:0
                              vertexCount:self.numVertices];
            
            // 命令結束。
            [renderEncoder endEncoding];
            
            // 顯示。
            [commandBuffer presentDrawable:view.currentDrawable];
            
            // 提交。
            [commandBuffer commit];
        }
        
        CFRelease(_pixelBuffer);
        _pixelBuffer = NULL;
    }
    dispatch_semaphore_signal(_semaphore);
}

#pragma mark - MTKViewDelegate
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
    self.viewportSize = (vector_uint2) {size.width, size.height};
}

- (void)drawInMTKView:(nonnull MTKView *)view {
    // Metal 檢視回撥,有資料情況下渲染檢視。
    __weak typeof(self) weakSelf = self;
    dispatch_async(_renderQueue, ^{
        [weakSelf _drawInMTKView:view];
    });
}

@end

更具體細節見上述程式碼及其註釋。

3、採集視訊資料並渲染

我們在一個 ViewController 中來實現對採集的視訊資料進行渲染播放。

KFVideoRenderViewController.m

#import "KFVideoRenderViewController.h"
#import "KFVideoCapture.h"
#import "KFMetalView.h"

@interface KFVideoRenderViewController ()
@property (nonatomic, strong) KFVideoCaptureConfig *videoCaptureConfig;
@property (nonatomic, strong) KFVideoCapture *videoCapture;
@property (nonatomic, strong) KFMetalView *metalView;
@end

@implementation KFVideoRenderViewController
#pragma mark - Property
- (KFVideoCaptureConfig *)videoCaptureConfig {
    if (!_videoCaptureConfig) {
        _videoCaptureConfig = [[KFVideoCaptureConfig alloc] init];
    }
    
    return _videoCaptureConfig;
}

- (KFVideoCapture *)videoCapture {
    if (!_videoCapture) {
        _videoCapture = [[KFVideoCapture alloc] initWithConfig:self.videoCaptureConfig];
        __weak typeof(self) weakSelf = self;
        _videoCapture.sampleBufferOutputCallBack = ^(CMSampleBufferRef sampleBuffer) {
             // 視訊採集資料回撥。將採集回來的資料給渲染模組渲染。
            [weakSelf.metalView renderPixelBuffer:CMSampleBufferGetImageBuffer(sampleBuffer)];
        };
        _videoCapture.sessionErrorCallBack = ^(NSError* error) {
            NSLog(@"KFVideoCapture Error:%zi %@", error.code, error.localizedDescription);
        };
    }
    
    return _videoCapture;
}

#pragma mark - Lifecycle
- (void)viewDidLoad {
    [super viewDidLoad];

    [self requestAccessForVideo];
    [self setupUI];
}

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    self.metalView.frame = self.view.bounds;
}

#pragma mark - Action
- (void)changeCamera {
    [self.videoCapture changeDevicePosition:self.videoCapture.config.position == AVCaptureDevicePositionBack ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack];
}

#pragma mark - Private Method
- (void)requestAccessForVideo {
    __weak typeof(self) weakSelf = self;
    AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
    switch (status) {
        case AVAuthorizationStatusNotDetermined:{
            // 許可對話沒有出現,發起授權許可。
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                if (granted) {
                    [weakSelf.videoCapture startRunning];
                } else {
                    // 使用者拒絕。
                }
            }];
            break;
        }
        case AVAuthorizationStatusAuthorized:{
            // 已經開啟授權,可繼續。
            [weakSelf.videoCapture startRunning];
            break;
        }
        default:
            break;
    }
}

- (void)setupUI {
    self.edgesForExtendedLayout = UIRectEdgeAll;
    self.extendedLayoutIncludesOpaqueBars = YES;
    self.title = @"Video Render";
    self.view.backgroundColor = [UIColor whiteColor];
    
    // Navigation item.
    UIBarButtonItem *cameraBarButton = [[UIBarButtonItem alloc] initWithTitle:@"Camera" style:UIBarButtonItemStylePlain target:self action:@selector(changeCamera)];
    self.navigationItem.rightBarButtonItems = @[cameraBarButton];
    
    // 渲染 view。
    _metalView = [[KFMetalView alloc] initWithFrame:self.view.bounds];
    _metalView.fillMode = KFMetalViewContentModeFill;
    [self.view addSubview:self.metalView];
}

@end

上面是 KFVideoRenderViewController 的實現,主要分為以下幾個部分:

  • 1)在頁面載入完成後,啟動採集模組。

    • -requestAccessForVideo 方法中實現。
  • 2)做好渲染模組 KFMetalView 的佈局。
    • -setupUI 方法中實現。
  • 3)在採集模組的回撥中將採集的視訊資料給渲染模組渲染。

    • KFVideoCapturesampleBufferOutputCallBack 回撥中實現。

更具體細節見上述程式碼及其註釋。

- 完 -