因为项目中,ipcam的视频编码方式主要是基于H.264,因此ipcam出来的H.264码流会按照协议rfc3984来打包,mediastream2中收到rtp recv filter的数据后,必须先根据rfc3984协议来进行解析,解析成nalu单元才能丢给ffmpeg来进行解码,本文就对h.264 rtp包的解析工作进行分析。
/*process incoming rtp data and output NALUs, whenever possible*/
void rfc3984_unpack(Rfc3984Context *ctx, mblk_t *im, MSQueue *out){
uint8_t type=nal_header_get_type(im->b_rptr);
uint8_t *p;
int marker = mblk_get_marker_info(im);
uint32_t ts=mblk_get_timestamp_info(im);
if (ctx->last_ts!=ts){
/*a new frame is arriving, in case the marker bit was not set in previous frame, output it now*/
/* unless this is a FU-A (workarond some other apps bugs)*/
ctx->last_ts=ts;
if (ctx->m==NULL){
while(!ms_queue_empty(&ctx->q)){
ms_queue_put(out,ms_queue_get(&ctx->q));
}
}
}
if (im->b_cont) msgpullup(im,-1);
if (type==TYPE_STAP_A){
/*split into nalus*/
uint16_t sz;
uint8_t *buf=(uint8_t*)&sz;
mblk_t *nal;
ms_debug("Receiving STAP-A");
for(p=im->b_rptr+1;p<im->b_wptr;){
buf[0]=p[0];
buf[1]=p[1];
sz=ntohs(sz);
nal=dupb(im);
p+=2;
nal->b_rptr=p;
p+=sz;
nal->b_wptr=p;
if (p>im->b_wptr){
ms_error("Malformed STAP-A packet");
freemsg(nal);
break;
}
ms_queue_put(&ctx->q,nal);
}
freemsg(im);
}else if (type==TYPE_FU_A){
mblk_t *o=aggregate_fua(ctx,im);
ms_debug("Receiving FU-A");
if (o) ms_queue_put(&ctx->q,o);
}else{
if (ctx->m){
/*discontinued FU-A, purge it*/
freemsg(ctx->m);
ctx->m=NULL;
}
/*single nal unit*/
ms_debug("Receiving single NAL");
ms_queue_put(&ctx->q,im);
}
if (marker){
ctx->last_ts=ts;
ms_debug("Marker bit set");
/*end of frame, output everything*/
while(!ms_queue_empty(&ctx->q)){
ms_queue_put(out,ms_queue_get(&ctx->q));
}
}
}
函数rfc3984_unpack就是根据rfc3984协议对h264的rtp包进行解析,以便得到nalu单元。
对于一个原始的 H.264 NALU 单元常由 [Start Code] [NALU Header] [NALU Payload] 三部分组成, 其中 Start Code 用于标示这是一个NALU 单元的开始, 必须是 "00 00 00 01" 或 "00 00 01", 占4个字节,NALU 头仅一个字节, 其后都是 NALU 单元内容,但在传输的时候,Start code字段被去掉了,在解码的时候,需要重新添加进去。
函数的入参im是指输入的rtp payload,注意是rtp有效负载,已经去掉了12字节的rtp头。
uint8_t type=nal_header_get_type(im->b_rptr);
首先获取nal的type字段,负载中的第一个字节便是nal单元类型,nal单元类型的结构图如下:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
NAL单元类型字节部件的语义在H.264规范中制定, 简要描述如下.
F: 1 bit
forbidden_zero_bit. H.264规范声明设置为1指示语法违例。
NRI: 2 bits
nal_ref_idc. 00值指示NAL单元的不用于帧间图像预测的重构参考图像。这样的NAL单元可以被丢弃而不用冒参考
图像完整性的风险。大于0的值指示NAL单元的解码要求维护参考图像的完整性。
Type: 5 bits
nal_unit_type.
类型对应的表如下:
Type Packet Type name Section
---------------------------------------------------------
0 undefined -
1-23 NAL unit Single NAL unit packet per H.264 5.6
24 STAP-A Single-time aggregation packet 5.7.1
25 STAP-B Single-time aggregation packet 5.7.1
26 MTAP16 Multi-time aggregation packet 5.7.2
27 MTAP24 Multi-time aggregation packet 5.7.2
28 FU-A Fragmentation unit 5.8
29 FU-B Fragmentation unit 5.8
30-31 undefined
如果type为1-23,表示单一封装模式,表示该负载里面只包含一个NALU
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|NRI| type | |
+-+-+-+-+-+-+-+-+ |
| |
| Bytes 2..n of a Single NAL unit |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
单个NAL单元包的RTP荷载格式
这种情况下,没什么好说的,直接将RTP Payload提取出来就表示一个NALU,因为RTP Payload存放的是一个完整的NALU(去掉了4字节的起始码)。这种方式,rtp头部中的marker字段都置为1,标示是最后一个包。
-
如果type为TYPE_STAP_A(24),组合模式风暴,表示该负载里面只包含多个NALU
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| RTP Header |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 1 Data |
: :
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | NALU 2 Size | NALU 2 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 2 Data |
: :
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
在提取nalu的时候必须去掉第一个字节,第一个字节是nal的类型字段(即STAP-A NAL HDR字段),不是nalu的部分,紧接着nal的类型字段后面的是一个2个字节的nalu长度字段(NALU1 Size),这个长度表示后面一个完整nalu的长度,提取nalu的时候,需要丢掉这2个字节的长度字段,依次处理每一个nalu。在这种情况下,要注意计算该RTP Payload里包含的完整nalu的个数。
如果type为TYPE_FU_A(28),表示这是一个分片包,该rtp包里面只包含一个nalu的分片,需要将多个rtp包的nalu分片重新组合起来形成一个完整的nalu,这种情况适合于当nalu很大,大小超过mtu,不适合单独作为一个RTP包来发送,因此需要分散到多个rtp包来发送。
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
RTP payload format for FU-A
The FU indicator octet has the following format:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
The FU header has the following format:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
FU指示字节的类型域的28,29表示FU-A和FU-B。
FU头的格式如下:
+---------------+
|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
NAL单元荷载类型
解析这种包,首先要获取FU头中的S和E字段,用来指示是否是nalu头或者尾。当为头的时候,表示一个nalu的开始,这个时候要还原该nalu的nal头,还原方法是将FU指示字段的前三位加FU头的后5位组成
nal_unit_type = (fu_indicator & 0xe0) | (fu_header & 0x1f),见nal_header_init(im->b_rptr,nri,type)这个函数实现。
去掉FU指示器和FU头字段,将还原的nalu头放在nalu的开始。
如果不是头,则直接加入到之前重构的nalu后面,必须要去掉fu指示器字段和fu头,fu指示器和fu头不属于NALU的一部分。
在这种情况下,包含最后一个分片的rtp的marker字段置为1,其它置为0
至此,主要的三种基于rtp的H.264包都解析成功了,在丢给ffmpeg解码器进行解码的时候,必须是以nalu的形式数据作为输入,在有些情况下,需要重新在每个nalu的前面添加4个字节的起始码,有些情况下不需要。如果是字节流进行解析的话,必须要加入起始码,否则解码器无法定位每个nalu;如果是一个一个nalu丢给解码器进行解码,则不需要添加起始码。