H264码流
H264协议中将编码器分成两个逻辑层,就是经常见到的VCL(视频数据编码层)和NAL(网络抽象层)。VCL负责具体图像数据的编码,NAL负责组织这些编码后的数据。
既然称之为码流,那么,这些数据肯定像流一样,一位接着一位的排成一队。组织这些编码数据的方式,常见的是Annex B
格式和AVCC
格式,感觉Annex B
格式最为常见,它长这样:
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都是可变长的。
type
和size
的规则一样:将待写入的数值拆分,分多次写入,每次只写入一个字节,除最后一次写入以外,之前的每次写入数值为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两种;