iOS硬解处理H264数据

来源:互联网 发布:人工智能中文第四版pdf 编辑:程序博客网 时间:2024/05/16 11:45

http://www.jianshu.com/p/f7736c34ca70?utm_campaign=maleskine&utm_content=note&utm_medium=pc_all_hots&utm_source=recommendation


苹果从iOS8开始,开放了硬编码和硬解码的api,所以,从iOS8开始,需要解码H264视频时,推荐使用系统提供的videotoolbox来进行硬解

因为videotoolbox解码时的输入是H264数据,而通常看到的视频流或者文件都是经过复用封装之后的类似MP4格式的,所以在将数据交由videotoolbox处理之前需要先进行解复用的操作来将H264数据抽取出来。目前比较通用的做法是使用ffmpeg来进行这个解复用的操作。

此处介绍通过ffempg读取到视频数据之后,在交由videotoolbox解码之前需要进行的一些操作。

H264数据的结构

通常所说的H264裸流,指的是由StartCode分割开来的一个个NALU组成的二进制序列,每个NALU一般来说就是一帧视频图像的数据(也有可能是多个NALU组成一帧图像,或者该NALU是SPS、PPS等数据)


图1 - H264格式


如上图所示,0x00 00 00 01四个字节为StartCode,在两个StartCode之间的内容即为一个完整的NALU。
每个NALU的第一个字节包含了该NALU的类型信息,该字节的8个bit将其转为二进制数据后,解读顺序为从左往右算,如下:
(1)第1位禁止位,值为1表示语法出错
(2)第2~3位为参考级别
(3)第4~8为是nal单元类型
由此可知计算NALU类型时,只需将该字节的值与0x1F(二进制的0001 1111)相与,结果即为该NALU类型。
NALU类型有一下几种:


图2 - NALU类型


其中常见的有1、5、7、8、9几种类型。

VideoToolbox可接收的数据格式

与通常所说的H264数据格式有区别,VideoToolbox解码时需要数据的H264数据格式为AVC1格式,开始的4个字节不是StartCode,而是后面NALU的数据长度。
常见的封装格式中mp4和flv格式封装的是AVC1格式的H264, ts格式中是以StartCode为其实的H264。
如果原始数据是StartCode的格式,则需要将其转换为AVC1的格式才能交给Videotoolbox进行解码

SPS和PPS

SPS(序列参数集Sequence Parameter Set)和 PPS(图像参数集Picture Parameter Set)是图2中NALU类型为7、8的两种NALU,其中包含了图像编码的各种参数信息,为解码时必须的输入。

可能遇到的坑

SPS和PPS的获取

如果是自己实现解复用来提取音视频流中H264数据,可以通过分析H264数据中的NALU类型来获取SPS和PPS。
但通常的做法是使用ffmpeg来实现解复用,此时调用avformat_open_input和avformat_find_stream_info函数后,可以在AVCodecContext结构中的extradata数据中获取SPS和PPS数据
extradata为一个数据块的地址指针,extradata_size指明了其长度,其中存储的数据有两种格式:
(1) 直接存储SPS和PPS两个NALU
(2) 存储一个AVCDecoderConfigurationRecord格式的数据,该结构在标准文档“ISO-14496-15 AVC file format”中有详细说明,如下图:


图3 - AVCDecoderConfigurationRecord


实际使用过程中可以通过extradata中的开始几个字节来判断是其中存储的是那种类型的数据(起始数据为00 00 00 01或00 00 01的为两个NALU)

4字节还是3字节的StartCode

StartCode可以是4个字节的00 00 00 01,也可以是3个字节的00 00 01, 有资料说当一帧图像被编码为多个slice(即需要有多个NALU)时,每个NALU的StartCode为3个字节,否则为4个字节。但实际并不是所有编码器都按照这个规定实现,所以在实际使用过程中,要对4个字节和3个字节的StartCode都进行一次判断,包括上面说的extradata中的SPS和PPS,还有实际图像的NALU。

ffmpeg中side_data的影响

如果使用ffmpeg对ts格式进行解复用操作,在av_read_frame读取到一帧视频数据之后,需要将数据转换为AVC1的格式,但如果在ffmpeg中没有对AVFormatContext结构的flags变量设置AVFMT_FLAG_KEEP_SIDE_DATA,那么获取的AVPacket结构中的data地址中,保存的将不仅仅只有原始数据,它还将在末尾包含一个叫做side_data的数据(其实存储的是MEPG2标准中定义的StreamID),这个数据会导致计算的NALU长度比实际要长,从而可能导致VideoToolbox无法解码。
避免方式一个是设置AVFMT_FLAG_KEEP_SIDE_DATA,另一个是调用av_packet_split_side_data将side_data从data数据中分离。

分辨率变化

flv和ts格式的流都支持中途改变分辨率,所以在分辨率变化后需要重新初始化VideoToolbox的session,否则将会产生解码错误。
码流的分辨率发生变化(或者说编码参数发生变化时),都会有SPS和PPS数据的更新,需要根据新的SPS和PPS信息重新建立解码session。
ts流中更新的SPS和PPS数据和普通视频数据一样,正常解析数据即可获取到新的SPS和PPS数据。
flv流或者rtmp流中的SPS和PPS数据的更新,位于FLV结构中一个叫做AVC sequence header的tag中,其中存储的为图3所示的一个AVCDecoderConfigurationRecord结构,需要从中提取出SPS和PPS数据。该数据在使用ffmpeg的av_read_frame获取数据时,依然保存在AVPacket的side_data中。
获取到SPS和PPS数据后,可以创建一个CMFormatDescriptionRef结构,然后使用VTDecompressionSessionCanAcceptFormatDescription函数判定原有session是否能解码新的数据,如果返回值为假,则需要重建解码session。
而新的视频分辨率可以通过解析SPS数据来获取。

SPS解析

SPS结构如下图所示:


图4 - SPS

根据SPS信息计算视频分辨率如下

width = ((pic_width_in_mbs_minus1 +1)*16) - frame_crop_left_offset*2 - frame_crop_right_offset*2;height = ((2 - frame_mbs_only_flag)* (pic_height_in_map_units_minus1 +1) * 16) - (frame_crop_top_offset * 2) - (frame_crop_bottom_offset * 2);

图4中descriptor中的描述意义如下

  • u(N) - 长度为N个bit的无符号数字
  • s(N) - 长度为N个bit的有符号数字
  • ue(v) - 无符号的指数哥伦布编码
  • se(v) - 有符号的指数哥伦布编码
    其中的难点是指数哥伦布编码的解析,原理请搜索指数哥伦布编码,具体实现可以参考ijkplayer中解析SPS信息时的实现https://github.com/Bilibili/ijkplayer/blob/master/ios/IJKMediaPlayer/IJKMediaPlayer/ijkmedia/ijkplayer/ios/pipeline/h264_sps_parser.h
    其中未实现se(v)的解析,它的实现如下
    static int64_tnal_bs_read_se(nal_bitstream *bs){  int64_t ueVal = nal_bs_read_ue(bs);  double k = ueVal;  int64_t nValue = ceil(k/2);  if(ueVal%2 == 0)  {      nValue = -nValue;  }  return nValue;}
    或者参考ffmpeg中对SPS解析的实现https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/h264_ps.c#L334
 日记本






原创粉丝点击