视频H264解码详解(下)(视频H264解码详解下一步)
前言
本篇接着
主要做H264编解码流程中的最后2环
- 继续封装解码工具类
- 拿到解码的流数据之后,渲染显示视频帧画面
一、初始化
初始化分为2个方法执行
- 工具类本身的对外公开的初始化方法 - (instancetype)initWithConfig:(CCVideoConfig*)config;
- 解码器的初始化 这是在解码的时候才做的事情!
- 和编码工具类一样,也是2个异步队列分别做解码和回调
@property (nonatomic, strong) dispatch_queue_t decodeQueue;
@property (nonatomic, strong) dispatch_queue_t callbackQueue;
2. 解码器的初始化和编码器一样,需要解码session
@property (nonatomic) VTDecompressionSessionRef decodeSesion;
3.解码session的创建函数,需依赖SPS/PPS等关键帧的信息,然后输出一个视频帧格式描述
CMVideoFormatDescriptionRef,所以还需要定义以下成员变量
@implementation CCVideoDecoder{
uint8_t *_sps;
NSUInteger _spsSize;
uint8_t *_pps;
NSUInteger _ppsSize;
CMVideoFormatDescriptionRef _decodeDesc; // 视频输出格式
}
C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
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 指向一个变量以接收新的解压会话
C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
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中
C++音视频开发学习路线资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
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个主要流程!
C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
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。
C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
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;"
"}";
2.片元着色器
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 渲染显示数据
C++音视频开发学习资料:点击领取→音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
接下来就是渲染这块的处理,先看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;
}
}