iOS视频 硬编码代码

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

  • 存在冗余信息

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

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

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

    • 人类视觉系统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方式写入到文件

视频采集

iOS视频 硬编码代码_第1张图片

视频硬件编码

  • 初始化压缩编码会话(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);
}
iOS视频 硬编码代码_第2张图片
编码前后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视频 硬编码代码)