音视频编解码(二) —— iOS中的H264硬编解码的实现(一)

版本记录

版本号 时间
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的码流结构。

还可以看下面这个示意图

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

后记

未完,待续~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容