基于android手机实时监控ipcam视频之三:H.264的RTP打包解析

时间:2022-05-15 09:32:48

    因为项目中,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   |
      +---------------+

FU指示字节有以下格式:
      +---------------+
      |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丢给解码器进行解码,则不需要添加起始码。