VideoToolbox使用说明

使用VideoToolbox硬编&硬解

VideoToolbox简介

VideoToolbox 是一个低级的框架,可直接访问硬件的编解码器。能够为视频提供压缩和解压缩的服务,同时也提供存储在 CoreVideo 像素缓冲区的图像进行格式的转换。

优点

  • 利用GPU或者专用处理器对视频流进行编解码,不用大量占用CPU资源。性能高,很好的实时性。

缺点

  • 低码率下通常质量低于软编

VideoToolbox数据

  1. CVPixelBuffer

    // CVPixelBuffer 与 CVImageBuffer 类型相同
    typealias CVPixelBuffer = CVImageBuffer
    

    CVPixelBuffer 是存储在内存中的一个未压缩的光栅图像 Buffer,包括图像的宽度、高度等。

  2. CMBlockBuffer

    CMBlockBuffer 是一个任意的 Buffer,相当于 Buffer 中的 Any. 在管道中压缩视频的时候,会把它包装成 CMBlockBuffer。相当于 CMBlockBuffer 代表着一个压缩的数据。

  3. CMSampleBuffer

    CMSampleBuffer 可能是一个压缩的数据,也可能是一个未压缩的数据。取决于 CMSampleBuffer 里面是 CMBlockBuffer(压缩后) 还是 CVPixelBuffer(未压缩)

对于VideoToolbox,可以通过直接访问硬编解码器,将 H.264 文件或传输流转换为 iOS上的 CMSampleBuffer 并解码成 CVPixelBuffer, 或将未压缩的 CVPixelBuffer 编码成 CMSampleBuffer(将未编码的CMSampleBuffer(CVPixelBuffer)与已编码的CMSampleBuffer(CMBlockBuffer)的相互转换):

解码

  • H.264 -> CMSampleBuffer -> CVPixelBuffer

编码:

  • CVPixelBuffer -> CMSampleBuffer -> H.264

解码

把原始码流包装成 CMSampleBuffer

解码前的原始数据为H264码流,iOS可以使用 NSInputStream 读取H264文件。

H264 有两种封装格式,一种为 MP4 格式,一种是annexb格式。MP4格式是以NALU的长度分割;annexb格式是以 0x00000001 或 0x0000000001 分割。

VideoToolbox解码使用的 H264 为MP4格式,因此需要替换NALU的Header

  • 使用 CMVideoFormatDescriptionCreateFromH264ParameterSets 将 SPS 和 PPS 封装成 CMVideoFormatDescription

    typealias CMVideoFormatDescription = CMFormatDescription
    
  • 修改 NALU 的 Header

    NALU 只要有两种格式:Annex B 和 AVCC。Annex B 格式以 0x 00 00 01 或 0x 00 00 00 01 开头, AVCC 格式以所在 NALU 的长度开头。

    替换掉NALU 的 StartCode

  • 使用 CMBlockBufferCreateWithMemoryBlock 接口将 NALU unit 封装成 CMBlockBuffer

  • 通过 CMSampleBufferCreate 将 CMBlockBuffer + CMVideoFormatDescription + CMTime 创建成 CMSampleBuffer

解码流程:

  1. 使用 VTDecompressionSessionCreate 创建解码会话

    VT_EXPORT OSStatus 
    VTDecompressionSessionCreate(
        // 会话的分配器,默认使用kCFAllocatorDefault 
     CM_NULLABLE CFAllocatorRef allocator,
        // 源视频帧的描述(包含SPS & PPS 信息)
     CM_NONNULL CMVideoFormatDescriptionRef videoFormatDescription,
        // 视频解码器(默认为空,由 VideoToolbox 选择)
     CM_NULLABLE CFDictionaryRef videoDecoderSpecification,
        // 包含解码配置信息的数组
     CM_NULLABLE CFDictionaryRef destinationImageBufferAttributes,
        // 回调函数
     const VTDecompressionOutputCallbackRecord * CM_NULLABLE outputCallback,
        // 解码会话对象的指针
     CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTDecompressionSessionRef * CM_NONNULL decompressionSessionOut)
    
  2. 使用 VTSessionSetProperty 设置会话设置

    VTSessionSetProperty(
      // 解码会话
      CM_NONNULL VTSessionRef       session,
      // 属性 KEY
      CM_NONNULL CFStringRef        propertyKey,
      // 设置的属性值
      CM_NULLABLE CFTypeRef         propertyValue )
    
  3. 使用 VTDecompressionSessionDecodeFrame 编码视频帧,在之前设置的回调函数中获取编码后的结果

    VT_EXPORT OSStatus
    VTDecompressionSessionDecodeFrame(
        // 解码会话
     CM_NONNULL VTDecompressionSessionRef    session,
        // 要解码的视频数据(包含一个或多个视频帧)
     CM_NONNULL CMSampleBufferRef            sampleBuffer,
        // 解码器和解码会话的指令
     VTDecodeFrameFlags                      decodeFlags, 
        // 解码后的数据
     void * CM_NULLABLE                      sourceFrameRefCon,
     VTDecodeInfoFlags * CM_NULLABLE         infoFlagsOut)
    

    回调函数返回数据

    typedef void (*VTDecompressionOutputCallback)(
         // VTDecompressionOutputCallbackRecord 的 decompressionOutputRefCon字段值
         void * CM_NULLABLE decompressionOutputRefCon,
         // 解码返回的数据
         void * CM_NULLABLE sourceFrameRefCon,
         // 错误码
         OSStatus status, 
         // 解码操作的信息
         VTDecodeInfoFlags infoFlags,
         // 包含解压缩的帧数据
         CM_NULLABLE CVImageBufferRef imageBuffer,
         // 帧数据的时间戳
         CMTime presentationTimeStamp, 
         // 帧数据的表示时间
         CMTime presentationDuration );
    
  4. 使用 VTCompressionSessionCompleteFrames 强制结束并完成编码

  5. 编码完成后使用 VTCompressionSessionInvalidate 结束编码,并释放内存

