H264码流

H264协议中将编码器分成两个逻辑层,就是经常见到的VCL(视频数据编码层)和NAL(网络抽象层)。VCL负责具体图像数据的编码,NAL负责组织这些编码后的数据。

既然称之为码流,那么,这些数据肯定像流一样,一位接着一位的排成一队。组织这些编码数据的方式,常见的是Annex B格式和AVCC格式,感觉Annex B格式最为常见,它长这样:

H264码流

Startcode

其中,NALU是NAL层数据的基本单元,U是Unit。之所以加上StartCode,是因为这是数据流,想解析一个包,必须知道包从哪里开始,到哪里结束,这在计算机网络体系里边,应该叫帧定界符才对。在一个码流中,两个StartCode之间的数据,就是一个NALU。StartCode只有3字节:0x00 0x00 0x01,特殊情况下,也会在其前边加上一个1个字节的0x00构成4字节: 0x00 0x00 0x00 0x01

NALU

NALU是码流的核心元素,如上图,一个NALU由两部分组成:Header和Data。

Nalu Header

和大部分协议一样,Header部分一般会包含载荷的相关信息,Nalu Header定义如下:

  • F字段占1 bit,初始为0,当该NALU在传输过程中识别为错误时,该字段会被修改为1,作为标识。
  • NRI字段占2 bits,记录着该NALU的重要等级。
  • Type字段占5 bits,指明载荷是什么类型的数据。

较为常用Type如下:

Type值 说明
1 非IDR帧
5 IDR帧
6 SEI数据
7 SPS数据
8 PPS数据

Nalu Data

Nalu Data是NALU中的载荷,也就是具体传输的内容。
这里要注意一下,前边提到的StartCode,既然用StartCode来做帧定界符,那么就必须要保证传输的数据中没有StartCode字符才可以,要不然解析时就会出错,这个环节在计算机网络中也有,其实就是数据链路层的“透明传输”问题。

这里引出3个概念: SODB, RBSP,EBSP。

  • SODB: 要传输的最原始数据,比如想发送字符串Hello Clay,那么Hello Clay就是SODB。
  • RBSP:SODB数据由VCL产生,其基本单位是bit。不一定能转换成整数个字节,所以,这一步要做的,是将SODB进行字节对齐,方法是先在SODB的末尾加1个bit的1,然后再用若干比特个0对齐到字节(8的整数倍)。处理后的数据,即为RBSP。尾部填充的这些数据,有个专有名词“RBSP Trailing Bits”
  • EBSP:RBSP中有可能包含StartCode,所以,还需要将这些冲突的数据处理一下。方法是:在RBSP中连续的两个0x00后边插入一个0x03,在解析时移除。

SEI

SEI的全程是 Supplemental Enhancement Information,补充增强信息。可以用SEI向码流中加入额外信息,比如libx264在刚编码刚开始的时候,会用SEI加入编码器参数、版本等信息。

在一些特殊场景下,也会用到SEI附加信息到码流中,比如,在直播场景下,一般的通信链路和媒体流都是分开的,如果想在某个时刻(比如有大哥刷了礼物)触发一个特效,但是,这个特效必须要和一个动作强关联,这种情况下,如果不在码流中插入触发信息,想做好二者的同步,好像不太容易,一是通信和媒体流走的是两条路,必须要做一次对齐才行,二是这个动作未必能和视频帧准确对齐。此时,如果能在码流中的视频帧后边用SEI附加这个指令,同步问题就很好做了。

其结构如下:

SEI信息,也包含很多类型,所以,这里有一个payload type字段。
payload size字段表达的是后边的payload content数据的长度,单位是字节。

上图中没有标明每个字段所占的位数,这是因为,在SEI信息中,type、size和content都是可变长的。
typesize的规则一样:将待写入的数值拆分,分多次写入,每次只写入一个字节,除最后一次写入以外,之前的每次写入数值为255

伪代码如下:

uint32_t type;
uint8_t *buf;
int x = 0; // write to buf
int idx = 0;
for(x = type; x >= 255; x -= 255) buf[idx++] = 255;
buf[idx++] = x;

