上次整理视频一些知识,这些知识以采集,编码/解码相关的,也引出了H264概念
文章: https://www.cnblogs.com/winafa/p/12768392.html
H264的设计,主要还是网络传输
但网络传输,在我们印象中,基础也就是UDP/TCP之类的,那么视频(数据量又那么大),有没有合适的应用层协议呢?
答案肯定有的,像rtsp rtp都是为了视频传输而来,因为视频数据对实时性要求比较高,因此这些协议本身也是针对实时性进行部分优化
RTP,即real-time transport protocol(实时传输协议),为实时传输交互的音频和视频提供了端到端传输服务。其中包括载荷的类型确认,序列编码,时间戳和传输监控功能。
特点:序列(也就是保证前后顺序)、时间戳(便于抉择,检测网络丢包/数据同步之类)、传输监控(意思是断开,重连之类可以恢复)
一般来说RTP协议,都是基于UDP,因此对每包数据都带有报文头信息:
除了X=1以外,其他情况下,报文头占12字节。
关于RTP格式,这篇文章整理的很详细,具体可以参考下
https://blog.csdn.net/chen495810242/article/details/39207305
1) V:RTP协议的版本号,占2位,当前协议版本号为2
2) P:填充标志,占1位,如果P=1,则在该报文的尾部填充一个或多个额外的八位组,它们不是有效载荷的一部分。
3) X:扩展标志,占1位,如果X=1,则在RTP报头后跟有一个扩展报头
4) CC:CSRC计数器,占4位,指示CSRC 标识符的个数
5) M: 标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。
6) PT: 有效荷载类型,占7位,用于说明RTP报文中有效载荷的类型,如GSM音频、JPEM图像等,在流媒体中大部分是用来区分音频流和视频流的,这样便于客户端进行解析。
7) 序列号:占16位,用于标识发送者所发送的RTP报文的序列号,每发送一个报文,序列号增1。这个字段当下层的承载协议用UDP的时候,网络状况不好的时候可以用来检查丢包。同时出现网络抖动的情况可以用来对数据进行重新排序,序列号的初始值是随机的,同时音频包和视频包的sequence是分别记数的。
8) 时戳(Timestamp):占32位,必须使用90 kHz 时钟频率。时戳反映了该RTP报文的第一个八位组的采样时刻。接收者使用时戳来计算延迟和延迟抖动,并进行同步控制。
9) 同步信源(SSRC)标识符:占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。
10) 特约信源(CSRC)标识符:每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源。
这里要补充点知识,我们知道视频数据本身是很大的,比如I帧数据,往往都比较大,
而UDP是本身单包长度有限制的(在无线传输的事情,肯定),大概一包可以传输1500字节
因此要对大视频(单帧超过1400字节)流数据进行拆包处理
从RTP的Header来看,还不足以知道该包是什么?包含哪些信息
话说回来,前面也介绍了H264主要分成 I帧/P帧/B帧,这些可以称为裸流,拿来编解码是够了,但传输是不够的。
因此,H264又做了进一步努力,以便更好的传输
NALU(Network Abstract Layer Unit) 可以通俗点理解,就是网络传输单元,也就是H264定义报文格式
NALU结构非常简单,NALU结构:NALU header(1byte) + NALU payload
header就一个字节,最高位b7=0,如果=1表示错误
然后b6~b5 表示数据优先级,11 最高,也就是不能丢弃
最后5位,b4~b0表示数据类型,像PPS/SPS/都这里定义(加粗为重点)
0:未定义
1:非IDR图像不采用数据划分片段 大名鼎鼎的P帧
2:非IDR图像采用数据划分片段A部分
3:非IDR图像采用数据划分片段B部分
4:非IDR图像采用数据划分片段C部分
5:IDR图像片段。 大名鼎鼎的I帧
6:补充增强信息 SEI
7:序列参数集 SPS,一般跟PPS一起,里面会填视频本身一些基础信息,比如分辨率,解码的时候必须要用到
8:图像参数集 PPS
9:分隔符
10:序列结束符
11:流结束符
12:填充数据
13:序列参数集扩展
14:带前缀的NALU
15:子序列参数集
16-18:保留
19:不采用数据划分的辅助编码图像片段
20:编码片段扩展
21-23:保留
24-31:未定义
另外,需要强调的是,H264的开头还有固定 00 00 00 01 四个字节内容(这个基本上是给人看)
实际VCL流应该是这样的
因此,像PPS SPS很小的帧,也不会超过1400字节,也就是一包就够了
但像I帧这样,比较大的数据帧,一般来说都是超过1400的,那么要进行拆包
这里要插入一点知识,前面说到H264前面有 00 00 00 01 4个字节内容,且没任何含义,因此,用RTP传输的时候,
要先去掉 00 00 00 01 四个固定字节内容,接收端收到之后,需要添加上去。这个也很重要,否则H264解码会挂掉的
单包就能够搞定的:
直接将NALU打包成RTP包进行传输 RTP header(12bytes) + NALU header (1byte) + NALU payload(实际数据)
需要多包:
RTP header (12bytes)+ FU Indicator (1byte) + FU header(1 byte) + NALU payload
会有疑问:怎么区分 NALU header 还是 FU Indicator?
同样1个字节,是这样的:NALU header 只用 0~20,像21~31还是预留的,可以拿来用
FU header 的格式如下:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
S: 1 bit
当设置成1,开始位指示分片NAL单元的开始。当跟随的FU荷载不是分片NAL单元荷载的开始,开始位设为0。(分包用)
E: 1 bit
当设置成1, 结束位指示分片NAL单元的结束,即, 荷载的最后字节也是分片NAL单元的最后一个字节。当跟随的FU荷载不是分片NAL单元的最后分片,结束位设置为0。(分包用)
R: 1 bit
保留位必须设置为0,接收者必须忽略该位。
Type: 5 bits (H264里的什么PPS SPS I帧,都在这里定义) 从这里可以知道这是什么帧
贴一段,据说来自live555的源码:
Boolean H264VideoRTPSource
::processSpecialHeader(BufferedPacket* packet,
unsigned& resultSpecialHeaderSize) {
unsigned char* headerStart = packet->data();
unsigned packetSize = packet->dataSize();
unsigned numBytesToSkip;
// Check the 'nal_unit_type' for special 'aggregation' or 'fragmentation' packets:
if (packetSize < 1) return False;
fCurPacketNALUnitType = (headerStart[0]&0x1F); //FU Indicator后五位即NALU类型 0x1F = 0001 1111
switch (fCurPacketNALUnitType) {
case 24: { // STAP-A
numBytesToSkip = 1; // discard the type byte
break;
}
case 25: case 26: case 27: { // STAP-B, MTAP16, or MTAP24
numBytesToSkip = 3; // discard the type byte, and the initial DON
break;
}
case 28: case 29: { // // FU-A or FU-B
// For these NALUs, the first two bytes are the FU indicator and the FU header.
// If the start bit is set, we reconstruct the original NAL header into byte 1:
if (packetSize < 2) return False;
unsigned char startBit = headerStart[1]&0x80; //FU Header start标记位 0x80= 1000 0000
unsigned char endBit = headerStart[1]&0x40; //FU Header End标记位 0x40= 0100 0000
if (startBit) {
fCurrentPacketBeginsFrame = True;
headerStart[1] = (headerStart[0]&0xE0)|(headerStart[1]&0x1F); //还原NALU头
numBytesToSkip = 1;
} else {
// The start bit is not set, so we skip both the FU indicator and header:
fCurrentPacketBeginsFrame = False;
numBytesToSkip = 2;
}
fCurrentPacketCompletesFrame = (endBit != 0);
break;
}
default: {
// This packet contains one complete NAL unit:
fCurrentPacketBeginsFrame = fCurrentPacketCompletesFrame = True; //默认没有分片,完整的NALU
numBytesToSkip = 0;
break;
}
}
resultSpecialHeaderSize = numBytesToSkip;
return True;
}
以上内容,部分参考
https://www.cnblogs.com/leehm/p/11009504.html
live555是一个开源框架,支持rtp等协议。实际上,你可以理解为rtp协议实现代码。
当然live555也不止rtp协议
实际上要写好传输代码,也不容易,虽然有live555这样开源背书了,但还是比较困难的
以上知识是基础知识,实际经验是无法语言描述出来的
代码还应考虑很多:比如缓冲区,时序控制,断开之后重连,握手,等等很多了,把这些都实现了,也需要很久很久