编码

  1. 使用 VTDecompressionSessionCreate 创建 session(编码会话)

    VTCompressionSessionCreate(
        // 分配器,传NULL或KCFAllocatorDefault
     CM_NULLABLE CFAllocatorRef      allocator,
        // 宽度
     int32_t     width,
        // 高度
     int32_t     height,
        // 编码类型
     CMVideoCodecType  codecType,
        // 编码规范 传NULL,videotoolbox自行选择
     CM_NULLABLE CFDictionaryRef     encoderSpecification,
        // 源像素缓冲区
     CM_NULLABLE CFDictionaryRef     sourceImageBufferAttributes,
        // 压缩数据分配器
     CM_NULLABLE CFAllocatorRef      compressedDataAllocator,
        // 回调函数
     CM_NULLABLE VTCompressionOutputCallback             outputCallback,
        // 回调函数的引用
     void * CM_NULLABLE              outputCallbackRefCon,
        // 编码会话对象指针
     CM_RETURNS_RETAINED_PARAMETER CM_NULLABLE VTCompressionSessionRef * CM_NONNULL compressionSessionOut) 
    
  2. VTSessionSetProperty 配置相关属性

    设置一些例如码率、帧率、分辨率等属性

    • FPS(Frames PerSecond):每秒刷新的帧数。帧数越高,流畅度越高
    • 分辨率
    • 比特率/码率:表示经过编码(压缩)后的视频数据每秒钟需要用多少个比特来表示。比特率越高,视频的质量就越好;但编码后的文件也就越大。
  3. VTCompressionSessionPrepareToEncodeFrames 准备编码

    VTCompressionSessionPrepareToEncodeFrames(self.session);
    
  4. 调用VTCompressionSessionEncodeFrame传入需要编码的视频帧

    VTCompressionSessionEncodeFrame(
        // 编码会话
     CM_NONNULL VTCompressionSessionRef  session,
        // 要编码的数据
     CM_NONNULL CVImageBufferRef         imageBuffer,
        // 时间戳
     CMTime                              presentationTimeStamp,
        // 表示时间(may be kCMTimeInvalid)
     CMTime                              duration,
        // 数据的其他属性(key-value)
     CM_NULLABLE CFDictionaryRef         frameProperties,
        // 帧数据的引用,将被传递给回调函数
     void * CM_NULLABLE                  sourceFrameRefCon,
     VTEncodeInfoFlags * CM_NULLABLE     infoFlagsOut )
    
  5. 执行编码回调函数 VTCompressionOutputCallback

    如果是关键帧调用 CMSampleBufferGetFormatDescription 获取 CMFormatDescriptionRef,;

    然后用CMVideoFormatDescriptionGetH264ParameterSetAtIndex取得PPS和SPS;

    最后把每一帧的所有NALU数据前四个字节变成 0X00,00,00,01 之后再写入文件

    void didCompressionOutputCallback(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
         //获取传入的参数
        VideoEncode *encode = (__bridge VideoEncode *)outputCallbackRefCon;
        
        //判断是否是关键帧
        CFArrayRef arrayRef = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true,);
    }
    
  6. 结束编码

    调用编码完成函数,将编码会话销毁,释放资源

    VTCompressionSessionCompleteFrames(session, KCMTimeInvalid);
    VTCompressionSessionInvalidate(session);
    CFRelease(session);
    session = NULL;
    frameID = 0;
    

读取H264文件,解码然后编码的Demo

推荐阅读更多精彩内容