前言
本篇接着005 - 视频H264编码详解(中),主要做H264编解码流程中的最后2环 👇
- 继续封装
解码工具类
- 拿到
解码
的流数据之后,渲染显示
视频帧画面
一、初始化
初始化
分为2个方法执行👇🏻
工具类本身
的对外公开的初始化方法 👉🏻- (instancetype)initWithConfig:(CCVideoConfig*)config;
解码器
的初始化 👉🏻 这是在解码的时候
才做的事情!
- 和编码工具类一样,也是2个异步队列分别做
解码
和回调
👇🏻
@property (nonatomic, strong) dispatch_queue_t decodeQueue;
@property (nonatomic, strong) dispatch_queue_t callbackQueue;
解码器
的初始化和编码器
一样,需要解码session
👇🏻
@property (nonatomic) VTDecompressionSessionRef decodeSesion;
- 解码session的
创建
函数,需依赖SPS/PPS
等关键帧的信息,然后输出一个视频帧格式
描述CMVideoFormatDescriptionRef
,所以还需要定义以下成员变量👇🏻
@implementation CCVideoDecoder{
uint8_t *_sps;
NSUInteger _spsSize;
uint8_t *_pps;
NSUInteger _ppsSize;
CMVideoFormatDescriptionRef _decodeDesc; // 视频输出格式
}
1.1 工具类的初始化
首先是工具类的初始化
,和编码器工具类一样,都是依赖配置类CCVideoConfig
👇🏻
- (instancetype)initWithConfig:(CCVideoConfig *)config {
self = [super init];
if (self) {
//初始化VideoConfig 信息
_config = config;
//创建解码队列与回调队列
_decodeQueue = dispatch_queue_create("h264 hard decode queue", DISPATCH_QUEUE_SERIAL);
_callbackQueue = dispatch_queue_create("h264 hard decode callback queue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
1.2 解码器的初始化
接着是编码器的初始化
,包括2部分:创建
+ 配置
。
1.2.1 相关函数
解码session的创建
比编码器的稍微复杂点,包括3部分内容👇🏻
- 根据
sps pps
设置解码的视频输出格式
使用函数CMVideoFormatDescriptionCreateFromH264ParameterSets
👇🏻
其参数释义如下👇🏻
- 参数1: kCFAllocatorDefault
分配器
- 参数2: 2个
参数个数
- 参数3: parameterSetPointers
参数集指针
- 参数4: parameterSetSizes
参数集大小
- 参数5: NALUnitHeaderLength
起始位的长度
长度为4
- 参数6: _decodeDesc
解码器描述
解码器参数
的配置
常用的解码器参数
有以下几个👇🏻
- kCVPixelBufferPixelFormatTypeKey:摄像头的输出数据格式,已测可用值为
- kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,即420v
- kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,即420f
- kCVPixelFormatType_32BGRA,iOS在内部进行YUV至BGRA格式转换
YUV420
一般用于标清
视频,YUV422
用于高清
视频,这里的限制让人感到意外。但是,在相同条件下,YUV420计算耗时和传输压力比YUV422都小。
- kCVPixelBufferWidthKey/kCVPixelBufferHeightKey: 视频源的
分辨率
width*height - kCVPixelBufferOpenGLCompatibilityKey: 它允许在
OpenGL
的上下文中直接绘制解码后的图像,而不是从总线和 CPU 之间复制数据。这有时候被称为零拷贝通道
,因为在绘制过程中没有解码的图像被拷贝。
- 解码器的
回调设置
编码器的回调
是在创建session时所指定的函数指针
,但是解码器的回调
却不同,解码器的回调
是一个简单的结构体
VTDecompressionOutputCallbackRecord👇🏻
它带有一个指针decompressionOutputCallback
,指向帧解压完成后的回调方法,还需要提供可以找到这个回调方法的实例decompressionOutputRefCon
。其中VTDecompressionOutputCallback
定义如下 👇🏻
回调方法包括七个参数 👇🏻
- 参数1: 回调的引用
- 参数2: 帧的引用
- 参数3: 一个状态标识 (包含未定义的代码)
- 参数4: 指示同步/异步解码,或者解码器是否打算丢帧的标识
- 参数5: 实际图像的缓冲
- 参数6: 出现的时间戳
- 参数7: 出现的持续时间
最后,就是解码session创建函数
👇🏻
创建用于解压缩视频帧的会话,解压后的帧将通过调用OutputCallback
发出,参数包括 👇🏻
- 参数1: allocator 内存的会话。使用默认的
kCFAllocatorDefault
- 参数2: videoFormatDescription 描述源视频帧
- 参数3: videoDecoderSpecification 指定必须使用的特定视频解码器.
NULL
- 参数4: destinationImageBufferAttributes 描述源像素缓冲区的要求
NULL
- 参数5: outputCallback 使用已解压缩的帧调用的
回调
- 参数6: decompressionSessionOut 指向一个变量以接收新的解压会话
1.2.2 完整版代码
- (BOOL)initDecoder {
// 保证解码器只初始化一次
if (_decodeSesion) {
return true;
}
const uint8_t * const parameterSetPointers[2] = {_sps, _pps};
const size_t parameterSetSizes[2] = {_spsSize, _ppsSize};
int naluHeaderLen = 4;
//根据sps pps设置解码视频输出格式
OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, parameterSetPointers, parameterSetSizes, naluHeaderLen, &_decodeDesc);
if (status != noErr) {
NSLog(@"Video hard DecodeSession create H264ParameterSets(sps, pps) failed status= %d", (int)status);
return false;
}
//解码参数
NSDictionary *destinationPixBufferAttrs =
@{
(id)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange], //iOS上 nv12(uvuv排布) 而不是nv21(vuvu排布)
(id)kCVPixelBufferWidthKey: [NSNumber numberWithInteger:_config.width],
(id)kCVPixelBufferHeightKey: [NSNumber numberWithInteger:_config.height],
(id)kCVPixelBufferOpenGLCompatibilityKey: [NSNumber numberWithBool:true]
};
//解码回调设置
VTDecompressionOutputCallbackRecord callbackRecord;
callbackRecord.decompressionOutputCallback = videoDecompressionOutputCallback;
callbackRecord.decompressionOutputRefCon = (__bridge void * _Nullable)(self);
//创建session
status = VTDecompressionSessionCreate(kCFAllocatorDefault, _decodeDesc, NULL, (__bridge CFDictionaryRef _Nullable)(destinationPixBufferAttrs), &callbackRecord, &_decodeSesion);
//判断一下status
if (status != noErr) {
NSLog(@"Video hard DecodeSession create failed status= %d", (int)status);
return false;
}
//设置解码会话属性(实时编码)
status = VTSessionSetProperty(_decodeSesion, kVTDecompressionPropertyKey_RealTime,kCFBooleanTrue);
NSLog(@"Vidoe hard decodeSession set property RealTime status = %d", (int)status);
return true;
}
二、解码 & 回调
2.1 解码流程
之前我们定义了一个public解码方法 👇
- (void)decodeNaluData:(NSData *)frame {
//将解码放在异步队列.
dispatch_async(_decodeQueue, ^{
//获取frame 二进制数据,将数据拆解
uint8_t *nalu = (uint8_t *)frame.bytes;
//调用解码Nalu数据方法,参数1:数据 参数2:数据长度
[self decodeNaluData:nalu size:(uint32_t)frame.length];
});
}
将解码Nalu流数据
的过程,单独放在了decodeNaluData:size:
方法里 👇🏻
- (void)decodeNaluData:(uint8_t *)frame size:(uint32_t)size {
//数据类型:frame的前4个字节是NALU数据的开始码,也就是00 00 00 01,
//第5个字节是表示数据类型type,转为10进制后,7是sps, 8是pps, 5是IDR(I帧)信息
int type = (frame[4] & 0x1F);
// 将NALU的开始码转为4字节大端NALU的长度信息
uint32_t naluSize = size - 4;
uint8_t *pNaluSize = (uint8_t *)(&naluSize);
CVPixelBufferRef pixelBuffer = NULL;
frame[0] = *(pNaluSize + 3);
frame[1] = *(pNaluSize + 2);
frame[2] = *(pNaluSize + 1);
frame[3] = *(pNaluSize);
//第一次解析时: 初始化解码器initDecoder
/*
关键帧/其他帧数据: 调用[self decode:frame withSize:size] 方法
sps/pps数据:则将sps/pps数据赋值到_sps/_pps中.
*/
switch (type) {
case 0x05: //关键帧
if ([self initDecoder]) {
pixelBuffer= [self decode:frame withSize:size];
}
break;
case 0x06:
//NSLog(@"SEI");//增强信息
break;
case 0x07: //sps memcpy保存起来
_spsSize = naluSize;
_sps = malloc(_spsSize);
memcpy(_sps, &frame[4], _spsSize);
break;
case 0x08: //pps memcpy保存起来
_ppsSize = naluSize;
_pps = malloc(_ppsSize);
memcpy(_pps, &frame[4], _ppsSize);
break;
default: //其他帧(1-5)
if ([self initDecoder]) {
pixelBuffer = [self decode:frame withSize:size];
}
break;
}
}
之所以定义decodeNaluData:size:
这个方法,就是清晰解码的流程,该方法通过switch-case
方式单独处理每一帧的流数据,先判断是什么类型帧,再单独做处理👇🏻
- sps和pps: 不解码,只缓存 👉🏻 初始化解码器会用到
关键帧
和其他非关键帧
: 解码前需先判断解码器初始化是否完成
。
接下来就是核心的解码流程,之前我们分析过,知道解码涉及了2个数据结构👇🏻
CVPixelBufferRef
👉🏻编码之前
/解码之后
的数据CMBlockBufferRef
👉🏻编码之后
的数据
解码的流程,我们封装在方法decode:withSize:
中 👇🏻
- (CVPixelBufferRef)decode:(uint8_t *)frame withSize:(uint32_t)frameSize {
CVPixelBufferRef outputPixelBuffer = NULL;
CMBlockBufferRef blockBuffer = NULL;
CMBlockBufferFlags flag0 = 0;
//创建blockBuffer
OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, frame, frameSize, kCFAllocatorNull, NULL, 0, frameSize, flag0, &blockBuffer);
if (status != kCMBlockBufferNoErr) {
NSLog(@"Video hard decode create blockBuffer error code=%d", (int)status);
return outputPixelBuffer;
}
CMSampleBufferRef sampleBuffer = NULL;
const size_t sampleSizeArray[] = {frameSize};
//创建sampleBuffer
status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _decodeDesc, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);
if (status != noErr || !sampleBuffer) {
NSLog(@"Video hard decode create sampleBuffer failed status=%d", (int)status);
CFRelease(blockBuffer);
return outputPixelBuffer;
}
//解码
//向视频解码器提示使用低功耗模式是可以的
VTDecodeFrameFlags flag1 = kVTDecodeFrame_1xRealTimePlayback;
//异步解码
VTDecodeInfoFlags infoFlag = kVTDecodeInfo_Asynchronous;
//解码数据
status = VTDecompressionSessionDecodeFrame(_decodeSesion, sampleBuffer, flag1, &outputPixelBuffer, &infoFlag);
if (status == kVTInvalidSessionErr) {
NSLog(@"Video hard decode InvalidSessionErr status =%d", (int)status);
} else if (status == kVTVideoDecoderBadDataErr) {
NSLog(@"Video hard decode BadData status =%d", (int)status);
} else if (status != noErr) {
NSLog(@"Video hard decode failed status =%d", (int)status);
}
CFRelease(sampleBuffer);
CFRelease(blockBuffer);
return outputPixelBuffer;
}
以上方法,可以看出解码的过程👇🏻
(uint8_t *)frame --> CMBlockBufferRef --> CMSampleBufferRef
- 解码器只认
CMSampleBufferRef
,且解码后的数据存储在CVPixelBufferRef
中
2.2 解码回调函数的流程
接下来,我们看看解码回调函数
中做了什么?
void videoDecompressionOutputCallback(void * CM_NULLABLE decompressionOutputRefCon,
void * CM_NULLABLE sourceFrameRefCon,
OSStatus status,
VTDecodeInfoFlags infoFlags,
CM_NULLABLE CVImageBufferRef imageBuffer,
CMTime presentationTimeStamp,
CMTime presentationDuration ) {
if (status != noErr) {
NSLog(@"Video hard decode callback error status=%d", (int)status);
return;
}
//解码后的数据sourceFrameRefCon -> CVPixelBufferRef
CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
*outputPixelBuffer = CVPixelBufferRetain(imageBuffer);
//获取self
CCVideoDecoder *decoder = (__bridge CCVideoDecoder *)(decompressionOutputRefCon);
//调用回调队列
dispatch_async(decoder.callbackQueue, ^{
//将解码后的数据给decoder代理.viewController
[decoder.delegate videoDecodeCallback:imageBuffer];
//释放数据
CVPixelBufferRelease(imageBuffer);
});
}
流程很简单,拿到解码后的数据CVPixelBufferRef
,再在回调队列中异步delegate输出数据。
至此,整个解码工具类的封装完毕。
三、渲染显示
最后,就是显示流数据了,其实是将CVPixelBufferRef
中的数据显示到屏幕上,此时我们需要使用OpenGL ES
,它是专门做图形/图片
纹理渲染的,OpenGL ES
默认的颜色体系是RGB
,但是CVPixelBufferRef
中的颜色配置的是YUV 4:2:0
,所以需要做个转换 👉🏻 YUV --> RGB
!
YUV
模式中 Y
表示亮度
,也就是灰阶值
,它是基础信号,而U和V
表示的则是色度
,UV的作用是描述影像的色彩及饱和度
,它们用于指定像素的颜色
。所以,只有Y
是可以显示图像的,只不过是黑白色的,有了UV
的加持,就变成彩色的了,因此,我们可以推断出 👉🏻 视频由2个图层组成:Y图层纹理+UV图层纹理
,那么👇🏻
视频的渲染-->纹理的渲染-->片元着色器填充-->width*height正方形(渲染2个纹理)
再回到代码部分,我们最终在解码回调中,将解码后的数据delegate到viewController中👇🏻
//h264解码回调
- (void)videoDecodeCallback:(CVPixelBufferRef)imageBuffer {
//显示
if (imageBuffer) {
_displayLayer.pixelBuffer = imageBuffer;
}
}
这个_displayLayer
是AAPLEAGLLayer
👇🏻
@property (nonatomic, strong) AAPLEAGLLayer *displayLayer;
AAPLEAGLLayer
是继承CAEAGLLayer
的,而CAEAGLLayer
是iOS原生库QuartzCore
里的👇🏻
所以CAEAGLLayer
只是个图层
,它是iOS macOS提供的一个专门用来渲染OpenGL ES
的图层继承CALayer
👇🏻
而OpenGL ES
它是负责核心的渲染
动作,至于交给谁去显示(比如Layer、比如view),OpenGL ES
并不关心,这个是由编译器去决定
的,这个就是OpenGL ES
跨平台的核心,不被任何系统所约束!
最后,我们来看看图层类AAPLEAGLLayer
这块的封装,图层显示数据,无非就是初始化
+ 渲染
这2个主要流程!
3.1 初始化
首先看看初始化,主要就是图层类
的初始化 和 OpenGL
的初始化。
3.1.1 图层的初始化
@interface AAPLEAGLLayer : CAEAGLLayer
@property CVPixelBufferRef pixelBuffer;
- (id)initWithFrame:(CGRect)frame;
- (void)resetRenderBuffer;
@end
AAPLEAGLLayer提供的初始化方法就是- (id)initWithFrame:(CGRect)frame;
,调用的地方(ViewController.m
)是这么写👇🏻
//显示解码后的数据 -> OpenGL ES
CGSize size = CGSizeMake(self.view.frame.size.width/2, self.view.frame.size.height/2);//分辨率
_displayLayer = [[AAPLEAGLLayer alloc] initWithFrame:CGRectMake(size.width, 100, size.width, size.height)];
[self.view.layer addSublayer:_displayLayer];
初始化方法的实现 👇🏻
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super init];
if (self) {
// scale
CGFloat scale = [[UIScreen mainScreen] scale];
self.contentsScale = scale;
// 透明度
self.opaque = TRUE;
// kEAGLDrawablePropertyRetainedBacking 👉🏻视频绘制完成后是否需要保留其内容
self.drawableProperties = @{ kEAGLDrawablePropertyRetainedBacking :[NSNumber numberWithBool:YES]};
[self setFrame:frame];
//上下文 Set the context into which the frames will be drawn.
_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
if (!_context) {
return nil;
}
//指定默认的颜色转换类型 HDTV标准BT.709
_preferredConversion = kColorConversion709;
//初始化OpenGL
[self setupGL];
}
return self;
}
3.1.2 OpenGL的初始化
接下来就是OpenGL
的初始化,需要定义一些成员变量,为初始化做准备 👇🏻
@interface AAPLEAGLLayer ()
{
//渲染缓存区的宽和高
GLint _backingWidth;
GLint _backingHeight;
//上下文:用来判断图层是否初始化成功
EAGLContext *_context;
//2个纹理:亮度纹理 和 色度纹理,渲染显示时使用
CVOpenGLESTextureRef _lumaTexture;
CVOpenGLESTextureRef _chromaTexture;
//缓存区:帧缓存区/渲染缓存区,
GLuint _frameBufferHandle;
GLuint _colorBufferHandle;
//所需要的颜色标准,如kColorConversion601/kColorConversion709
const GLfloat *_preferredConversion;
}
接着看setupGL
方法的实现👇🏻
- (void)setupGL
{
if (!_context || ![EAGLContext setCurrentContext:_context]) {
return;
}
//1.设置缓存区
[self setupBuffers];
//2.加载Shaders
[self loadShaders];
//3.使用program
glUseProgram(self.program);
//4.设置相关的参数 👉🏻 Uniform
// 0 and 1 are the texture IDs of _lumaTexture and _chromaTexture respectively.
glUniform1i(uniforms[UNIFORM_Y], 0);
glUniform1i(uniforms[UNIFORM_UV], 1);
glUniform1f(uniforms[UNIFORM_ROTATION_ANGLE], 0);
glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion);
}
3.1.2.1 配置Uniform相关参数
上述代码中,我们看到是通过glUniform1i
方法配置的Uniform相关参数,其中,key是uniforms数组
中的元素,后面的0或1是value,最后又通过glUniformMatrix3fv
处理颜色转换矩阵
,这个后面会细讲。
uniforms数组
定义如下👇🏻
// Uniform index.
enum
{
UNIFORM_Y, //Y纹理
UNIFORM_UV,//UV纹理
UNIFORM_ROTATION_ANGLE,//渲染角度
UNIFORM_COLOR_CONVERSION_MATRIX,//颜色变换矩阵
NUM_UNIFORMS
};
GLint uniforms[NUM_UNIFORMS];
这是OpenGL ES
中,配置uniforms
相关的一些常用参数。
3.1.2.2 设置缓存区
接着看看setupBuffers
设置缓存区方法的实现👇🏻
- (void)setupBuffers
{
//取消深度测试 深度问题 👉🏻 一个时间点只显示一张图片
glDisable(GL_DEPTH_TEST);
//配置顶点信息
glEnableVertexAttribArray(ATTRIB_VERTEX);
glVertexAttribPointer(Attribute index, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
//配置纹理坐标信息(x, y)
glEnableVertexAttribArray(ATTRIB_TEXCOORD);
glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
//创建buffer
[self createBuffers];
}
缓存区分帧缓存区
和 渲染缓存区
,它们的创建和使用时机👇🏻
- 初始化OpenGL时,需要创建缓存区
- 渲染显示数据时,需要从缓存区里取出数据
- Attribute index
设置缓存区的时候,我们使用到了
ATTRIB_VERTEX
和ATTRIB_TEXCOORD
,它们的定义如下👇🏻
enum
{
ATTRIB_VERTEX,//顶点坐标
ATTRIB_TEXCOORD,//纹理坐标
NUM_ATTRIBUTES
};
- 创建buffer
接着就是
createBuffers
buffer的创建过程👇🏻
- (void) createBuffers
{
//创建 帧缓存区
glGenFramebuffers(1, &_frameBufferHandle);
glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferHandle);
//创建 渲染缓存区
glGenRenderbuffers(1, &_colorBufferHandle);
glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self];
//设置渲染缓存区的宽/高
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_backingWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_backingHeight);
//设置渲染缓存区的 颜色挂载点 和 目标类型
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBufferHandle);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
NSLog(@"Failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
}
}
- 释放buffer
有创建,当然也有释放
releaseBuffers
👇🏻
- (void)releaseBuffers
{
if(_frameBufferHandle) {
glDeleteFramebuffers(1, &_frameBufferHandle);
_frameBufferHandle = 0;
}
if(_colorBufferHandle) {
glDeleteRenderbuffers(1, &_colorBufferHandle);
_colorBufferHandle = 0;
}
}
其实就是删除
_frameBufferHandle和_colorBufferHandle。
3.1.2.3 加载Shaders
loadShaders
(加载Shaders)中,主要是针对片元着色器
和顶点着色器
的处理 👇🏻
编译 + 链接 + uniforms的连接
首先需要定义program👇🏻
@property GLuint program;
接着编译片元着色器
和顶点着色器
,方法如下👇🏻
- (BOOL)compileShaderString:(GLuint *)shader type:(GLenum)type shaderString:(const GLchar*)shaderString
{
//创建Shader,绑定Source,编译shader
*shader = glCreateShader(type);
glShaderSource(*shader, 1, &shaderString, NULL);
glCompileShader(*shader);
//接下来,就是获取shader的状态信息,有错就打印出来,一切正常,最终返回YES
#if defined(DEBUG)
GLint logLength;
glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength);
if (logLength > 0) {
GLchar *log = (GLchar *)malloc(logLength);
glGetShaderInfoLog(*shader, logLength, &logLength, log);
NSLog(@"Shader compile log:\n%s", log);
free(log);
}
#endif
GLint status = 0;
glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
if (status == 0) {
glDeleteShader(*shader);
return NO;
}
return YES;
}
- 通过参数
(GLenum)type
区分着色器,是片元着色器
还是顶点着色器
- 参数
(GLuint *)shader
:用来接收生成的着色器
- 参数
(const GLchar*)shaderString
:配置着色器生成规则
的字符串
熟悉OpenGL ES的同学应该能写出着色器生成规则
,如果是第一次接触,可以先尝试百度搜索了解一下先。我这里就示例写一下👇🏻
- 顶点着色器
const GLchar *shader_vsh = (const GLchar*)"attribute vec4 position;"
"attribute vec2 texCoord;"
"uniform float preferredRotation;"
"varying vec2 texCoordVarying;"
"void main()"
"{"
" mat4 rotationMatrix = mat4(cos(preferredRotation), -sin(preferredRotation), 0.0, 0.0,"
" sin(preferredRotation), cos(preferredRotation), 0.0, 0.0,"
" 0.0, 0.0, 1.0, 0.0,"
" 0.0, 0.0, 0.0, 1.0);"
" gl_Position = position * rotationMatrix;"
" texCoordVarying = texCoord;"
"}";
- 片元着色器
const GLchar *shader_fsh = (const GLchar*)"varying highp vec2 texCoordVarying;"
"precision mediump float;"
"uniform sampler2D SamplerY;"
"uniform sampler2D SamplerUV;"
"uniform mat3 colorConversionMatrix;"
"void main()"
"{"
" mediump vec3 yuv;"
" lowp vec3 rgb;"
// Subtract constants to map the video range start at 0
" yuv.x = (texture2D(SamplerY, texCoordVarying).r - (16.0/255.0));"
" yuv.yz = (texture2D(SamplerUV, texCoordVarying).rg - vec2(0.5, 0.5));"
" rgb = colorConversionMatrix * yuv;"
" gl_FragColor = vec4(rgb, 1);"
"}";
⚠️注意:先编译
顶点着色器
,再根据顶点着色器
编译片元着色器
。
编译完成后,需要将着色器与program连接起来,涉及2个方法👇🏻
- glAttachShader 附着
- glBindAttribLocation 绑定
接下来就是链接
👇🏻
- (BOOL)linkProgram:(GLuint)prog
{
GLint status;
glLinkProgram(prog);
#if defined(DEBUG)
GLint logLength;
glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
if (logLength > 0) {
GLchar *log = (GLchar *)malloc(logLength);
glGetProgramInfoLog(prog, logLength, &logLength, log);
NSLog(@"Program link log:\n%s", log);
free(log);
}
#endif
glGetProgramiv(prog, GL_LINK_STATUS, &status);
if (status == 0) {
return NO;
}
return YES;
}
核心的就一句glLinkProgram(prog);
,后面都是些错误状态status
的打印。
最后就是uniforms的连接
👇🏻
//uniform和"SamplerY"、"SamplerUV"、"preferredRotation"和"colorConversionMatrix"的连接
uniforms[UNIFORM_Y] = glGetUniformLocation(self.program, "SamplerY");
uniforms[UNIFORM_UV] = glGetUniformLocation(self.program, "SamplerUV");
uniforms[UNIFORM_ROTATION_ANGLE] = glGetUniformLocation(self.program, "preferredRotation");
uniforms[UNIFORM_COLOR_CONVERSION_MATRIX] = glGetUniformLocation(self.program, "colorConversionMatrix");
完整版👇🏻
- (BOOL)loadShaders {
GLuint vertShader = 0, fragShader = 0;
// Create the shader program.
self.program = glCreateProgram();
//编译 片元着色器和顶点着色器
if(![self compileShaderString:&vertShader type:GL_VERTEX_SHADER shaderString:shader_vsh]) {
NSLog(@"Failed to compile vertex shader");
return NO;
}
if(![self compileShaderString:&fragShader type:GL_FRAGMENT_SHADER shaderString:shader_fsh]) {
NSLog(@"Failed to compile fragment shader");
return NO;
}
//附着 顶点着色器和片元着色器
glAttachShader(self.program, vertShader);
glAttachShader(self.program, fragShader);
//绑定 ATTRIB_VERTEX 和 program的"position"属性 / ATTRIB_TEXCOORD 和program的"texCoord"属性
glBindAttribLocation(self.program, ATTRIB_VERTEX, "position");
glBindAttribLocation(self.program, ATTRIB_TEXCOORD, "texCoord");
// Link the program.
if (![self linkProgram:self.program]) {//link失败的处理
NSLog(@"Failed to link program: %d", self.program);
if (vertShader) {
glDeleteShader(vertShader);
vertShader = 0;
}
if (fragShader) {
glDeleteShader(fragShader);
fragShader = 0;
}
if (self.program) {
glDeleteProgram(self.program);
self.program = 0;
}
return NO;
}
//uniform和"SamplerY"、"SamplerUV"、"preferredRotation"和"colorConversionMatrix"的连接
uniforms[UNIFORM_Y] = glGetUniformLocation(self.program, "SamplerY");
uniforms[UNIFORM_UV] = glGetUniformLocation(self.program, "SamplerUV");
uniforms[UNIFORM_ROTATION_ANGLE] = glGetUniformLocation(self.program, "preferredRotation");
uniforms[UNIFORM_COLOR_CONVERSION_MATRIX] = glGetUniformLocation(self.program, "colorConversionMatrix");
//此时uniform和self.program已经连接配置好了,那么之前的片元着色器和顶点着色器与program的连接就可以释放删除了
// Release vertex and fragment shaders.
if (vertShader) {
glDetachShader(self.program, vertShader);
glDeleteShader(vertShader);
}
if (fragShader) {
glDetachShader(self.program, fragShader);
glDeleteShader(fragShader);
}
return YES;
}
3.2 渲染显示数据
接下来就是渲染
这块的处理,先看ViewController.m
中的调用处代码👇🏻
- (void)videoDecodeCallback:(CVPixelBufferRef)imageBuffer {
//显示
if (imageBuffer) {
_displayLayer.pixelBuffer = imageBuffer;
}
}
在解码器的回调方法中,通过设置图层_displayLayer
的pixelBuffer
属性来传输解码后的数据,所以属性pixelBuffer
的set方法,就是渲染的入口
。
//渲染入口
- (void)setPixelBuffer:(CVPixelBufferRef)pb {
// ...
}
废话不多说,直接上代码👇🏻
- (void)setPixelBuffer:(CVPixelBufferRef)pb {
//视频是一帧一帧的数据,不断往里填充,之前的缓存中已经显示过了,所以必须先清空
if(_pixelBuffer) {
CVPixelBufferRelease(_pixelBuffer);
}
//获得数据
_pixelBuffer = CVPixelBufferRetain(pb);
//获取视频帧的宽和高
int frameWidth = (int)CVPixelBufferGetWidth(_pixelBuffer);
int frameHeight = (int)CVPixelBufferGetHeight(_pixelBuffer);
//显示数据
[self displayPixelBuffer:_pixelBuffer width:frameWidth height:frameHeight];
}
3.2.1 显示前的准备
渲染显示
的流程大致包括👇🏻
- 获取颜色转换矩阵
- 创建纹理
- 设置纹理的属性
- 顶点坐标的处理
- 纹理坐标的处理
其中,第1步是为第3步做准备。
关键函数
- 获取颜色转换矩阵 👉🏻
CVBufferGetAttachment
参数释义:
参数1:像素缓存区
参数2:YUV -> RGB
kCVImageBufferYCbCrMatrixKey
参数3:附加模式
一般传值NULL
- 创建纹理缓冲区 👉🏻
CVOpenGLESTextureCacheCreate
参数释义:
参数1:分配器
参数2:缓冲区属性配置信息
(字典类型),一般传NULL
参数3:上下文
EAGLContext
参数4:创建纹理CVOpenGLESTexture
对象所需要的配置信息(字典类型),一般传NULL
参数5:缓冲区输出保存对象的指针
- 创建纹理 👉🏻
CVOpenGLESTextureCacheCreateTextureFromImage
参数释义:
参数1:分配器
参数2:纹理缓冲区
参数3:解码后的流数据
CVPixelBufferRef
参数4:创建纹理对象CVOpenGLESTexture
所需要的配置信息(字典类型),一般传NULL
参数5:纹理类型
,当前只支持GL_TEXTURE_2D
或者 GL_RENDERBUFFER
参数6:颜色组件
参数7:纹理宽度
参数8:纹理高度
参数9:指定像素数据
的格式
。例如GL_RGBA和GL_LUMINANCE
参数10:指定像素数据
的数据类型
。例如GL_UNSIGNED_BYTE
参数11:指定要映射绑定的CVImageBuffer
的索引值
参数12:新创建的纹理对象
将被存储到该参数
3.2.2 显示的核心流程
核心函数介绍完毕后,剩下的就是显示
的核心方法displayPixelBuffer:width:height:
,它大致包含几部分流程👇🏻
部分1:特殊情况的优先处理
- 上下文是否正常
if (!_context || ![EAGLContext setCurrentContext:_context]) {
return;
}
- 解码后的数据为NULL 👉🏻 直接返回
if(pixelBuffer == NULL) {
NSLog(@"Pixel buffer is null");
return;
}
部分2:获取颜色转换矩阵
//获取像素缓存区的PlaneCount
size_t planeCount = CVPixelBufferGetPlaneCount(pixelBuffer);
//CVBufferGetAttachment获取颜色转换矩阵CFTypeRef
CFTypeRef colorAttachments = CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, NULL);
//CFStringCompare比较颜色
if (CFStringCompare(colorAttachments, kCVImageBufferYCbCrMatrix_ITU_R_601_4, 0) == kCFCompareEqualTo) {
_preferredConversion = kColorConversion601;
} else {
_preferredConversion = kColorConversion709;
}
其中,_preferredConversion
是所需要的颜色标准
,获取到颜色转换矩阵CFTypeRef
后,再指定_preferredConversion
是kColorConversion601
(标清)还是kColorConversion709
(高清)👇🏻
// BT.601, which is the standard for SDTV.
static const GLfloat kColorConversion601[] = {
1.164, 1.164, 1.164,
0.0, -0.392, 2.017,
1.596, -0.813, 0.0,
};
// BT.709, which is the standard for HDTV.
static const GLfloat kColorConversion709[] = {
1.164, 1.164, 1.164,
0.0, -0.213, 2.112,
1.793, -0.533, 0.0,
};
部分3:创建纹理
CVReturn err;
CVOpenGLESTextureCacheRef _videoTextureCache;
//从上下文_context中,创建纹理缓冲区,输出到_videoTextureCache中,为创建纹理做准备
err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_videoTextureCache);
if (err != noErr) {
NSLog(@"Error at CVOpenGLESTextureCacheCreate %d", err);
return;
}
//激活纹理
glActiveTexture(GL_TEXTURE0);
接着创建Y纹理
,即亮度纹理
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_videoTextureCache,
pixelBuffer,
NULL,
GL_TEXTURE_2D,
GL_RED_EXT,//颜色组件
frameWidth,
frameHeight,
GL_RED_EXT,
GL_UNSIGNED_BYTE,
0,
&_lumaTexture);
if (err) {
NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
}
//绑定Y纹理
glBindTexture(CVOpenGLESTextureGetTarget(_lumaTexture), CVOpenGLESTextureGetName(_lumaTexture));
//设置纹理的属性👇🏻
//1.放大/缩小的过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//2.环绕方式
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
再创建UV纹理
,即色度纹理 👇🏻
if(planeCount == 2) {
// UV-plane.
glActiveTexture(GL_TEXTURE1);
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_videoTextureCache,
pixelBuffer,
NULL,
GL_TEXTURE_2D,
GL_RG_EXT,
frameWidth / 2,
frameHeight / 2,
GL_RG_EXT,
GL_UNSIGNED_BYTE,
1,
&_chromaTexture);
if (err) {
NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
}
//绑定UV纹理
glBindTexture(CVOpenGLESTextureGetTarget(_chromaTexture), CVOpenGLESTextureGetName(_chromaTexture));
//配置UV纹理的属性
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
部分4: 帧缓存区、着色器和uniforms的准备
//绑定帧缓存区
glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferHandle);
// Set the view port to the entire view.
glViewport(0, 0, _backingWidth, _backingHeight);
//清理屏幕
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// Use shader program.
glUseProgram(self.program);
//传递值
//渲染角度
glUniform1f(uniforms[UNIFORM_ROTATION_ANGLE], 0);
//颜色转换矩阵
glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion);
部分5: 顶点坐标和纹理坐标的处理
首先是顶点坐标
👇🏻
//根据视频的方向和纵横比,来设置最终显示的视频的frame
CGRect viewBounds = self.bounds;
CGSize contentSize = CGSizeMake(frameWidth, frameHeight);
/**
AVMakeRectWithAspectRatioInsideRect 👉🏻 计算纵横比
参数1:size 纵横比
参数2:填充的矩形rect
*/
CGRect vertexSamplingRect = AVMakeRectWithAspectRatioInsideRect(contentSize, viewBounds);
// 计算rect的坐标,用来绘制矩形rect
CGSize normalizedSamplingSize = CGSizeMake(0.0, 0.0);
CGSize cropScaleAmount = CGSizeMake(vertexSamplingRect.size.width/viewBounds.size.width,
vertexSamplingRect.size.height/viewBounds.size.height);
//规范化rect的四个角的坐标点,即将四个角坐标点计算成(-1,1)的范围区间内,因为OpenGL ES的坐标范围是(-1,1)
if (cropScaleAmount.width > cropScaleAmount.height) {
normalizedSamplingSize.width = 1.0;
normalizedSamplingSize.height = cropScaleAmount.height/cropScaleAmount.width;
}
else {
normalizedSamplingSize.width = cropScaleAmount.width/cropScaleAmount.height;
normalizedSamplingSize.height = 1.0;;
}
//以下是把四个角的坐标换算,判断是在4个象限中的哪个象限
//扩展:图像在平面中,根据水平X轴和垂直的Y轴,按照从左至右,从上至下的顺序,可划分成第一、第二、第三和第四象限
GLfloat quadVertexData [] = {
-1 * normalizedSamplingSize.width, -1 * normalizedSamplingSize.height,
normalizedSamplingSize.width, -1 * normalizedSamplingSize.height,
-1 * normalizedSamplingSize.width, normalizedSamplingSize.height,
normalizedSamplingSize.width, normalizedSamplingSize.height,
};
//glVertexAttribPointer 👉🏻 将顶点坐标值传递到OpenGL ES里面
glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, quadVertexData);
glEnableVertexAttribArray(ATTRIB_VERTEX);
接着是纹理坐标
的处理👇🏻
//纹理坐标是倒的,需要翻转
CGRect textureSamplingRect = CGRectMake(0, 0, 1, 1);
GLfloat quadTextureData[] = {
CGRectGetMinX(textureSamplingRect), CGRectGetMaxY(textureSamplingRect),
CGRectGetMaxX(textureSamplingRect), CGRectGetMaxY(textureSamplingRect),
CGRectGetMinX(textureSamplingRect), CGRectGetMinY(textureSamplingRect),
CGRectGetMaxX(textureSamplingRect), CGRectGetMinY(textureSamplingRect)
};
//将纹理坐标值传递到OpenGL ES里面
glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, 0, 0, quadTextureData);
glEnableVertexAttribArray(ATTRIB_TEXCOORD);
部分6:绘制显示数据并到屏幕
//glDrawArrays 👉🏻 绘制数据
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
//glBindRenderbuffer 👉🏻 从渲染缓存区里面取数据,显示到屏幕上
glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
[_context presentRenderbuffer:GL_RENDERBUFFER];
至此,我们完成了视频的显示
!
部分7:清理
因为我们在初始化方法中,设置了kEAGLDrawablePropertyRetainedBacking
是YES 👉🏻 表示视频绘制完成后保留内容,所以需要cleanUpTextaures
清空数据👇🏻
[self cleanUpTextures];
cleanUpTextures
的实现就是清除亮度纹理
和色度纹理
👇🏻
- (void) cleanUpTextures {
if (_lumaTexture) {
CFRelease(_lumaTexture);
_lumaTexture = NULL;
}
if (_chromaTexture) {
CFRelease(_chromaTexture);
_chromaTexture = NULL;
}
}
同时,由于我们的视频是一帧一帧的流数据,实时解析并显示的,所以每一帧的纹理缓存区
也需要清空
👇🏻
CVOpenGLESTextureCacheFlush(_videoTextureCache, 0);
if(_videoTextureCache) {
CFRelease(_videoTextureCache);
}
3.3 dealloc清理
当然,我们封装的图层AAPLEAGLLayer
,也需要在dealloc中清理一些对象,包括_context 上下文
,2个纹理
,解码后的流数据_pixelBuffer
和program
👇🏻
- (void)dealloc {
if (!_context || ![EAGLContext setCurrentContext:_context]) {
return;
}
[self cleanUpTextures];
if(_pixelBuffer) {
CVPixelBufferRelease(_pixelBuffer);
}
if (self.program) {
glDeleteProgram(self.program);
self.program = 0;
}
if(_context) {
//[_context release];
_context = nil;
}
}
总结
- 解码工具类封装
- 初始化
- 工具类初始化 👉🏻 依赖配置类
CCVideoConfig
,创建解码队列
与回调队列
- 解码器
VTDecompressionSessionRef
的初始化,需要以下参数👇🏻- 根据
sps/pps
设置解码的视频输出格式
👉🏻CMVideoFormatDescriptionCreateFromH264ParameterSets
解码器参数
的配置,常用的包括摄像头的输出数据格式
、视频源的分辨率
和零拷贝通道
等- 解码器的
回调设置
,回调是一个结构体VTDecompressionOutputCallbackRecord
- 根据
- 工具类初始化 👉🏻 依赖配置类
- 解码
- 在
解码队列
上异步进行 - 取第5个字节(数据类型type),换算10进制,判断帧类型
sps/pps
👉🏻 只保存不解码,给初始化解码器做准备关键帧
和其他非关键帧
👉🏻 解码前需先判断解码器初始化是否完成
- 解码步骤
- 创建
CMBlockBuffer
- 创建
CMSampleBuffer
VTDecompressionSessionDecodeFrame
异步解码CMSampleBuffer
,且解码后的数据存储在CVPixelBufferRef
中- 解码完成后释放
CMBlockBuffer
和CMSampleBuffer
- 创建
- 在
- 解码的回调
- 获取解码后的数据
sourceFrameRefCon -> CVPixelBufferRef
- 获取
self
👉🏻 桥接转换decompressionOutputRefCon
- 在
回调队列
上异步进行delegate输出 - 最后释放数据
CVImageBufferRef
- 获取解码后的数据
- 初始化
- 渲染显示
- 过程:
视频的渲染-->纹理的渲染-->片元着色器填充-->width*height正方形(渲染2个纹理)
OpenGL ES
跨平台核心 👉🏻OpenGL ES
负责核心的渲染
动作,而编译器才决定显示
- 图层类AAPLEAGLLayer的封装
- 继承关系:继承
CAEAGLLayer
(iOS原生库QuartzCore
里的,是iOS macOS提供的一个专门用来渲染OpenGL ES
的图层),而CAEAGLLayer
继承CALayer
- 初始化
- 图层类的初始化
- scale、透明度、frame、上下文context、颜色转换类型
kColorConversion709
kEAGLDrawablePropertyRetainedBacking
视频绘制完成后是否需要保留其内容
- scale、透明度、frame、上下文context、颜色转换类型
- OpenGL的初始化
- 设置缓存区
- 配置顶点信息
- 配置纹理坐标信息(x, y)
- 创建buffer缓存区:
帧缓存区
和渲染缓存区
- 加载着色器Shaders
片元着色器
和顶点着色器
的处理,顺序:先顶点再片元
glCreateShader
创建Shader +glShaderSource
绑定 +glCompileShader
编译- 链接
glLinkProgram
- uniforms与program 的连接
- 释放shader和program的连接
glUseProgram
使用programglUniform1i
设置Uniform相关的参数:Y纹理
、UV纹理
、渲染角度
和颜色变换矩阵
- 设置缓存区
- 图层类的初始化
- 渲染显示数据
- 实时渲染,需清空之前的
CVPixelBufferRef
数据 - 获取视频帧的宽和高
- 渲染显示流程
- 获取颜色转换矩阵 👉🏻
CVBufferGetAttachment
- 创建纹理
- 创建纹理缓冲区 👉🏻
CVOpenGLESTextureCacheCreate
- 激活纹理 👉🏻
glActiveTexture
- 创建纹理 👉🏻
CVOpenGLESTextureCacheCreateTextureFromImage
- 绑定纹理 👉🏻
glBindTexture
- 配置纹理的属性 👉🏻
glTexParameteri
- 创建纹理缓冲区 👉🏻
- 帧缓存区、着色器和uniforms的准备
- 绑定帧缓存区
glBindFramebuffer
- 清理屏幕
- 使用着色器
glUseProgram
- uniforms的准备:
渲染角度
和颜色转换矩阵
- 绑定帧缓存区
- 顶点坐标和纹理坐标的处理
- 顶点坐标:根据视频帧的宽高绘制矩形,并将矩形的四个角坐标值规范到四个象限之中
- 计算纵横比
AVMakeRectWithAspectRatioInsideRect
- 计算rect的坐标
- 规范化rect的四个角的坐标点,OpenGL ES的坐标范围是(-1,1)
- 坐标换算,对应到4个象限之中
glVertexAttribPointer
glEnableVertexAttribArray
将纹理坐标值传递到OpenGL ES里面
- 计算纵横比
- 纹理坐标
- 纹理坐标是倒的,需要翻转
glVertexAttribPointer
glEnableVertexAttribArray
将纹理坐标值传递到OpenGL ES里面
- 顶点坐标:根据视频帧的宽高绘制矩形,并将矩形的四个角坐标值规范到四个象限之中
- 绘制显示数据并到屏幕
- 绘制数据 👉🏻
glDrawArrays
- 从渲染缓存区里面取数据 👉🏻
glBindRenderbuffer
- 显示到屏幕上 👉🏻
[_context presentRenderbuffer:xxx]
- 绘制数据 👉🏻
- 最后的清理工作
-
释放
亮度纹理
和色度纹理
👉🏻CFRelease
-
清空
纹理缓存区
👉🏻CVOpenGLESTextureCacheFlush
+CFRelease
-
- 获取颜色转换矩阵 👉🏻
- 实时渲染,需清空之前的
- 继承关系:继承
- 过程: