iOS高级开发视频相关视频开发

iOS VideoToolbox硬解H.264数据说明

2017-04-24  本文已影响2164人  金山视频云

一. 概述

苹果从iOS 8开始,开放了硬编码和硬解码的api,所以,从iOS 8开始,需要解码H.264视频时,推荐使用系统提供的VideoToolbox来进行硬解

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

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

1.1 H.264数据的结构

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

图1 - H.264格式

如上图所示,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几种类型。

1.2 VideoToolbox可接收的数据格式

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

1.3 SPS和PPS

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

二. 实践

实践过程中遇到过不少坑,大致如下。

2.1 SPS和PPS的获取

如果是自己实现解复用来提取音视频流中H.264数据,可以通过分析H.264数据中的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)

2.2 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。

2.3 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无法解码。
避免VideoToolbox解码失败的方法有两种,任选其一即可:

2.4 分辨率的变化

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中的描述意义如下

static int64_t
nal_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

转载请注明:
作者金山视频云,首发简书 Jianshu.com


如果准备使用多媒体移动端SDK,请参考https://github.com/ksvc

金山云SDK相关的QQ交流群:

上一篇 下一篇

猜你喜欢

热点阅读