在较多的博客文章中,想在SEI中插入自定义信息,都建议使用Type类型为5的报文格式,这是因为,根据参考[1]中的内容,SEI的类型为5时,该报文载荷数据为未注册的用户数据类型,可以存入自定义的数据。此时,payload content中的前16个字节应为符合标准的UUID,同时,这也说明,payload size最小应该为16字节。但是感觉,这和开发人员按照自己的业务逻辑自定义一个类型为100、200的好像没有区别?所以,在一些场景下,比如只是想单纯的为某一帧标记一些数据,可以自定义类型,保证自定义的类型不和标准协议中冲突即可。

代码实践

RBSP 转 EBSP

std::vector<uint8_t> RBSP2EBSP(const std::vector<uint8_t> &rbsp)
{
    std::vector<uint8_t> res;
    res.reserve(rbsp.size() * 1.5);

    int cnt = 0;
    for(auto&& c : rbsp) 
    {
        res.emplace_back(c);

        if(c == 0x00) 
        {
            if(++cnt == 2)
            {
                res.emplace_back(0x03);
                cnt = 0;
            }
        }else{
            cnt = 0;
        }
    }

    return res;
}

EBSP 转 RBSP

std::vector<uint8_t> EBSP2RBSP(const std::vector<uint8_t> &ebsp)
{
    std::vector<uint8_t> res;
    res.reserve(ebsp.size());

    int cnt = 0;
    for(auto&& c : ebsp)
    {
        if(c == 0x03 && cnt >= 2) 1;
        else res.emplace_back(c);

        if(c == 0x00) ++cnt;
        else cnt = 0;
    }

    return res;
}

构建SEI NALU

std::vector<uint8_t> MakeSEI(uint32_t sei_type, const std::string &data)
{

    const auto ebsp = RBSP2EBSP(std::vector<uint8_t>(data.begin(), data.end()));

    const size_t len = 4 + 1 + (1 + sei_type / 255) + (1 + ebsp.size() / 255) + ebsp.size() + 1;

    std::vector<uint8_t> nalu(len, 0);

    // start code(4 B): 0x00 0x00 0x00 0x01
    int idx = 3;
    nalu[idx++] = 0x01;

    // nalu header (1 B): 0x06
    nalu[idx++] = 0x06;

    // payload type(n B): 0x64
    for(; sei_type >= 255; sei_type -= 255) nalu[idx++] = 0xff;
    nalu[idx++] = sei_type;

    // payload size(n B)
    int c = ebsp.size();
    for(;c >= 255; c -= 255) nalu[idx++] = 0xff;
    nalu[idx++] = c; 

    // payload content
    std::copy(ebsp.begin(), ebsp.end(), nalu.begin() + idx);

    // rbsp trailing bits (1 B)
    nalu.back() = 0x80;

    return nalu;
}

解析SEI NALU

std::pair<uint32_t, std::vector<uint8_t>> ExtractSEI(const std::vector<uint8_t> &nalu)
{
    int startcodeLen = GetStartcodeLen(nalu);
    if(startcodeLen == -1) return {-1, std::vector<uint8_t>()};

    int idx = startcodeLen + 1; // at payload type

    // read payload type
    uint32_t payloadType = 0;
    uint8_t byte = 0xff;
    while(byte == 0xff)
    {
        byte = nalu[idx++];
        payloadType += byte;
    }

    // read payload size
    uint32_t payloadSize = 0;
    byte = 0xff;
    while(byte == 0xff)
    {
        byte = nalu[idx++];
        payloadSize += byte;
    }

    // read payload content
    std::vector<uint8_t> ebsp(payloadSize, 0);
    std::copy(nalu.begin() + idx, nalu.begin() + idx + payloadSize, ebsp.begin());

    return {payloadType, EBSP2RBSP(ebsp)};
}

H265码流

仅从Nalu层面来看,与H264码流相比,开发时应该注意:

  • StartCode没有变化;
  • 拥有更多Nalu类型;
  • Nalu Header占2个字节;
  • H264中有SPS和PPS,H265中加入VPS,送入解码器的顺序应该是VPS -> SPS -> PPS;
  • SEI有PREFIX_SEI和SUFFIX_SEI两种;

Reference

  1. https://www.itu.int/ITU-T/recommendations/rec.aspx?id=14659