版本记录
版本号 | 时间 |
---|---|
V1.0 | 2017.12.23 |
前言
视频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码流结构下面我们就看一下这个解析过程。
- 提取
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解码数据转换图。
H264码流转换CMSampleBuffer示意图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);
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发送出去。
参考文章
后记
未完,待续~~~