本系列文章通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。

这里是第十三篇:iOS 视频渲染 Demo。这个 Demo 里包含以下内容:

  • 1)实现一个视频采集装模块;

  • 2)实现一个视频渲染模块;

  • 3)串联视频采集和渲染模块,将采集的视频数据输入给渲染模块进行渲染;

  • 4)详尽的代码注释,帮你理解代码逻辑和原理。

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; // 切换摄像头。
@endNS_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 */

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

然后,我们在 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; // 渲染。
@endNS_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:&matrixlength: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:quadVerticeslength: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.verticesoffset:0atIndex: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:textureYatIndex:KFFragmentTextureIndexTextureY];[renderEncoder setFragmentTexture:textureUVatIndex:KFFragmentTextureIndexTextureUV];}// 初始化 YUV 矩阵。if (!self.yuvMatrix) {CFTypeRef matrixKey = CVBufferCopyAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, NULL);[self _setupYUVMatrix:[self _pixelBufferIsFullRange:pixelBuffer] colorSpace:matrixKey];CFRelease(matrixKey);}// 传递 YUV 矩阵。[renderEncoder setFragmentBuffer:self.yuvMatrixoffset:0atIndex: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:textureRGBatIndex:KFFragmentTextureIndexTextureRGB];}}// 绘制。[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangleStripvertexStart:0vertexCount: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)在采集模块的回调中将采集的视频数据给渲染模块渲染。

    • 在 KFVideoCapture 的 sampleBufferOutputCallBack 回调中实现。

更具体细节见上述代码及其注释。

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

iOS音视频开发十三:视频渲染,用 Metal 渲染相关推荐

  1. Moviepy音视频开发:视频转gif动画或jpg图片exe图形化工具开发案例

    ☞ ░ 前往老猿Python博文目录 ░ 一.引言 老猿之所以学习和研究Moviepy的使用,是因为需要一个将视频转成动画的工具,当时在网上到处搜索查找免费使用工具,结果找了很多自称免费的工具,但转完 ...

  2. 【Anychat音视频开发】视频直播系统的开发技术点

    视频直播是利用视频压缩.直播等流媒体技术,在装有电视卡或视频采集卡的电脑上安装一套视频直播服务软件,把采集到的视频信号进行一系列实时编码.处理,然后再广播出去,起到同步直播的效果.视频直播被广泛的应用 ...

  3. Qt/C++音视频开发45-音视频类结构体参数的设计

    一.前言 视频监控内核组件重构和完善花了一年多时间,整个组件个人认为设计的最好的部分就是各种结构体参数的设计,而且分门别类,有枚举值,也有窗体相关的结构体参数,解码相关的结构体参数,同时将部分常用的结 ...

  4. Qt音视频开发27-ffmpeg视频旋转显示

    一.前言 用手机或者平板拍摄的视频文件,很可能是旋转的,比如分辨率是1280x720,确是垂直的,相当于分辨率变成了720x1280,如果不做旋转处理的话,那脑袋必须歪着看才行,这样看起来太难受,所以 ...

  5. 短视频开发,录制视频添加背景音乐功能实现

    短视频开发,会在视频录制时提供相关的背景音乐选择,看似简单的选取背景音乐,但对开发人员来说却不是那么简单,如何实现短视频开发录制视频添加背景音乐功能呢? 1.短视频开发在录制界面点击音乐,绘制UI 添 ...

  6. Android短视频开发都需要什么技术?

    今天我们来讲点干货,估计来看这篇帖子的人都知道短视频APP有多火,也都知道安卓系统在手机系统中占的市场份额有多大,那我就不多嘴巴拉巴拉一些行业背景了,以下我着重讲一讲Android端的短视频开发技术. ...

  7. android短视频技术,Android短视频开发都需要什么技术?

    今天我们来讲点干货,估计来看这篇帖子的人都知道短视频APP有多火,也都知道安卓系统在手机系统中占的市场份额有多大,那我就不多嘴巴拉巴拉一些行业背景了,以下我着重讲一讲Android端的短视频开发技术. ...

  8. iOS音视频开发七:视频采集

    将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发. 这里是第七篇:iOS 视频采集 Demo.这个 ...

  9. iOS音视频开发八:视频编码,H.264 和 H.265 都支持

    我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发. 这里是第八篇:iOS 视频编码 Demo. ...

最新文章

  1. mysql数据库oem_Oracle 11gR2学习之二(创建数据库及OEM管理篇)
  2. 是否可以从一个static方法内部发出对非static方法的调用?
  3. 转:GridView 中如何给删除按钮添加提示
  4. Qt入门——三个臭皮匠顶个诸葛亮
  5. python语言format用法_python基础_格式化输出(%用法和format用法)
  6. 程序员刚结婚3天,老婆疑似骗婚?聊天记录曝光,网友:拜金实锤
  7. HDU - 5176 The Experience of Love(并查集)
  8. Coursera自动驾驶课程第11讲:2D Object Detection
  9. Ubuntu 下JDK安装
  10. 【FastJson】FastJson一个Bug java.util.LinkedHashMap cannot be cast to com.alibaba.fastjson.Jsonobject
  11. Ubuntu 下 Eclipse 桌面图标创建
  12. 用代码来理解 C#委托与事件
  13. 如何在Mac电脑上更改地区或国家位置设定?
  14. 基于VHDL语言的一位全加器
  15. Java、python、数据分析精美简历模板
  16. 视频编码格式——h264优点
  17. cattee翻译_0302 echo、重定向、管道、cat、tee
  18. 阿里云短信通道被人恶意刷了几万条短信,怎么办?(短信接口被盗刷系列3)
  19. 大学计算机课能旷课吗,计算机课旷课检讨书
  20. PMBOK(第六版) 学习笔记 ——《第八章 项目质量管理》

热门文章

  1. 契约锁助力仓库单据电子签:货物“入-检-调-提-出”再提速
  2. 一幅长文细学HTML5
  3. 【操作系统】虚拟地址和页表项的关系
  4. DedeCms织梦 tag标签静态化-教程
  5. 【Tip】让我安安静静看直播,一键屏蔽斗鱼虎牙右聊天框
  6. [转]百度UEditor编辑器(php)
  7. 基于IGX Web SCADA平台构建 - 污水处理厂监控系统
  8. 实验吧-因缺思厅的绕过writeup
  9. 计算机网络-网络结构
  10. CSS .class .class与.class.class区别