美文网首页
三、视频编解码

三、视频编解码

作者: Mjs | 来源:发表于2021-04-01 18:00 被阅读0次

    目前主要的编码方式为h264,h265虽然更好,但是ios11以上才支持,并且cpu负荷比较大

    • 硬编码:基于GPU

      • 视频:VideoToolBox
      • 音频:AudioToolBox
    • 软编码:基于CPU

      • 视频压缩:视频编码MPEG,H264
        X264把视频原数据YUV/RGB编码H264
      • 音频:AudioToolBox
        fdk_aac将音频数据PCM转AAC

    H264基本概念.

    I帧: 关键帧,采用帧内压缩技术.

    • 举个例子,如果摄像头对着你拍摄,1秒之内,实际你发生的变化是非常少的.1秒钟之内实际少很少有大幅度的变化.摄像机一般一秒钟会抓取几十帧的数据.比如像动画,就是25帧/s,一般视频文件都是在30帧/s左右.对于一些要求比较高的,对动作的精细度有要求,想要捕捉到完整的动作的,高级的摄像机一般是60帧/s.那些对于一组帧的它的变化很小.为了便于压缩数据,那怎么办了?将第一帧完整的保存下来.如果没有这个关键帧后面解码数据,是完成不了的.所以I帧特别关键.

    P帧: 向前参考帧.压缩时只参考前一个帧.属于帧间压缩技术.

    • 视频的第一帧会被作为关键帧完整保存下来.而后面的帧会向前依赖.也就是第二帧依赖于第一个帧.后面所有的帧只存储于前一帧的差异.这样就能将数据大大的减少.从而达到一个高压缩率的效果.

    B帧: 双向参考帧,压缩时即参考前一帧也参考后一帧.帧间压缩技术.

    • B帧,即参考前一帧,也参考后一帧.这样就使得它的压缩率更高.存储的数据量更小.如果B帧的数量越多,你的压缩率就越高.这是B帧的优点,但是B帧最大的缺点是,如果是实时互动的直播,那时与B帧就要参考后面的帧才能解码,那在网络中就要等待后面的帧传输过来.这就与网络有关了.如果网络状态很好的话,解码会比较快,如果网络不好时解码会稍微慢一些.丢包时还需要重传.对实时互动的直播,一般不会使用B帧.
    • 如果在泛娱乐的直播中,可以接受一定度的延时,需要比较高的压缩比就可以使用B帧.
    • 如果我们在实时互动的直播,我们需要提高时效性,这时就不能使用B帧了.

    二. GOF(Group of Frame)一组帧

    两个I帧之间形成的一组图片,就是GOP(Group of Picture).
    通常在编码器设置参数时,必须会设置gop_ size 的值其实就是代表2个|帧之间的帧数目.在一个GOP组中容量最大的就是I帧.所以相对而言, gop_ size 设置的越大,整个视频画面质量就会越好.但是解码端必须从接收的第一个|帧开始才可以正确解码出原始图像.否则无法正确解码.


    image

    SPS/PPS

    SPS/PPS实际上就是存储GOP的参数.

    SPS: (Sequence Parameter Set,序列参数集)存放帧数,参考帧数目,解码图像尺寸,帧场编码模式选择标识等.

    • 一组帧的参数集.

    PPS:(Picture Parameter Set,图像参数集).存放熵编码模式选择标识,片组数目,初始量化参数和去方块滤波系数调整标识等.(与图像相关的信息)

    在一组帧之前我们首先收到的是SPS/PPS数据.如果没有这组参数的话,我们是无法解码.

    如果我们在解码时发生错误,首先要检查是否有SPS/PPS.如果没有,是因为对端没有发送过来还是因为对端在发送过程中丢失了.

    SPS/PPS数据,我们也把其归类到I帧.这2组数据是绝对不能丢的.

    那么下面我们来看一下实际开发中遇到的问题.

    视频花屏/卡顿原因

    我们在观看视频时,会遇到花屏或者卡顿现象.那这个与我们刚刚所讲的GOF就息息相关了.

    • 如果GOP分组中的P帧丢失就会造成解码端的图像发生错误.
    • 为了避免花屏问题的发生,一般如果发现P帧或者I帧丢失.就不显示本GOP内的所有帧.只到下一个I帧来后重新刷新图像.
    • 当这时因为没有刷新屏幕.丢包的这一组帧全部扔掉了.图像就会卡在哪里不动.这就是卡顿的原因.

    所以总结起来,花屏是因为你丢了P帧或者I帧.导致解码错误. 而卡顿是因为为了怕花屏,将整组错误的GOP数据扔掉了.直达下一组正确的GOP再重新刷屏.而这中间的时间差,就是我们所感受的卡顿.

    VideoToolBox

    在iOS4.0,苹果就已经支持硬编解码但是硬编解码在当时属于私有API.不提供给开发者使用在2014年的WWDC大会上,iOS 8.0之后,苹果开放了硬编解码的APl。就是VideoToolbox. framework的API。VideoToolbox 是一套纯C语言API。其中包含了很多C语言函数. VideoToolbox . framework是基于Core Foundation库函数,基于C语言

    VideoToolBox框架的流程

    • 创建session
    • 设置编码相关参数
    • 开始编码
    • 循环获取采集数据
    • 获取编码后数据.
    • 将数据写入H264文件
    编码的输入与输出.png

    h264编码采集

    
    #import <AVFoundation/AVFoundation.h>
    #import <VideoToolbox/VideoToolbox.h>
    
    
    @interface ViewController ()<AVCaptureVideoDataOutputSampleBufferDelegate>
    
    @property(nonatomic,strong)UILabel *cLabel;
    @property(nonatomic,strong)AVCaptureSession *cCapturesession;//捕捉会话,用于输入输出设备之间的数据传递
    @property(nonatomic,strong)AVCaptureDeviceInput *cCaptureDeviceInput;//捕捉输入
    @property(nonatomic,strong)AVCaptureVideoDataOutput *cCaptureDataOutput;//捕捉输出
    @property(nonatomic,strong)AVCaptureVideoPreviewLayer *cPreviewLayer;//预览图层
    
    @end
    
    @implementation ViewController
    {
        int  frameID; //帧ID
        dispatch_queue_t cCaptureQueue; //捕获队列
        dispatch_queue_t cEncodeQueue;  //编码队列
        VTCompressionSessionRef cEncodeingSession;//编码session
        CMFormatDescriptionRef format; //编码格式
        NSFileHandle *fileHandele;
        
        
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view, typically from a nib.
        
        //基础UI实现
        _cLabel = [[UILabel alloc]initWithFrame:CGRectMake(20, 20, 200, 100)];
        _cLabel.text = @"cc课堂之H.264硬编码";
        _cLabel.textColor = [UIColor redColor];
        [self.view addSubview:_cLabel];
        
        UIButton *cButton = [[UIButton alloc]initWithFrame:CGRectMake(200, 20, 100, 100)];
        [cButton setTitle:@"play" forState:UIControlStateNormal];
        [cButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [cButton setBackgroundColor:[UIColor orangeColor]];
        [cButton addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:cButton];
        
    
        
    }
    
    
    -(void)buttonClick:(UIButton *)button
    {
        
        //判断_cCapturesession 和 _cCapturesession是否正在捕捉
        if (!_cCapturesession || !_cCapturesession.isRunning ) {
            
            //修改按钮状态
            [button setTitle:@"Stop" forState:UIControlStateNormal];
            
            //开始捕捉
            [self startCapture];
            
            
        }else
        {
            [button setTitle:@"Play" forState:UIControlStateNormal];
            
            //停止捕捉
            [self stopCapture];
        }
        
    }
    
    //开始捕捉
    - (void)startCapture
    {
        self.cCapturesession = [[AVCaptureSession alloc]init];
        
        //设置捕捉分辨率
        self.cCapturesession.sessionPreset = AVCaptureSessionPreset640x480;
        
        //使用函数dispath_get_global_queue去得到队列
        cCaptureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        cEncodeQueue  = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        
        AVCaptureDevice *inputCamera = nil;
        //获取iPhone视频捕捉的设备,例如前置摄像头、后置摄像头......
        NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
        for (AVCaptureDevice *device in devices) {
            
            //拿到后置摄像头
            if ([device position] == AVCaptureDevicePositionBack) {
                
                inputCamera = device;
            }
        }
        
        //将捕捉设备 封装成 AVCaptureDeviceInput 对象
        self.cCaptureDeviceInput = [[AVCaptureDeviceInput alloc]initWithDevice:inputCamera error:nil];
        
        //判断是否能加入后置摄像头作为输入设备
        if ([self.cCapturesession canAddInput:self.cCaptureDeviceInput]) {
            
            //将设备添加到会话中
            [self.cCapturesession addInput:self.cCaptureDeviceInput];
            
            
        }
        
        
        //配置输出
        self.cCaptureDataOutput = [[AVCaptureVideoDataOutput alloc]init];
        
        //设置丢弃最后的video frame 为NO
        [self.cCaptureDataOutput setAlwaysDiscardsLateVideoFrames:NO];
        
        //设置video的视频捕捉的像素点压缩方式为 420
        [self.cCaptureDataOutput setVideoSettings:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] forKey:(id)kCVPixelBufferPixelFormatTypeKey]];
        
        //设置捕捉代理 和 捕捉队列
        [self.cCaptureDataOutput setSampleBufferDelegate:self queue:cCaptureQueue];
        
        //判断是否能添加输出
        if ([self.cCapturesession canAddOutput:self.cCaptureDataOutput]) {
            
            //添加输出
            [self.cCapturesession addOutput:self.cCaptureDataOutput];
        }
        
        //创建连接
        AVCaptureConnection *connection = [self.cCaptureDataOutput connectionWithMediaType:AVMediaTypeVideo];
        
        //设置连接的方向
        [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
        
        //初始化图层
        self.cPreviewLayer = [[AVCaptureVideoPreviewLayer alloc]initWithSession:self.cCapturesession];
        
        //设置视频重力
        [self.cPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspect];
        
        //设置图层的frame
        [self.cPreviewLayer setFrame:self.view.bounds];
        
        //添加图层
        [self.view.layer addSublayer:self.cPreviewLayer];
        
        
        //文件写入沙盒
        NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES)lastObject]stringByAppendingPathComponent:@"cc_video.h264"];
       // NSString *filePath = [NSHomeDirectory()stringByAppendingPathComponent:@"/Documents/cc_video.h264"];
        
        
        //先移除已存在的文件
        [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
        
        //新建文件
        BOOL createFile = [[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
        if (!createFile) {
            
            NSLog(@"create file failed");
        }else
        {
            NSLog(@"create file success");
    
        }
        
        NSLog(@"filePaht = %@",filePath);
        fileHandele = [NSFileHandle fileHandleForWritingAtPath:filePath];
        
        
        //初始化videoToolbBox
        [self initVideoToolBox];
        
        //开始捕捉
        [self.cCapturesession startRunning];
        
        
        
        
    }
    
    
    //停止捕捉
    - (void)stopCapture
    {
        //停止捕捉
        [self.cCapturesession stopRunning];
        
        //移除预览图层
        [self.cPreviewLayer removeFromSuperlayer];
        
        //结束videoToolbBox
        [self endVideoToolBox];
        
        //关闭文件
        [fileHandele closeFile];
        
        fileHandele = NULL;
        
    }
    
    #pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate
    //AV Foundation 获取到视频流
    -(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
    {
        //开始视频录制,获取到摄像头的视频帧,传入encode 方法中
        dispatch_sync(cEncodeQueue, ^{
            [self encode:sampleBuffer];
        });
        
    }
    
    
    
    //初始化videoToolBox
    -(void)initVideoToolBox
    {
        dispatch_sync(cEncodeQueue, ^{
            
            frameID = 0;
            
            int width = 480,height = 640;
            
            //1.调用VTCompressionSessionCreate创建编码session
            //参数1:NULL 分配器,设置NULL为默认分配
            //参数2:width,像素为单位,如果此数据非法,编码会改为合理的值
            //参数3:height
            //参数4:编码类型,如kCMVideoCodecType_H264
            //参数5:NULL encoderSpecification: 编码规范。设置NULL由videoToolbox自己选择
            //参数6:NULL sourceImageBufferAttributes: 源像素缓冲区属性.设置NULL不让videToolbox创建,而自己创建
            //参数7:NULL compressedDataAllocator: 压缩数据分配器.设置NULL,默认的分配
            //参数8:回调  当VTCompressionSessionEncodeFrame被调用压缩一次后会被异步调用.注:当你设置NULL的时候,你需要调用VTCompressionSessionEncodeFrameWithOutputHandler方法进行压缩帧处理,支持iOS9.0以上
            //参数9:outputCallbackRefCon: 回调客户定义的参考值
            //参数10:compressionSessionOut: 编码会话变量
            OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &cEncodeingSession);
            
            NSLog(@"H264:VTCompressionSessionCreate:%d",(int)status);
            
            if (status != 0) {
                
                NSLog(@"H264:Unable to create a H264 session");
                return ;
            }
            /*
            VTSessionSetProperty(VTSessionRef  _Nonnull session, CFStringRef  _Nonnull propertyKey, CFTypeRef  _Nullable propertyValue)
             * 参数设置对象 cEncodeingSession
             
             */
            //设置实时编码输出(避免延迟)
            VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
            
            //舍弃B帧
            VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_ProfileLevel,kVTProfileLevel_H264_Baseline_AutoLevel);
            
            //是否产生B帧(因为B帧在解码时并不是必要的,是可以抛弃B帧的)
            VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_AllowFrameReordering, kCFBooleanFalse);
            
            //设置关键帧(GOPsize)间隔,GOP太小的话图像会模糊,太大视频体积增大
            int frameInterval = 10;
            //VTSessionSetProperty 不能直接设置int/float 作为属性值
            /*
            CFNumberCreate(CFAllocatorRef allocator, CFNumberType theType, const void *valuePtr)
             * allocator : 分配器,一般默认kCFAllocatorDefault
             * theType : 数据类型
             * *valuePtr : 地址
             */
            CFNumberRef frameIntervalRaf = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
            VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRaf);
            
            //设置期望帧率,不是实际帧率
            int fps = 10;
            CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
            VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
            
            //码率的理解:码率大了话就会非常清晰,但同时文件也会比较大。码率小的话,图像有时会模糊,但也勉强能看
            //码率计算公式,参考印象笔记
            //设置码率、上限、单位是bps
            int bitRate = width * height * 3 * 4 * 8;
            CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
            VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
            
            //设置码率,均值,单位是byte
            int bigRateLimit = width * height * 3 * 4;
            CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bigRateLimit);
            VTSessionSetProperty(cEncodeingSession, kVTCompressionPropertyKey_DataRateLimits, bitRateLimitRef);
            
            //开始编码
            VTCompressionSessionPrepareToEncodeFrames(cEncodeingSession);
            
            
        });
        
    }
    
    
    
    - (void) encode:(CMSampleBufferRef )sampleBuffer
    {
        //拿到每一帧未编码数据
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
        
        //设置帧时间,如果不设置会导致时间轴过长。
        CMTime presentationTimeStamp = CMTimeMake(frameID++, 1000);
        //同步,异步
        VTEncodeInfoFlags flags;
        
        //参数1:编码会话变量
        //参数2:未编码数据
        //参数3:获取到的这个sample buffer数据的展示时间戳。每一个传给这个session的时间戳都要大于前一个展示时间戳.
        //参数4:对于获取到sample buffer数据,这个帧的展示时间.如果没有时间信息,可设置kCMTimeInvalid.
        //参数5:frameProperties: 包含这个帧的属性.帧的改变会影响后边的编码帧.一般为null
        //参数6:ourceFrameRefCon: 回调函数会引用你设置的这个帧的参考值. null
        //参数7:infoFlagsOut: 指向一个VTEncodeInfoFlags来接受一个编码操作.如果使用异步运行,kVTEncodeInfo_Asynchronous被设置;同步运行,kVTEncodeInfo_FrameDropped被设置;设置NULL为不想接受这个信息.
        
        OSStatus statusCode = VTCompressionSessionEncodeFrame(cEncodeingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags);
        
        if (statusCode != noErr) {
            
            NSLog(@"H.264:VTCompressionSessionEncodeFrame faild with %d",(int)statusCode);
            
            VTCompressionSessionInvalidate(cEncodeingSession);
            CFRelease(cEncodeingSession);
            cEncodeingSession = NULL;
            return;
        }
        
        NSLog(@"H264:VTCompressionSessionEncodeFrame Success");
        
    }
    
    
    //编码完成回调
    /*
        1.H264硬编码完成后,回调VTCompressionOutputCallback
        2.将硬编码成功的CMSampleBuffer转换成H264码流,通过网络传播
        3.解析出参数集SPS & PPS,加上开始码组装成 NALU。提现出视频数据,将长度码转换为开始码,组成NALU,将NALU发送出去。
     */
    void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer)
    {
        NSLog(@"didCompressH264 called with status %d infoFlags %d",(int)status,(int)infoFlags);
        //状态错误
        if (status != 0) {
            
            return;
        }
        
        //没准备好
        if (!CMSampleBufferDataIsReady(sampleBuffer)) {
            
            NSLog(@"didCompressH264 data is not ready");
            return;
            
        }
        //C转OC
        ViewController *encoder = (__bridge ViewController *)outputCallbackRefCon;
        
        //判断当前帧是否为关键帧
        /* 分步骤判断
        CFArrayRef array = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true);
        CFDictionaryRef dic = CFArrayGetValueAtIndex(array, 0);
        bool isKeyFrame = !CFDictionaryContainsKey(dic, kCMSampleAttachmentKey_NotSync);
        */
        bool keyFrame = !CFDictionaryContainsKey((CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
        
        //判断当前帧是否为关键帧
        //获取sps & pps 数据 只获取1次,保存在h264文件开头的第一帧中
        //sps(sample per second 采样次数/s),是衡量模数转换(ADC)时采样速率的单位,帧的参数信息
        //pps(),单个图像的参数信息
        if (keyFrame) {
            
            //图像存储方式,编码器等格式描述
            CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
            
            //sps
            size_t sparameterSetSize,sparameterSetCount;
            const uint8_t *sparameterSet;
            
            OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
            
            if (statusCode == noErr) {
                
                //获取pps
                size_t pparameterSetSize,pparameterSetCount;
                const uint8_t *pparameterSet;
                
                //从第一个关键帧获取sps & pps
                OSStatus statusCode = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
                
                //获取H264参数集合中的SPS和PPS
                if (statusCode == noErr)
                {
                    //Found pps & sps
                    NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
                    NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
                    
                    if(encoder)
                    {
                        [encoder gotSpsPps:sps pps:pps];
                    }
                }
            }
            
        }
        
        CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
        size_t length,totalLength;
        char *dataPointer;
        /*
         CMBlockBufferGetDataPointer(CMBlockBufferRef  _Nonnull theBuffer, size_t offset, size_t * _Nullable lengthAtOffsetOut, size_t * _Nullable totalLengthOut, char * _Nullable * _Nullable dataPointerOut)
         * theBuffer: 数据源
         * offset : 偏移量
         * lengthAtOffsetOut : 单个数据长度
         * totalLengthOut : 总数据长度
         * dataPointerOut : 数据块首地址
         */
        OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
        if (statusCodeRet == noErr) {
            /*
             大端: 01 23 45 67
             小端: 67 45 23 01
             */
            size_t bufferOffset = 0;
            static const int AVCCHeaderLength = 4;//返回的nalu数据前4个字节不是001的startcode,而是大端模式的帧长度length
            
            //循环获取nalu数据
            while (bufferOffset < totalLength - AVCCHeaderLength) {
                
                uint32_t NALUnitLength = 0;
                
                //读取 一单元长度的 nalu
                memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
                
                //从大端模式转换为系统端模式(小端)
                NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
                
                //获取nalu数据
                NSData *data = [[NSData alloc]initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
                
                //将nalu数据写入到文件
                [encoder gotEncodedData:data isKeyFrame:keyFrame];
                
                //move to the next NAL unit in the block buffer
                //读取下一个nalu 一次回调可能包含多个nalu数据
                bufferOffset += AVCCHeaderLength + NALUnitLength;
                
                
            }
           
        }
        
        
    }
    
    //第一帧写入 sps & pps
    - (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
    {
        NSLog(@"gotSpsPp %d %d",(int)[sps length],(int)[pps length]);
        //写入之前(起始位)
        const char bytes[] = "\x00\x00\x00\x01";
        //去除末尾的/0
        size_t length = (sizeof bytes) - 1;
        
        NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
        
        [fileHandele writeData:ByteHeader];
        [fileHandele writeData:sps];
        [fileHandele writeData:ByteHeader];
        [fileHandele writeData:pps];
        
        
        
    }
    
    
    - (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
    {
        NSLog(@"gotEncodeData %d",(int)[data length]);
        
        if (fileHandele != NULL) {
            
            //添加4个字节的H264 协议 start code 分割符
            //一般来说编码器编出的首帧数据为PPS & SPS
            //H264编码时,在每个NAL前添加起始码 0x000001,解码器在码流中检测起始码,当前NAL结束。
            /*
             为了防止NAL内部出现0x000001的数据,h.264又提出'防止竞争 emulation prevention"机制,在编码完一个NAL时,如果检测出有连续两个0x00字节,就在后面插入一个0x03。当解码器在NAL内部检测到0x000003的数据,就把0x03抛弃,恢复原始数据。
             
             总的来说H264的码流的打包方式有两种,一种为annex-b byte stream format 的格式,这个是绝大部分编码器的默认输出格式,就是每个帧的开头的3~4个字节是H264的start_code,0x00000001或者0x000001。
             另一种是原始的NAL打包格式,就是开始的若干字节(1,2,4字节)是NAL的长度,而不是start_code,此时必须借助某个全局的数据来获得编 码器的profile,level,PPS,SPS等信息才可以解码。
             
             */
            const char bytes[] ="\x00\x00\x00\x01";
            
            //长度
            size_t length = (sizeof bytes) - 1;
            
            //头字节
            NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
            
            //写入头字节
            [fileHandele writeData:ByteHeader];
            
            //写入H264数据
            [fileHandele writeData:data];
            
        }
        
    }
    
    //结束VideoToolBox
    -(void)endVideoToolBox
    {
        //完成
        VTCompressionSessionCompleteFrames(cEncodeingSession, kCMTimeInvalid);
        //释放
        VTCompressionSessionInvalidate(cEncodeingSession);
        CFRelease(cEncodeingSession);
        cEncodeingSession = NULL;
    }
    
    结构.png

    h264解码

    一.解码的思路:

    1. 解析数据(NALU Unit) I/P/B...
    2. 初始化解码器
    3. 将解析后的H264 NALU Unit输入解码器
    4. 解码完成回调,输出解码数据
    5. 解码数据显示(OpenGL ES)

    二.解码三个核心函数:

    1. 创建session, VTDecompressionSessionCreate
    2. 解码一个frame, VTDecompressionSessionDecodeFrame
    3. 销毁解码session, VTDecompressionSessionInvalidate

    三.原理分析:

    • H264原始码流-->NALU.
      • I帧:保留了一张完整视频帧.解码关键!
      • P帧:先前参考帧.差异数据.解码需要依赖于I帧
      • B帧:双向参考帧,解码时既需要|帧,也需要P帧!

    如果H264码流中I帧错误/丢失,就会导致错误传递,P/B帧单独是完成不了解码工作!花屏的现象产生. VideoToolBox硬编码编码H264帧.I帧!手动加入SPS/PPS.
    解码时:需要使用SPS/PPS数据来对解码器进行初始化!

    
    #import <AVFoundation/AVFoundation.h>
    #import <VideoToolbox/VideoToolbox.h>
    
    @interface CCVideoDecoder ()
    @property (nonatomic, strong) dispatch_queue_t decodeQueue;
    @property (nonatomic, strong) dispatch_queue_t callbackQueue;
    /**解码会话*/
    @property (nonatomic) VTDecompressionSessionRef decodeSesion;
    
    @end
    
    @implementation CCVideoDecoder{
        uint8_t *_sps;
        NSUInteger _spsSize;
        uint8_t *_pps;
        NSUInteger _ppsSize;
        CMVideoFormatDescriptionRef _decodeDesc;
    }
    
    /**解码回调函数*/
    /*
     参数1: 回调引用
     参数2: 帧引用
     参数3: 状态标识
     参数4: 同步/异步解码
     参数5: 实际图像缓存
     参数6: 出现时间戳
     参数7: 出现持续时间
     */
    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);
        });
    }
    
    
    - (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;
    }
    
    /*初始化解码器**/
    - (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设置解码参数
         param kCFAllocatorDefault 分配器
         param 2 参数个数
         param parameterSetPointers 参数集指针
         param parameterSetSizes 参数集大小
         param naluHeaderLen nalu nalu start code 的长度 4
         param _decodeDesc 解码器描述
         return 状态
         */
        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;
        }
        
        /*
         解码参数:
        * kCVPixelBufferPixelFormatTypeKey:摄像头的输出数据格式
         kCVPixelBufferPixelFormatTypeKey,已测可用值为
            kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,即420v
            kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,即420f
            kCVPixelFormatType_32BGRA,iOS在内部进行YUV至BGRA格式转换
         YUV420一般用于标清视频,YUV422用于高清视频,这里的限制让人感到意外。但是,在相同条件下,YUV420计算耗时和传输压力比YUV422都小。
         
        * kCVPixelBufferWidthKey/kCVPixelBufferHeightKey: 视频源的分辨率 width*height
         * kCVPixelBufferOpenGLCompatibilityKey : 它允许在 OpenGL 的上下文中直接绘制解码后的图像,而不是从总线和 CPU 之间复制数据。这有时候被称为零拷贝通道,因为在绘制过程中没有解码的图像被拷贝.
         
         */
        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 是一个简单的结构体,它带有一个指针 (decompressionOutputCallback),指向帧解压完成后的回调方法。你需要提供可以找到这个回调方法的实例 (decompressionOutputRefCon)。VTDecompressionOutputCallback 回调方法包括七个参数:
                参数1: 回调的引用
                参数2: 帧的引用
                参数3: 一个状态标识 (包含未定义的代码)
                参数4: 指示同步/异步解码,或者解码器是否打算丢帧的标识
                参数5: 实际图像的缓冲
                参数6: 出现的时间戳
                参数7: 出现的持续时间
         */
        VTDecompressionOutputCallbackRecord callbackRecord;
        callbackRecord.decompressionOutputCallback = videoDecompressionOutputCallback;
        callbackRecord.decompressionOutputRefCon = (__bridge void * _Nullable)(self);
        
        //创建session
        
        /*!
         @function    VTDecompressionSessionCreate
         @abstract    创建用于解压缩视频帧的会话。
         @discussion  解压后的帧将通过调用OutputCallback发出
         @param    allocator  内存的会话。通过使用默认的kCFAllocatorDefault的分配器。
         @param    videoFormatDescription 描述源视频帧
         @param    videoDecoderSpecification 指定必须使用的特定视频解码器.NULL
         @param    destinationImageBufferAttributes 描述源像素缓冲区的要求 NULL
         @param    outputCallback 使用已解压缩的帧调用的回调
         @param    decompressionSessionOut 指向一个变量以接收新的解压会话
         */
        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;
    }
    
    /**解码函数(private)*/
    - (CVPixelBufferRef)decode:(uint8_t *)frame withSize:(uint32_t)frameSize {
        // CVPixelBufferRef 解码后的数据,编码之前源视频数据
        // CMBlockBufferRef 编码之后的数据
        CVPixelBufferRef outputPixelBuffer = NULL;
        CMBlockBufferRef blockBuffer = NULL;
        CMBlockBufferFlags flag0 = 0;
        
        //创建blockBuffer
        /*!
         参数1: structureAllocator kCFAllocatorDefault  默认内存分配
         参数2: memoryBlock  frame    内容
         参数3: frame size
         参数4: blockAllocator: Pass NULL
         参数5: customBlockSource Pass NULL
         参数6: offsetToData  数据偏移
         参数7: dataLength 数据长度
         参数8: flags 功能和控制标志
         参数9: newBBufOut 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
        /*
         参数1: allocator 分配器,使用默认内存分配, kCFAllocatorDefault
         参数2: blockBuffer.需要编码的数据blockBuffer.不能为NULL
         参数3: formatDescription,视频输出格式
         参数4: numSamples.CMSampleBuffer 个数.
         参数5: numSampleTimingEntries 必须为0,1,numSamples 
         参数6: sampleTimingArray.  数组.为空
         参数7: numSampleSizeEntries 默认为1
         参数8: sampleSizeArray
         参数9: 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;
        //解码数据
        /*
         参数1: 解码session
         参数2: 源数据 包含一个或多个视频帧的CMsampleBuffer
         参数3: 解码标志
         参数4: 解码后数据outputPixelBuffer
         参数5: 同步/异步解码标识
         */
        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;
    }
    
    // private
    - (void)decodeNaluData:(uint8_t *)frame size:(uint32_t)size {
        //数据类型:frame的前4个字节是NALU数据的开始码,也就是00 00 00 01,
        // 第5个字节是表示数据类型,转为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
                _spsSize = naluSize;
                _sps = malloc(_spsSize);
                memcpy(_sps, &frame[4], _spsSize);
                break;
            case 0x08: //pps
                _ppsSize = naluSize;
                _pps = malloc(_ppsSize);
                memcpy(_pps, &frame[4], _ppsSize);
                break;
            default: //其他帧(1-5)
                if ([self initDecoder]) {
                    pixelBuffer = [self decode:frame withSize:size];
                }
                break;
        }
    }
    
    // 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];
        });
    }
    
    //销毁
    - (void)dealloc
    {
        if (_decodeSesion) {
            VTDecompressionSessionInvalidate(_decodeSesion);
            CFRelease(_decodeSesion);
            _decodeSesion = NULL;
        }
        
    }
    
    /**
     nal_unit_type  NAL类型                         C
     0              未使用
     1              非IDR图像中不采用数据划分的片段     2,3,4
     2              非IDR图像中A类数据划分片段         2
     3              非IDR图像中B类数据划分片段         3
     4              非IDR图像中C类数据划分片段         4
     5              IDR图像的片                     2,3
     6              补充增强信息单元(SEI)           5
     7              序列参数集                       0
     8              图像参数集                       1
     9              分界符                          6
     10             序列结束                         7
     11             码流结束                        8
     12             填充                            9
     13..23         保留
     
     24..31        不保留(RTP打包时会用到)
     
     
     NSString * const naluTypesStrings[] =
     {
     @"0: Unspecified (non-VCL)",
     @"1: Coded slice of a non-IDR picture (VCL)",    // P frame
     @"2: Coded slice data partition A (VCL)",
     @"3: Coded slice data partition B (VCL)",
     @"4: Coded slice data partition C (VCL)",
     @"5: Coded slice of an IDR picture (VCL)",      // I frame
     @"6: Supplemental enhancement information (SEI) (non-VCL)",
     @"7: Sequence parameter set (non-VCL)",         // SPS parameter
     @"8: Picture parameter set (non-VCL)",          // PPS parameter
     @"9: Access unit delimiter (non-VCL)",
     @"10: End of sequence (non-VCL)",
     @"11: End of stream (non-VCL)",
     @"12: Filler data (non-VCL)",
     @"13: Sequence parameter set extension (non-VCL)",
     @"14: Prefix NAL unit (non-VCL)",
     @"15: Subset sequence parameter set (non-VCL)",
     @"16: Reserved (non-VCL)",
     @"17: Reserved (non-VCL)",
     @"18: Reserved (non-VCL)",
     @"19: Coded slice of an auxiliary coded picture without partitioning (non-VCL)",
     @"20: Coded slice extension (non-VCL)",
     @"21: Coded slice extension for depth view components (non-VCL)",
     @"22: Reserved (non-VCL)",
     @"23: Reserved (non-VCL)",
     @"24: STAP-A Single-time aggregation packet (non-VCL)",
     @"25: STAP-B Single-time aggregation packet (non-VCL)",
     @"26: MTAP16 Multi-time aggregation packet (non-VCL)",
     @"27: MTAP24 Multi-time aggregation packet (non-VCL)",
     @"28: FU-A Fragmentation unit (non-VCL)",
     @"29: FU-B Fragmentation unit (non-VCL)",
     @"30: Unspecified (non-VCL)",
     @"31: Unspecified (non-VCL)",
     };
     */
    
    
    @end
    

    渲染

    通过OpenGL渲染

    
    #import <AVFoundation/AVUtilities.h>
    #import <mach/mach_time.h>
    #include <AVFoundation/AVFoundation.h>
    #import <UIKit/UIScreen.h>
    #include <OpenGLES/EAGL.h>
    #include <OpenGLES/ES2/gl.h>
    #include <OpenGLES/ES2/glext.h>
    
    // Uniform index.
    enum
    {
        UNIFORM_Y,
        UNIFORM_UV,
        UNIFORM_ROTATION_ANGLE,
        UNIFORM_COLOR_CONVERSION_MATRIX,
        NUM_UNIFORMS
    };
    GLint uniforms[NUM_UNIFORMS];
    
    // Attribute index.
    enum
    {
        ATTRIB_VERTEX,
        ATTRIB_TEXCOORD,
        NUM_ATTRIBUTES
    };
    
    // Color Conversion Constants (YUV to RGB) including adjustment from 16-235/16-240 (video range)
    
    // 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,
    };
    
    
    
    @interface AAPLEAGLLayer ()
    {
        // The pixel dimensions of the CAEAGLLayer.
        GLint _backingWidth;
        GLint _backingHeight;
        
        EAGLContext *_context;
        CVOpenGLESTextureRef _lumaTexture;
        CVOpenGLESTextureRef _chromaTexture;
        
        GLuint _frameBufferHandle;
        GLuint _colorBufferHandle;
        
        const GLfloat *_preferredConversion;
    }
    @property GLuint program;
    
    @end
    @implementation AAPLEAGLLayer
    @synthesize pixelBuffer = _pixelBuffer;
    
    -(CVPixelBufferRef) pixelBuffer
    {
        return _pixelBuffer;
    }
    
    - (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];
    }
    
    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [super init];
        if (self) {
            CGFloat scale = [[UIScreen mainScreen] scale];
            self.contentsScale = scale;
            
            self.opaque = TRUE;
            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;
            }
            
            // Set the default conversion to BT.709, which is the standard for HDTV.
            _preferredConversion = kColorConversion709;
            
            [self setupGL];
        }
        
        return self;
    }
    
    - (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer width:(uint32_t)frameWidth height:(uint32_t)frameHeight
    {
        if (!_context || ![EAGLContext setCurrentContext:_context]) {
            return;
        }
        
        if(pixelBuffer == NULL) {
            NSLog(@"Pixel buffer is null");
            return;
        }
        
        CVReturn err;
        
        size_t planeCount = CVPixelBufferGetPlaneCount(pixelBuffer);
        
        /*
         Use the color attachment of the pixel buffer to determine the appropriate color conversion matrix.
         */
        CFTypeRef colorAttachments = CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, NULL);
        
        if (CFStringCompare(colorAttachments, kCVImageBufferYCbCrMatrix_ITU_R_601_4, 0) == kCFCompareEqualTo) {
            _preferredConversion = kColorConversion601;
        }
        else {
            _preferredConversion = kColorConversion709;
        }
        
        /*
         CVOpenGLESTextureCacheCreateTextureFromImage will create GLES texture optimally from CVPixelBufferRef.
         */
        
        /*
         Create Y and UV textures from the pixel buffer. These textures will be drawn on the frame buffer Y-plane.
         */
        
        CVOpenGLESTextureCacheRef _videoTextureCache;
        
        // Create CVOpenGLESTextureCacheRef for optimal CVPixelBufferRef to GLES texture conversion.
        err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_videoTextureCache);
        if (err != noErr) {
            NSLog(@"Error at CVOpenGLESTextureCacheCreate %d", err);
            return;
        }
        
        glActiveTexture(GL_TEXTURE0);
        
        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);
        }
        
        glBindTexture(CVOpenGLESTextureGetTarget(_lumaTexture), CVOpenGLESTextureGetName(_lumaTexture));
        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);
        
        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);
            }
            
            glBindTexture(CVOpenGLESTextureGetTarget(_chromaTexture), CVOpenGLESTextureGetName(_chromaTexture));
            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);
        }
        
        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_LUMA_THRESHOLD], 1);
        //    glUniform1f(uniforms[UNIFORM_CHROMA_THRESHOLD], 1);
        glUniform1f(uniforms[UNIFORM_ROTATION_ANGLE], 0);
        glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion);
        
        // Set up the quad vertices with respect to the orientation and aspect ratio of the video.
        CGRect viewBounds = self.bounds;
        CGSize contentSize = CGSizeMake(frameWidth, frameHeight);
        CGRect vertexSamplingRect = AVMakeRectWithAspectRatioInsideRect(contentSize, viewBounds);
        
        // Compute normalized quad coordinates to draw the frame into.
        CGSize normalizedSamplingSize = CGSizeMake(0.0, 0.0);
        CGSize cropScaleAmount = CGSizeMake(vertexSamplingRect.size.width/viewBounds.size.width,
                                            vertexSamplingRect.size.height/viewBounds.size.height);
        
        // Normalize the quad vertices.
        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;;
        }
        
        /*
         The quad vertex data defines the region of 2D plane onto which we draw our pixel buffers.
         Vertex data formed using (-1,-1) and (1,1) as the bottom left and top right coordinates respectively, covers the entire screen.
         */
        GLfloat quadVertexData [] = {
            -1 * normalizedSamplingSize.width, -1 * normalizedSamplingSize.height,
            normalizedSamplingSize.width, -1 * normalizedSamplingSize.height,
            -1 * normalizedSamplingSize.width, normalizedSamplingSize.height,
            normalizedSamplingSize.width, normalizedSamplingSize.height,
        };
        
        // Update attribute values.
        glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, quadVertexData);
        glEnableVertexAttribArray(ATTRIB_VERTEX);
        
        /*
         The texture vertices are set up such that we flip the texture vertically. This is so that our top left origin buffers match OpenGL's bottom left texture coordinate system.
         */
        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)
        };
        
        glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, 0, 0, quadTextureData);
        glEnableVertexAttribArray(ATTRIB_TEXCOORD);
        
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
        
        glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
        [_context presentRenderbuffer:GL_RENDERBUFFER];
        
        [self cleanUpTextures];
        // Periodic texture cache flush every frame
        CVOpenGLESTextureCacheFlush(_videoTextureCache, 0);
        
        if(_videoTextureCache) {
            CFRelease(_videoTextureCache);
        }
    }
    
    # pragma mark - OpenGL setup
    
    - (void)setupGL
    {
        if (!_context || ![EAGLContext setCurrentContext:_context]) {
            return;
        }
        
        [self setupBuffers];
        [self loadShaders];
        
        glUseProgram(self.program);
        
        // 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);
    }
    
    #pragma mark - Utilities
    
    - (void)setupBuffers
    {
        glDisable(GL_DEPTH_TEST);
        
        glEnableVertexAttribArray(ATTRIB_VERTEX);
        glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
        
        glEnableVertexAttribArray(ATTRIB_TEXCOORD);
        glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
        
        [self createBuffers];
    }
    
    - (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));
        }
    }
    
    - (void) releaseBuffers
    {
        if(_frameBufferHandle) {
            glDeleteFramebuffers(1, &_frameBufferHandle);
            _frameBufferHandle = 0;
        }
        
        if(_colorBufferHandle) {
            glDeleteRenderbuffers(1, &_colorBufferHandle);
            _colorBufferHandle = 0;
        }
    }
    
    - (void) resetRenderBuffer
    {
        if (!_context || ![EAGLContext setCurrentContext:_context]) {
            return;
        }
        
        [self releaseBuffers];
        [self createBuffers];
    }
    
    - (void) cleanUpTextures
    {
        if (_lumaTexture) {
            CFRelease(_lumaTexture);
            _lumaTexture = NULL;
        }
        
        if (_chromaTexture) {
            CFRelease(_chromaTexture);
            _chromaTexture = NULL;
        }
    }
    
    #pragma mark -  OpenGL ES 2 shader compilation
    
    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);"
    "}";
    
    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;"
    "}";
    
    - (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;
        }
        
        // Attach vertex shader to program.
        glAttachShader(self.program, vertShader);
        
        // Attach fragment shader to program.
        glAttachShader(self.program, fragShader);
        
        // Bind attribute locations. This needs to be done prior to linking.
        glBindAttribLocation(self.program, ATTRIB_VERTEX, "position");
        glBindAttribLocation(self.program, ATTRIB_TEXCOORD, "texCoord");
        
        // Link the program.
        if (![self linkProgram:self.program]) {
            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;
        }
        
        // Get uniform locations.
        uniforms[UNIFORM_Y] = glGetUniformLocation(self.program, "SamplerY");
        uniforms[UNIFORM_UV] = glGetUniformLocation(self.program, "SamplerUV");
        //    uniforms[UNIFORM_LUMA_THRESHOLD] = glGetUniformLocation(self.program, "lumaThreshold");
        //    uniforms[UNIFORM_CHROMA_THRESHOLD] = glGetUniformLocation(self.program, "chromaThreshold");
        uniforms[UNIFORM_ROTATION_ANGLE] = glGetUniformLocation(self.program, "preferredRotation");
        uniforms[UNIFORM_COLOR_CONVERSION_MATRIX] = glGetUniformLocation(self.program, "colorConversionMatrix");
        
        // Release vertex and fragment shaders.
        if (vertShader) {
            glDetachShader(self.program, vertShader);
            glDeleteShader(vertShader);
        }
        if (fragShader) {
            glDetachShader(self.program, fragShader);
            glDeleteShader(fragShader);
        }
        
        return YES;
    }
    
    - (BOOL)compileShaderString:(GLuint *)shader type:(GLenum)type shaderString:(const GLchar*)shaderString
    {
        *shader = glCreateShader(type);
        glShaderSource(*shader, 1, &shaderString, NULL);
        glCompileShader(*shader);
        
    #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;
    }
    
    - (BOOL)compileShader:(GLuint *)shader type:(GLenum)type URL:(NSURL *)URL
    {
        NSError *error;
        NSString *sourceString = [[NSString alloc] initWithContentsOfURL:URL encoding:NSUTF8StringEncoding error:&error];
        if (sourceString == nil) {
            NSLog(@"Failed to load vertex shader: %@", [error localizedDescription]);
            return NO;
        }
        
        const GLchar *source = (GLchar *)[sourceString UTF8String];
        
        return [self compileShaderString:shader type:type shaderString:source];
    }
    
    - (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;
    }
    
    - (BOOL)validateProgram:(GLuint)prog
    {
        GLint logLength, status;
        
        glValidateProgram(prog);
        glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
        if (logLength > 0) {
            GLchar *log = (GLchar *)malloc(logLength);
            glGetProgramInfoLog(prog, logLength, &logLength, log);
            NSLog(@"Program validate log:\n%s", log);
            free(log);
        }
        
        glGetProgramiv(prog, GL_VALIDATE_STATUS, &status);
        if (status == 0) {
            return NO;
        }
        
        return YES;
    }
    
    - (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;
        }
        //[super dealloc];
    }
    @end
    

    相关文章

      网友评论

          本文标题:三、视频编解码

          本文链接:https://www.haomeiwen.com/subject/sbzwhltx.html