准备知识
H264裸流数据十六进制展示
00 00 00 01 67 64 00 2a ad 84 01 0c 20 08 61 00 43 08 02 18 40 10 c2 00 84 3b 50 3c 01 13 f2 cd c0 40 40 50 00 00 3e 80 00 0c 35 08 40
00 00 00 01 68 ee 31 b2 1b
00 00 00 01 65 b8 00 00 0d 36 c1 3f 7c 81 af 52 7f 81 e5 59 dc ac 37 50 e5 81 80 28 5a 50 88 0a 93 86 54 0c 05 ec f1 ed 59 43 6f 0a 1a 93 1e d9 60 5d ef 20 34
一个NAL单元没有起始位置和终止位置确定其范围(NAL单元没有一个说明该数据长度的语法),如果编码数据存储位一个文件,编码器将无法从数据流中分离出每个NAL的起始位置和终止位置。因此H264采用起始码来解决这个问题。H.264编码时,在每个NAL前添加起始码 (一帧的开始则用4位字节表示,ox00000001,否则用3位字节表示ox000001),解码器在码流中检测到起始码,当前NAL结束。
附上RTP数据
80 69 00 01 00 00 0e 10 01 e0 a1 d7 67 64 00 2a ad 84 01 0c 20 08 61 00 43 08 02 18 40 10 c2 00 84 3b 50 3c 01 13 f2 cd c0 40 40 50 00 00 3e 80 00 0c 35 08 40
80 69 00 02 00 00 0e 10 01 e0 a1 d7 68 ee 31 b2 1b
80 e9 00 56 00 00 0e 10 01 e0 a1 d7 7c 45 b9 aa 17 75 27 90 b8 a6 c5 59 e4 f2 1d db 6c e6 6b 17 31 21 88 18 1e ab 3e 3d d7 12 4c 37 b1 51 39 26 a0 33 e0 1a 5b 70
附带RTP数据格式说明
80 69 00 01 00 00 0e 10 01 e0 a1 d7
十二个字节的RTP头
80(1000 0000)
1) V:RTP协议的版本号,占2位,当前协议版本号为2 (10)
2) P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。(0)
3) X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头(0)
4) CC:CSRC计数器,占4位,指示CSRC 标识符的个数(作用信源CSRC计数器)(0000)
69 (0100 0101)
5) M: 标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。(对于分组中的重要事件可用该位标识)
6) PT: 有效荷载类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等,在流媒体中大部分是用来区分音频流和视频流的,这样便于客户端进行解析。(100 0101)一般情况下96指定为H264,但是实际上有些厂商,例如宇视,就是使用105,所以不能通过这个负载类型判断
7) 序列号:占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。这个字段当下层的承载协议用UDP的时候,网络状况不好的时候可以用来检查丢包。同时出现网络抖动的情况可以用来对数据进行重新排序,序列号的初始值是随机的,同时音频包和视频包的sequence是分别记数的。(00 01)
8) 时戳(Timestamp):占32位,必须使用90 kHz 时钟频率。时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。(00 00 0e 10)
9) 同步信源标识符:占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的***C。(01 e0 a1 d7)
10) 特约信源(CSRC)标识符:每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源。
FU-A分包原理
超过MTU发送单元的H264视频包,需要分片发送,12字节的RTP头后面跟随的就是FU-A分片。FU-A分片的前两个字节分别位FU indicator 和 FU header。FU indicator的前三位和
FU header的后五位构成了NAL单元的头
说明下元素
F:禁止位,0表示正常,1表示错误,一般都是0
NRI:重要级别,11表示非常重要。
TYPE:表示该NALU的类型是什么,
所以有的文档中SPS是0x27,只是NRI的重要性设置不一样而已
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| FU indicator | FU header | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| |
| FU payload |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 14. RTP payload format for FU-A
代码
bool ReadNextFrameByH264FromRTP(const std::string strRTPBuffer)
{
static std::uint8_t szStartCode[] = { 0x00, 0x00, 0x00, 0x01 };
//收到的RTP包,保存在strRTPBuffer缓存中
std::uint8_t* pData = (std::uint8_t*)strRTPBuffer.c_str();
//同一帧的数据,时间戳都是一样的
std::uint32_t nTimeStamp = (((pData[4] << 8) + pData[5]) << 16) + (pData[6] << 8) + pData[7];
//获取FU-A分片的第一个字节的后五位,判断分片类型
std::uint8_t cFragmentationUnitType = pData[12] & 0x1F;
bKeyFrame = CVOS_FRAME_TYPE_NON_KEY_FRAME;
switch (cFragmentationUnitType)
{
case 7:
case 8:
{
//一般情况下SPS/PPS都是单独完整的一个RTP包,直接去掉RTP头部
m_strCompleteOneFrame.append((char*)szStartCode, 4);
m_strCompleteOneFrame.append(&m_strRTPBuffer[12], m_strRTPBuffer.size() - 12);
//在这里没有标志为完整的一帧,是因为一般的编码器编码SPS/PPS之后是I帧,打包到一起
break;
}
case 28:
{
//获取FU-A分片的第二个字节的后五位,判断当前的帧类型
std::uint8_t cFrameType = pData[13] & 0x1F;
if (5 == cFrameType)
{
//收到关键帧
}
std::uint8_t cStartBit = pData[13] >> 7;
std::uint8_t cEndBit = (pData[13] & 0x40) >> 6;
if (1 == cStartBit)
{
m_strCompleteOneFrame.append((char*)szStartCode, 4);
//根据FU-A indicator 和 FU-A header 拼凑出NAL头部
unsigned char cNALHeader = (pData[12] & 0xE0) | (pData[13] & 0x1F);
m_strCompleteOneFrame.append((char*)&cNALHeader, 1);
}
if (1 == cEndBit)
{
//标志为完整的一帧
m_bCompleteOneFrame = true;
}
//去掉RTP头部和FU-A头
m_strCompleteOneFrame.append(&m_strRTPBuffer[14], m_strRTPBuffer.size() - 14);
break;
}
}
bool bRet = m_bCompleteOneFrame;
if (m_bCompleteOneFrame)
{
//开始处理完整的一帧
m_strCompleteOneFrame.clear();
m_bCompleteOneFrame = false;
}
return bRet;
}