美文网首页音视频编辑
iOS视频 硬编码代码

iOS视频 硬编码代码

作者: mark666 | 来源:发表于2018-07-29 22:33 被阅读333次

    为什么视频可以压缩编码?

    • 存在冗余信息

      • 空间冗余:图像相邻像素之间有较强的相关性
      • 时间冗余:视频序列的相邻图像之间内容相似
      • 视觉冗余:人的视觉系统对某些细节不敏感
      • 其他冗余信息
    • 空间冗余

      • 同一张图像中,有很多像素点表示的信息是完全一样的
      • 如果对每一个像素进行单独的存储,必然会非常浪费空间,也完全没有必要
    • 时间冗余

      • 多张图像之间,有非常多的相关性,由于一些小运动造成了细小差别
      • 如果对每张图像进行单独的像素存储,在下一张图片中又出现了相同的。那么相当于很多像素都存储了多份,必然会非常浪费空间,也是完全没有必要的
    • 视觉冗余

      • 人类视觉系统HVS(Human Visual System)
        • 对高频信息不敏感
        • 对高对比度更敏感
        • 对亮度信息比色度信息更敏感
        • 对运动的信息更敏感
      • 数字视频系统的设计应该考虑HVS的特点:
        • 丢弃高频信息,只编码低频信息
        • 提高边缘信息的主观质量
        • 降低色度的解析度
        • 对感兴趣区域(Region of Interesting,ROI)进行特殊处理

    压缩编码的标准

    • ITU:International Telecommunications Union VECG:Video Coding Experts Group(国际电传视讯联盟)
    • ISO:International Standards Organization MPEG:Motion Picture Experts Group(国际标准组织机构)

    ios8.0 之后 使用VideoToolBox框架
    流程:

    • 采集
    • 获取到视频帧
    • 对视频帧进行编码
    • 获取到视频帧信息
    • 将编码后的数据以NALU方式写入到文件

    视频采集

    视频硬件编码

    • 初始化压缩编码会话(VTCompressionSessionRef)

      • 在VideoToolbox框架的使用过程中,基本都是C语言函数
    • 初始化后通过VTSessionSetProperty设置对象属性

      • 编码方式:H.264编码
      • 帧率:每秒钟多少帧画面
      • 码率:单位时间内保存的数据量
      • 关键帧(GOPsize)间隔:多少帧为一个GOP
    • 准备编码

    - (void)prepareEncodeWithWidth:(int)width height:(int)height{
        //0 定义帧的下标值
        frameIndex = 0;
        //1.创建VTCompressionSessionRef 对象
        //1.创建VTCompressionSessionRef 对象
        // 参数一: CoreFoundation 创建对象的方式 ,NULL -> Default
        // 参数二:编码的视频宽度
        // 参数三: 编码的视频高度
        // 参数四: 编码的标准 H.264/ H.265
        // 参数五 ~ 参数七 NULL
        // 参数八: 编码成功一帧数据后的函数回调
        // 参数九: 回调函数的第一个参数
        //  VTCompressionSessionRef session;
        VTCompressionSessionCreate(kCFAllocatorDefault, 
          width, height, kCMVideoCodecType_H264, 
          NULL, NULL, NULL, 
          compressionCallback, (__bridge void * _Nullable)(self),
                 &_session);
    
        //2.设置VTCompressionSessionRef 属性
       // 2.1 如果是直播,需要设置视频编码是实时输出
        VTSessionSetProperty(self.session, kVTCompressionPropertyKey_RealTime, (__bridge CFTypeRef _Nullable)(@YES));
        // 2.2 设置帧率 (16/24/30)
        // 帧/s
        VTSessionSetProperty(self.session, kVTCompressionPropertyKey_ExpectedFrameRate, (__bridge CFTypeRef _Nullable)(@30));
        //2.3 设置比特率 (码率) bit/s  单位时间的数据量
        VTSessionSetProperty(self.session, kVTCompressionPropertyKey_AverageBitRate, (__bridge CFTypeRef _Nullable)(@(1500000))); // bit
        CFArrayRef dataLimits = (__bridge CFArrayRef)(@[@(1500000/8),@1]); //byte
        VTSessionSetProperty(self.session, kVTCompressionPropertyKey_DataRateLimits, dataLimits);
        // 2.4 设置GOP的大小
        VTSessionSetProperty(self.session, kVTCompressionPropertyKey_MaxKeyFrameInterval, (__bridge CFTypeRef _Nullable)(@(20)));
        //3.准备开始编码
        VTCompressionSessionPrepareToEncodeFrames(self.session);
    }
    

    总结一下常用设置属性:

    • kVTCompressionPropertyKey_RealTime 编码是否实时输出

    • kVTCompressionPropertyKey_ExpectedFrameRate 帧率,也就是一秒输出多少帧图像,16张就可以形成动画,一般默认30

    • kVTCompressionPropertyKey_AverageBitRate 码率,一般是单位时间内的数据量 必须同时设置kVTCompressionPropertyKey_DataRateLimits

    • kVTCompressionPropertyKey_MaxKeyFrameInterval GOP,默认设置20

    • 开始编码

    - (void)encodeFrame:(CMSampleBufferRef)sampleBuffer{
          //1.从CMSampleBufferRef 中获取 CVImageBufferRef
        CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
        
        //利用 VTCompressionSessionRef 编码 CMSampleBufferRef
        //pts(presentationTimeStamp):展示时间戳,用来解码时,计算每一帧时间的
        //dts(DecodeTimeStamp): 解码时间戳,决定该帧在什么时间展示
        frameIndex ++;
        // 第几帧 帧率
        CMTime pts = CMTimeMake(frameIndex, 30);
        
        VTCompressionSessionEncodeFrame(self.session, 
         imageBuffer, pts, kCMTimeInvalid, NULL, NULL, NULL);
    }
    
    编码前后CMSampleBuffer区别

    CMSampleBuffer = CMTime(时间戳) +CMVideoFormatDesc(图片存储方式) + CMBlockBuffer(编码后的数据)

    • 编码成功一帧数据后的函数回调
    void compressionCallback(void * CM_NULLABLE outputCallbackRefCon,
                  void * CM_NULLABLE sourceFrameRefCon,
                  OSStatus status,
                  VTEncodeInfoFlags infoFlags,
                  CM_NULLABLE CMSampleBufferRef sampleBuffer){
        // 0 获取到当前对象
        H264Encoder *encoder = (__bridge H264Encoder *)(outputCallbackRefCon);
        
        // 1.CMSampleBufferRef
        // 2.判断该帧是否是关键帧
        CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
        CFDictionaryRef dict = CFArrayGetValueAtIndex(attachments, 0);
        BOOL iskeyFrame = !CFDictionaryContainsKey(dict, kCMSampleAttachmentKey_NotSync);
        // 3. 如果是关键帧,那么将关键帧写入文件之前,先写入 PPS / SPS数据
        if (iskeyFrame) {
           //3.1 获取参数信息
          CMFormatDescriptionRef format =   CMSampleBufferGetFormatDescription(sampleBuffer);
          //3.2 从format 中获取sps信息
            //
            //参数二 : sps 0 pps 1
            //参数三
            const uint8_t *spsPointer;
            size_t spsSize,spsCount;
            
            CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &spsPointer, &spsSize, &spsCount, NULL);
          //3.3 从format 中获取pps信息
            const uint8_t *ppsPointer;
            size_t ppsSize,ppsCount;
            CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &ppsPointer, &ppsSize, &ppsCount, NULL);
           // 3.4 将sps/pps 写入 NAL单元
            NSData *spsData = [NSData dataWithBytes:spsPointer length:spsSize];
            NSData *ppsData = [NSData dataWithBytes:ppsPointer length:ppsSize];
            [encoder writeData:spsData];
            [encoder writeData:ppsData];
        }
        // 4.将编码后的数据写入文件
        // 4.1 获取CMSampleBufferRef
        CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
        // 4.2 CMSampleBufferRef获取内存地址/长度
        size_t totalLength;
        char *dataPointer;
        CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &totalLength, &dataPointer);
        // 4.3 从dataPointer开始读取数据,并且写入NALU -> slice
        static const int h264HeaderLength = 4;
        size_t offsetLength = 0;
        // 4.4 通过循环,不断的读取slice的切片数据,并且封装成NALU 写入文件
        while (offsetLength < totalLength - h264HeaderLength) {
             // 4.5 读取slice的长度
            uint32_t naluLength;
            memcpy(&naluLength, dataPointer+offsetLength, h264HeaderLength);
            // 4.6 H264 大端字节序/ 小端字节序
            naluLength = CFSwapInt32BigToHost(naluLength);
            // 4.7 根据长度读取字节,并转成NSData
            NSData *data = [NSData dataWithBytes:dataPointer+offsetLength+h264HeaderLength length:naluLength];
            //4.8 写入文件
            [encoder writeData:data];
            //4.9 设置offsetLength
            offsetLength += naluLength + h264HeaderLength;
        }
    }
    

    需要注意的一点是,编码后的数据需要通过切片的方式读取数据,h264已经提供好了切片后的数据,并且默认使用4个字节提供每一个切片的数据的长度,在写入文件时候是不能包括这个4字节长度的。

    • 写入数据
    - (void)writeData:(NSData *)data{
       // NALU 的形式写入
       // NALU 头  0x 表示 16进制的某个数字 x 表示16进制的某个字节
        const char bytes[] = "\x00\x00\x00\x01";
        int headerLength = sizeof(bytes) - 1;
        NSData *headerData = [NSData dataWithBytes:bytes length:headerLength];
        // NALU 体
        [self.fileHandle writeData:headerData];
        [self.fileHandle writeData:data];
    }
    
    • 结束编码
    - (void)endEncoding{
        VTCompressionSessionInvalidate(self.session);
        CFRelease(self.session);
    }
    

    相关文章

      网友评论

        本文标题:iOS视频 硬编码代码

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