美文网首页FFmpeg与音视频流媒体工程开发相关
H264编解码(二) —— ios中的H264硬编解码的实现

H264编解码(二) —— ios中的H264硬编解码的实现

作者: 刀客传奇 | 来源:发表于2017-12-24 12:32 被阅读193次

    版本记录

    版本号 时间
    V1.0 2017.12.23

    前言

    对于做过视频的开发者,大家应该对H264编码都不陌生,接下来这几篇就详细的解析一下H264编码方面的相关知识。感兴趣的可以看这几篇文章。
    1. H264编码(一) —— 基本概览

    视频H264编解码数据结构

    下面我们看一下H264编码前后数据结构,如下图所示。

    下图为H264解码前后数据结构示意图,这里有几个对象需要说明一下。

    • CVPixelBuffer
      • 解码后的图像数据结构。
    • CMTime、CMClock和CMTimebase
      • 这个和时间戳相关,可能是32或者64位的形式。
    • CMBlockBuffer
      • 编码后的图像数据结构
    • CMVideoFormatDescription
      • 这里面存放的就是图像存储方式,编解码器等格式描述。
    • CMSampleBuffer
      • 这里面存放编解码前后的视频图像的容器数据结构。

    从上图中可以看出来:

    • 编解码前后的视频数据封装在CMSampleBuffer中。
    • 编码后的图像存储方式为CMBlockBuffer
    • 解码后的图像存储方式为CVPixelBuffer
    • CMSampleBuffer中还存储和时间已经描述相关的信息。

    具体上面几个对象怎么在代码中使用,后续会加上使用方法的Demo。


    硬编码和软编码优缺点

    利用CPU做视频的编码和解码,称为软编软解。该方法比较通用,但是占用CPU资源,编解码效率不高。

    一般系统都会提供GPU或者专用处理器来对视频流进行编解码,也就是硬件编码和解码。苹果在iOS 8.0系统之前,没有开放系统的硬件编码解码功能,不过Mac OS系统一直有,被称为Video ToolBox的框架来处理硬件的编码和解码,终于在iOS 8.0后,苹果将该框架引入iOS系统。

    硬编码具有很大的优势,它不像软编码大量占用CPU资源。可以更好的利用GPU以及专门的视频编解码芯片的高性能,可以实现很好的实时性。其实,对于VFoundation也使用硬件对视频进行硬件编解码,但是编码后直接写入文件,解码后就直接显示了。而使用Video Toolbox框架可以得到编码后的帧结构,也可以得到解码后的原始图像,因此具有更大的灵活性做一些视频图像处理。也就是说使用Video Toolbox框架更加灵活,方便进一步进行视频处理。


    硬解码

    硬解码其实就是从服务端下载视频数据,但是是编码后的,在客户端呈现出来视频数据之前,需要进行解码,然后才可以拿出来图像像素数据进行显示。下面我们先看一下硬编码相关原理及理论,先看一张图。

    1. 将H264码流转换为解码前CMSampleBuffer对象

    由前面的内容我们知道,解码前的CMSampleBuffer对象,包括CMTime、CMVideoFormatDesc、CMBlockBuffer等,我们解码的任务就是从H264码流里面提取上面三处的信息,合成解码后的CMSampleBuffer对象,提供给硬解码接口进行解码工作。

    H264码流由NALU单元组成,NALU单元包含视频图像数据CMBlockBuffer和H264的参数信息则可以组合成FormatDesc,具体参数信息包含SPS(Sequence Parameter Set)PPS(Picture Parameter Set),如下图所示为H264的码流结构。

    还可以看下面这个示意图

    H264码流结构

    下面我们就看一下这个解析过程。

    • 提取spspps生成format description
      • 每个NALU开始码位0x000001,按照开始码定位NALU
      • 通过类型信息找到sps和pps,开始码后的第一个byte的后5位,7代表sps,8代表pps。
    //sps
    _spsSize =format.getCsd_0_size()-4;_sps = (uint8_t *)malloc(_spsSize);memcpy(_sps,format.getCsd_0()+4, _spsSize);
    
    //pps
    _ppsSize =format.getCsd_1_size()-4;_pps = (uint8_t *)malloc(_ppsSize);memcpy(_pps,format.getCsd_1()+4, _ppsSize);
    
    • 利用函数CMVideoFormatDescriptionCreateFromH264ParameterSets来构建CMVideoFormatDescriptionRef,以获取描述信息。

    • 提取视频数据生成待解码对象CMBlockBuffer

      • 通过上面提到的开始码,定位到NALU
      • 确定类型为数据后,将开始码替换成NALU的长度信息(4Bytes)
      • 利用函数CMBlockBufferCreateWithMemoryBlock构造CMBlockBufferRef对象
    CMBlockBufferRef blockBuffer=NULL;
    CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault,                                  
    (void*)frame.bytes,                                   
     frame.length,                                  
    kCFAllocatorNull,NULL,0, frame.length,0,&blockBuffer);
    
    • 根据需要,生成CMTime信息。不过加入time信息可能产生不稳定的图像,如果不是特别需要,不建议加入time信息。

    根据前面产生的CMVideoFormatDescriptionRef、CMBlockBufferRef和可选的时间信息,使用函数CMSampleBufferCreate得到CMSampleBuffer这个待解码的原始数据。

    CMSampleBufferRef sampleBuffer =NULL;
    CMSampleBufferCreateReady(kCFAllocatorDefault,                          
    blockBuffer,                          
    _decoderFormatDescription,1,0,NULL,1, sampleSizeArray,                         
     &sampleBuffer);
    

    具体如下所示,为H264解码数据转换图。

    H264码流转换CMSampleBuffer示意图

    2. 硬解码后的图像显示

    下面我们就看一下硬解码后的图像显示,具体的显示方式有两种:

    • 通过系统提供的AVSampleBufferDisplayLayer来解码并显示。
    • 通过VTDecompression接口来,将CMSampleBuffer解码成图像,将图像通过UIImageView或者OpenGL上显示。

    通过系统提供的AVSampleBufferDisplayLayer来解码并显示

    AVSampleBufferDisplayLayer是苹果提供的一个专门显示解码后的H264数据的显示层,它是CALayer的子类,因此使用方式和其它CALayer类似。使用方法enqueueSampleBuffer :进行显示该层内置了硬件解码功能,将原始的CMSampleBuffer解码后的图像直接显示在屏幕上面,如下图所示。

    AVSampleBufferDisplayLayer显示硬解码后的图像

    下面看一下实例代码

    CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer,YES);
    CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments,0);
    CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
    if(status == kCMBlockBufferNoErr) {
    
    if([_avslayer isReadyForMoreMediaData]) {dispatch_sync(dispatch_get_main_queue(),^{        
    
        [_avslayer enqueueSampleBuffer:sampleBuffer];      
    
        });   
      }    
    
    CFRelease(sampleBuffer);
    }
    

    下面看一下这个显示方式的解码流程。

    通过VTDecompression接口来,将CMSampleBuffer解码成图像,将图像通过UIImageView或者OpenGL上显示

    • 初始化VTDecompressionSession,设置解码器的相关信息,初始化需要CMSampleBuffer里面的FormatDescription,以及设置解码后的图像存储方式。编码后的图像解码后,会调用一个回调函数,在这个回调函数里面,你可以获得解码后的图像。我们将解码后的图像发给control来显示,初始化的时候需要回调指针作为参数传给create接口函数,同时利用create接口函数对session进行初始化。
    VTDecompressionSessionRef _deocderSession;
    VTDecompressionSessionCreate(kCFAllocatorDefault,                           
     _decoderFormatDescription,NULL, attrs,                            
    &callBackRecord,                           
     &_deocderSession);
    
    • 上面的回调函数可以完成由CGBitmapUIImage之间的转换,将图像通过队列发送到control来处理显示。
    CIImage *ciImage= [CIImage imageWithCVPixelBuffer:outputPixelBuffer];
    UIImage *uiImage= [UIImage imageWithCIImage:ciImage];
    
    • 通过接口VTDecompresSessionDecodeFrame进行解码操作,并将解码后的图像交给上面两个步骤的回调函数,以便进一步处理。具体如下图所示。
    // 使用VTDecompressionSessionDecodeFrame接口解码成CVPixelBufferRef数据:
    
    CVPixelBufferRef outputPixelBuffer=NULL;
    VTDecompressionSessionDecodeFrame(_deocderSession,
    sampleBuffer,
    flags
    &outputPixelBuffer,
    &flagOut);
    
    VTDecompression硬解码过程示意图

    下面看一下这种解码方式和显示流程。

    下面看一下这两种解码方式的优缺点。

    • 解码方式一

      • 优点: 该方式通过系统提供的AVSampleBufferDisplayLayer显示层来解码并显示。该层内置了硬件解码功能,将原始的CMSampleBuffer解码后的图像直接显示在屏幕上,非常的简单方便,且执行效率高,占用内存相对较少。

      • 缺点: 从解码的数据中不能直接获取图像数据并对其做相应处理,解码后的数据不能直接进行其他方面的应用(一般要做较复杂的转换)。

    • 解码方式二

      • 优点: 该方式通过VTDecompressionSessionDecodeFrame接口,得到CVPixelBufferRef数据,我们可以直接从CVPixelBufferRef数据中获取图像数据并对其做相应处理,方便于其他应用。

      • 缺点: 解码中执行效率相对降低,占用的内存也会相对较大。


    硬编码

    硬编码我们也经常见,比如说,我们直播录制视频,就要先通过摄像头采集图像,然后进行硬编码,最后将硬编码后的数据组合成H264码流通过网络传播。

    1. 视频采集

    这个硬件设备就是摄像头了,通过AVFoundation框架中的AVCaptureSession类来采集图像,并设定好input和output,同时设定deleagte代理和输出队列,在代理delegate方法中,处理采集好的图像。图像输出的格式是未编码的CMSampleBuffer形式。

    2. 使用VTCompressionSession进行硬编码

    获取采集后的图像,我们需要使用VTCompressionSession进行硬编码。

    • 初始化VTCompressionSession

      • 在初始化VTCompressionSession的时候,我们需要给出width和height,还有编码器类型kCMVideoCodecType_H264等。然后,通过VTSessionSetProperty接口设置帧率等属性。最后,需要设定一个回调函数,这个回调是视频编码成功后调用,全部准备好后,调用VTCompressionSessionCreate创建session
    • 提取摄像头采集的原始图像数据给VTCompressionSession来硬编码

      • 摄像头采集后的图像是未编码的CMSampleBuffer形式,利用给定的接口函数CMSampleBufferGetImageBuffer从中提取出CVPixelBufferRef,使用硬编码接口VTCompressionSessionEncodeFrame来对该帧进行硬编码,编码成功后,会自动调用session初始化时设置的回调函数。
    • 利用回调函数,将因编码成功的CMSampleBuffer转换成H264码流,通过网络传播。

      • 解析成SPS和PPS参数,加上开始码后组装成NALU,提取出视频数据,将长度码转换成开始码,组长成NALU,并将NALU发送出去。
    硬编码处理流程示意图

    参考文章

    1. iOS8系统H264视频硬件编解码说明
    2. iOS-H264 硬解码
    3. 一轮圆月作者关于硬编解码的GitHub Demo

    后记

    未完,待续~~~

    相关文章

      网友评论

        本文标题:H264编解码(二) —— ios中的H264硬编解码的实现

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