版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2017.12.23 |
前言
对于做过音视频的开发者,编解码都不陌生,接下来这几篇就详细的看一下音视频编解码相关知识。感兴趣的可以看这几篇文章。
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的码流结构。

还可以看下面这个示意图

下面我们就看一下这个解析过程。
- 提取
sps和pps生成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解码数据转换图。

2. 硬解码后的图像显示
下面我们就看一下硬解码后的图像显示,具体的显示方式有两种:
- 通过系统提供的
AVSampleBufferDisplayLayer来解码并显示。 - 通过
VTDecompression接口来,将CMSampleBuffer解码成图像,将图像通过UIImageView或者OpenGL上显示。
通过系统提供的AVSampleBufferDisplayLayer来解码并显示
AVSampleBufferDisplayLayer是苹果提供的一个专门显示解码后的H264数据的显示层,它是CALayer的子类,因此使用方式和其它CALayer类似。使用方法enqueueSampleBuffer :进行显示该层内置了硬件解码功能,将原始的CMSampleBuffer解码后的图像直接显示在屏幕上面,如下图所示。

下面看一下实例代码
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);
- 上面的回调函数可以完成由
CGBitmap到UIImage之间的转换,将图像通过队列发送到control来处理显示。
CIImage *ciImage= [CIImage imageWithCVPixelBuffer:outputPixelBuffer];
UIImage *uiImage= [UIImage imageWithCIImage:ciImage];
- 通过接口
VTDecompresSessionDecodeFrame进行解码操作,并将解码后的图像交给上面两个步骤的回调函数,以便进一步处理。具体如下图所示。
// 使用VTDecompressionSessionDecodeFrame接口解码成CVPixelBufferRef数据:
CVPixelBufferRef outputPixelBuffer=NULL;
VTDecompressionSessionDecodeFrame(_deocderSession,
sampleBuffer,
flags
&outputPixelBuffer,
&flagOut);

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

下面看一下这两种解码方式的优缺点。
-
解码方式一
优点: 该方式通过系统提供的
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
后记
未完,待续~~~
