快捷搜索:  汽车  科技

H264解码详解下(H264解码详解下)

H264解码详解下(H264解码详解下)首先是工具类的初始化,和编码器工具类一样,都是依赖配置类CCVideoConfig@implementation CCVideoDecoder{ uint8_t *_sps; NSUInteger _spsSize; uint8_t *_pps; NSUInteger _ppsSize; CMVideoFormatDescriptionRef _DecodeDesc; // 视频输出格式 }C 音视频开发学习资料:点击领取→音视频开发(资料文档 视频教程 面试题)(FFmpeg WebRTC RTMP RTSP HLS RTP)初始化分为2个方法执行@property (nonatomic strong) dispatch_queue_t decodeQueue; @property (nonatomic strong) dispatch_q

前言

本篇接着

视频H264编码详解(上)

视频H264编码详解(中)

主要做H264编解码流程中的最后2环

  1. 继续封装解码工具类
  2. 拿到解码的流数据之后,渲染显示视频帧画面
一、初始化

初始化分为2个方法执行

  • 工具类本身的对外公开的初始化方法 - (instancetype)initWithConfig:(CCVideoConfig*)config;
  • 解码器的初始化 这是在解码的时候才做的事情!
  1. 和编码工具类一样,也是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

H264解码详解下(H264解码详解下)(1)

其参数释义如下

  • 参数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

H264解码详解下(H264解码详解下)(2)

它带有一个指针decompressionOutputCallback,指向帧解压完成后的回调方法,还需要提供可以找到这个回调方法的实例decompressionOutputRefCon。其中VTDecompressionOutputCallback定义如下

H264解码详解下(H264解码详解下)(3)

回调方法包括七个参数

  • 参数1: 回调的引用
  • 参数2: 帧的引用
  • 参数3: 一个状态标识 (包含未定义的代码)
  • 参数4: 指示同步/异步解码,或者解码器是否打算丢帧的标识
  • 参数5: 实际图像的缓冲
  • 参数6: 出现的时间戳
  • 参数7: 出现的持续时间

最后,就是解码session创建函数

H264解码详解下(H264解码详解下)(4)

创建用于解压缩视频帧的会话,解压后的帧将通过调用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个数据结构

H264解码详解下(H264解码详解下)(5)

  • 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里的

H264解码详解下(H264解码详解下)(6)

所以CAEAGLLayer只是个图层,它是iOS macOS提供的一个专门用来渲染OpenGL ES的图层继承CALayer

H264解码详解下(H264解码详解下)(7)

而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]; }

缓存区分帧缓存区 和 渲染缓存区,它们的创建和使用时机

  1. 初始化OpenGL时,需要创建缓存区
  2. 渲染显示数据时,需要从缓存区里取出数据
  • 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的同学应该能写出着色器生成规则,如果是第一次接触,可以先尝试百度搜索了解一下先。我这里就示例写一下

  1. 顶点着色器

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);" "}";

⚠️注意:先编译顶点着色器,再根据顶点着色器编译片元着色器。

H264解码详解下(H264解码详解下)(8)

编译完成后,需要将着色器与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. 获取颜色转换矩阵
  2. 创建纹理
  3. 设置纹理的属性
  4. 顶点坐标的处理
  5. 纹理坐标的处理

其中,第1步是为第3步做准备。

关键函数
  • 获取颜色转换矩阵 CVBufferGetAttachment

H264解码详解下(H264解码详解下)(9)

参数释义:
参数1:像素缓存区
参数2:YUV -> RGB kCVImageBufferYCbCrMatrixKey
参数3:附加模式 一般传值NULL

  • 创建纹理缓冲区 CVOpenGLESTextureCacheCreate

H264解码详解下(H264解码详解下)(10)

参数释义:
参数1:分配器
参数2:缓冲区属性配置信息(字典类型),一般传NULL
参数3:上下文EAGLContext
参数4:创建纹理CVOpenGLESTexture对象所需要的配置信息(字典类型),一般传NULL
参数5:缓冲区输出保存对象的指针

  • 创建纹理 CVOpenGLESTextureCacheCreateTextureFromImage

H264解码详解下(H264解码详解下)(11)

参数释义:
参数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; } }总结

H264解码详解下(H264解码详解下)(12)

猜您喜欢: