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环
- 继续封装解码工具类
 - 拿到解码的流数据之后,渲染显示视频帧画面
 
初始化分为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)
首先是工具类的初始化,和编码器工具类一样,都是依赖配置类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)
- (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)
接下来,我们看看解码回调函数中做了什么?
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)
首先看看初始化,主要就是图层类的初始化 和 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)
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:新创建的纹理对象将被存储到该参数
核心函数介绍完毕后,剩下的就是显示的核心方法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;
    }
}总结
    





