RTP解包

来源:互联网 发布:手机pdf语音朗读软件 编辑:程序博客网 时间:2024/06/04 23:30
这里讨论的场景为,流媒体服务器推送H264码流,且RTP over tcp。
当服务器返回RTSP play 回应后,会发送
 | ‘$’(1byte) | channel number(1byte) | rtp packet size(2byte) |
然后客户端根据size读取随后服务器发送的rtp包。RTP包格式如下:
| RTP header(12bytes) | CSRCs | extension headers | RTP payload | padding |
每个CSRC和extension header长度为4字节,可能含有多个。大多数情况下 CSRCs,extension headers和padding为空,去掉12字节的RTP头部就能得到负载。最好还是分析RTP header得到正确的extension header 和 padding。
RTP头部格式如下:
        |V(2)|P(1)|X(1)|cc(4)|M(1)|PT(7)|seq(16)|timestamp(32)|SSRC(32)|
     * |<--                         header 12 bytes                                                     -->|
        | CSRC ( cc * 4 * bytes) ...
        |(2bytes)|NumOfRemExtHdr(2bytes)| remain extension header ... |
     * |<-- ExtHdr, length = (1+NumOfRemExtHdr)*4 |
这里的没有单位的长度默认单位都是位(bit)。
V代表RTP version,必须为2.
P代表是否有padding。
X代表是否有extension header。

cc代表CSRC的个数。

M: 标记,占1位,不同的有效载荷有不同的含义,对于视频,标记一帧的结束;对于音频,标记会话的开始。

如果X为1,则包含extension header。第一个extension header的前2字节不考虑,之后2字节代表剩下extension headers的个数。
如果P为1,则这个RTP包的最后一个字节代表padding的长度。这个逻辑我是从live555源码中看到的。
根据以上信息可以把多余数据去掉,得到负载。
此外,PT代表负载类型了,我们的例子中是H264,代码为96。

我们所需要的NALU负载装在RTP负载中,而RTP负载格式有多中,取决于RTP负载的第一个字节indicator,该字节包含了NALU包在RTP负载内的结构信息,即NALU包打包方式。该字节格式如下:(单位:bit)
| F(1) | NRI(2) | Type(5)  |
(indicator)
F: forbidden_zero_bit. 在 H.264 规范中规定了这一位必须为 0.
NRI: nal_ref_idc. 似乎指示这个 NALU 的重要性, 如 00 的 NALU 解码器可以丢弃它而不影响图像的回放. 不过一般情况下不太关心这个属性.
Type: nal_unit_type.
  0     没有定义
  1-23  NAL单元  单个 NAL 单元包.
  24    STAP-A   单一时间的组合包
  25    STAP-B   单一时间的组合包
  26    MTAP16   多个时间的组合包
  27    MTAP24   多个时间的组合包
  28    FU-A     分片的单元
  29    FU-B     分片的单元
  30-31 没有定义

在我们的例子中Type字段为24,即 STAP-A   单一时间的组合包,当 NALU 的长度特别小时,可以把几个 NALU 单元封在一个 RTP 包。该类型的打包格式如下:(单位:byte)
| length(2) | header(1) | NALU data | length(2) | header(1) | NALU data | ... |
length包含了头部的长度。头部我们暂不关心,感兴趣可参考:
http://www.360doc.com/content/13/0916/10/9008018_314776620.shtml

好吧,我弄错了,在我们的例子中Type字段为28而非24. 我之前是把这个字节输出到文件,然后查看文件得到的,然而后来在程序里打印这个字节,与文件里不一致。
字节装在 ByteBuffer里,然后写到文件,
fos.getChannel().write(RTPByteBuf);

后来换了个方式,
fos.write(RTPByteBuf.array(), RTPByteBuf.position(), RTPByteBuf.limit() - RTPByteBuf.position());

就跟程序打印出来的结果一致了,不知为何用前一种方法会有问题。

所以在我们的例子中Type字段为28,即FU-A分片的单元,一个NALU被拆分为多个RTP包传输。格式如下:
| FU_A_Header(1byte) | Nalu PL ... |
而FU_A_Header的格式为 : (单位:bit)
| S(1) | E(1) | not care(1) | Nalu Type(5) |
第一位S和第二位E分别为Nalu包开始结束标志位。第三位忽略,后五位是Nalu类型码。具体指代未知。
再重复一遍我们的目标,把 
| Nalu header | Nalu PL | ... | Nalu header | Nalu PL | 
这样的格式写入到文件,然后vlc播放成功。
根据网上资料,当采用FU-A打包方式时,Nalu Header前三位就是indicator前三位,即 | F(1) | NRI(2) |,后五位为FU-A Header的后五位,即| Nalu Type(5) |

据此,我们可以组包了。


更新:

上面说到服务器的采用FA-U的方式传输Nalu,即可以从RTP包的FU_A_Header中读取到顺序,RTP包应该按照 start -> middle ... -> end ->start -> middle -> end ...的顺序,然而测试后发现并非如此,第一个包为M,E后面也不是S,而是M。

在交流群有人说应该看看RTP header的mark标志位。这个标志位也许代表着结束,再加上seq排序,就可以得到一个Nalu。但是我的疑问是FU_A_Header中的起始结束标志位就没用了?


http://blog.csdn.NET/anobodykey/article/details/7876047

这里的逻辑是当Mark标志位为0,即非结束标志,且FU_A_Header标记为起始,就多写入一个起始码: 0x00 0x00 0x00 0x01,然后写入Nalu数据。其他情况则直接写入Nalu数据。

而我例子中FU_A_Header起始结束标志位基本是错误的。所以就直接看M标志位来确定起始结束吧。

默认第一个包为起始包,直到收到M位为1的包作为结束包,然后把NaluHeader与NaluPL写入文件。

多个RTP包组成一个Nalu包,所以每个RTP包中得到的NaluHeader expected都是 相同的。然而我观察我的数据,发现并非如此。我也不知道该怎么处理,就拿第一个RTP包作为正确结果吧。


/*********************** 2016/11/7 更新 ************************/

今天看了live555源码,它逻辑是这样的,读取到RTP包然后去掉RTP header以后根据下一次字节(即Indicator)的后五位确定NalUnit打包类型,

case STAP-A 则skip一个字节

case FU-A or FU-B 则skip两个字节

其他有用信息还没看到。


今天观察了客户端的OutPut,又不一样了,现在是规律的 S  -> M -> ...  -> M -> E 这样顺序的包。那就能解析了。太奇怪了,之前看的时候还是没有规律的。

做了测试,接受2w个RTP包,以下是统计:

status order is not correct. Last status: E, tmp status: M
status order is not correct. Last status: M, tmp status: S

Count For Nalu Type
0 1
8 1
10 1
11 2
12 2
15 1
20 1
21 2
22 1
23 2
24 63
27 1
28 19919
29 1
30 1
31 1

有两次顺序不对,可能是丢包了,那就把整个Nalu丢弃。Nalu类型基本上是28即FU-A,还有一点点24 STAP-A,剩下的几乎可以忽略。

那我们的方案就出来了,只处理24和28两种类型,其他类型则丢弃。然后处理28类型时,顺序不对则丢弃整个Nalu包。


/*********************** 2016/11/8 更新 ************************/

昨天的测试环境为wifi局域网下,收到的数据是按照S  -> M -> ...  -> M -> E的顺序的,是符合预期的,然而今天在4G网络下测试发现顺序完全混乱,看来昨天并不是乱序的问题突然消失了,而是网络的问题。以下是4G网络下的测试结果:
左右分别为顺序和连续出现次数。可以看到没有规律。这样的情况就需要看RTP的Marker标志位了。
M 10
E 1
M 25
S 1
M 6
E 1
M 4
S 1
M 8
E 1
M 1
S 1
M 5
E 1
M 3
S 1
M 6
E 1
M 2
S 1
M 8
E 1
S 1
M 5
E 1

修改代码,打印了Marker,序列号和顺序,以下结果列出了前100个RTP包:

seq: 550, M: false Order: M
seq: 551, M: false Order: M
seq: 552, M: false Order: M
seq: 553, M: false Order: M
seq: 554, M: false Order: M
seq: 555, M: false Order: M
seq: 556, M: false Order: M
seq: 557, M: false Order: M
seq: 558, M: false Order: M
seq: 559, M: false Order: M
seq: 560, M: false Order: M
seq: 561, M: false Order: M
seq: 562, M: false Order: M
seq: 563, M: false Order: M
seq: 564, M: false Order: M
seq: 565, M: false Order: M
seq: 566, M: false Order: M
seq: 567, M: false Order: M
seq: 568, M: false Order: M
seq: 569, M: false Order: M
seq: 570, M: false Order: M
seq: 571, M: false Order: M
seq: 572, M: false Order: M
seq: 573, M: false Order: M
seq: 574, M: false Order: M
seq: 575, M: false Order: M
seq: 576, M: false Order: M
seq: 577, M: false Order: M
seq: 578, M: false Order: M
seq: 579, M: false Order: M
seq: 580, M: false Order: M
seq: 581, M: false Order: M
seq: 585, M: true Order: E
seq: 582, M: false Order: M
seq: 583, M: false Order: M
seq: 584, M: false Order: M
seq: 586, M: false Order: S
seq: 587, M: false Order: M
seq: 588, M: false Order: M
seq: 589, M: false Order: M
seq: 590, M: false Order: M
seq: 591, M: false Order: M
seq: 597, M: true Order: E
seq: 592, M: false Order: M
seq: 593, M: false Order: M
seq: 594, M: false Order: M
seq: 595, M: false Order: M
seq: 596, M: false Order: M
seq: 598, M: false Order: S
seq: 599, M: false Order: M
seq: 600, M: false Order: M
seq: 601, M: false Order: M
seq: 602, M: false Order: M
seq: 603, M: false Order: M
seq: 604, M: false Order: M
seq: 605, M: false Order: M
seq: 608, M: true Order: E
seq: 606, M: false Order: M
seq: 607, M: false Order: M
seq: 609, M: false Order: S
seq: 610, M: false Order: M
seq: 618, M: true Order: E
seq: 611, M: false Order: M
seq: 612, M: false Order: M
seq: 613, M: false Order: M
seq: 614, M: false Order: M
seq: 615, M: false Order: M
seq: 616, M: false Order: M
seq: 617, M: false Order: M
seq: 628, M: true Order: E
seq: 619, M: false Order: S
seq: 620, M: false Order: M
seq: 621, M: false Order: M
seq: 622, M: false Order: M
seq: 623, M: false Order: M
seq: 624, M: false Order: M
seq: 625, M: false Order: M
seq: 626, M: false Order: M
seq: 627, M: false Order: M
seq: 629, M: false Order: S
seq: 630, M: false Order: M
seq: 631, M: false Order: M
seq: 632, M: false Order: M
seq: 633, M: false Order: M
seq: 634, M: false Order: M
seq: 635, M: false Order: M
seq: 638, M: true Order: E
seq: 636, M: false Order: M
seq: 637, M: false Order: M
seq: 639, M: false Order: S
seq: 640, M: false Order: M
seq: 641, M: false Order: M
seq: 642, M: false Order: M
seq: 643, M: false Order: M
seq: 644, M: false Order: M
seq: 645, M: false Order: M
seq: 646, M: false Order: M
seq: 648, M: true Order: E
seq: 647, M: false Order: M
seq: 649, M: false Order: S
seq: 650, M: false Order: M
seq: 651, M: false Order: M
seq: 652, M: false Order: M
seq: 658, M: true Order: E
Num Of FU-A RTP Write To Nalu Buf: 104
Num Of STAP-A RTP: 0
Num Of other RTP: 0

可以看到,当顺序为End的时候,Marker为true,其余均为false。从结果分析,由于4G网络没有局域网稳定,所以出现了RTP序列号顺序错乱的问题,如果序列号顺序正确,那么RTP包中Nalu的顺序也是正确的。接下来就需要进行排序了。


*********************** 2016/11/9 更新 ************************/

打算参考live555源码,将各种NalUnit类型都处理了。

/*********************** 2016/11/10 更新 ***********************/

把live555处理的各种Nalu type的逻辑加进去了,有一种类型解析出错,26.不过2000个包就一个,就不管了。然而测了一遍,解析的数据仍无效,但是live555没有把前导码加到每个Nalu前,找到一篇文章说,需要把前导码加到每个Nalu前 http://blog.csdn.Net/d_l_u_f/article/details/7260772

把前导码就进去以后,又测了一遍,用stream eye打开终于不会出错了,但是画面还是有问题。

/*********************** 2016/11/11 更新 ***********************/

今天没啥进展,目前的状态是,NALU数据应该是没问题了,但是仍然无法播放。在网上找到这样一段话:
你从网络中收包解析出的H264数据应该是不携带SPPS的,SPPS数据应该是由SDP外带的。你需要从SDP中解析出SPPS,然后把这段数据拼接到之前保存的H264数据之前,然后用“Elecard StreamEye Tools”这个工具来播放。如果想直接用VLC播放应该是不行的,因为这个是ES流,需要封装在一个文件容器中才能被一般的播放器来播放。 from http://bbs.csdn.net/topics/360188741

于是将setup时得到的SPS和PPS Base64解码后写入文件,但是测试结果还是不行。

网上提到RTP头部的Marker的作用是:

rtp包的marker参数:marker代表本包是一系列RTP包的结尾,这些RTP包携带的是分片数据,一般是I帧,因为单帧太大进行分片,每个RTP包发送一片,marker=false,最后一包marker=ture。而如果帧数据很小,一般是BP帧,一帧或者几帧被一个RTP包发送,则marker=true。也即marker代表完整帧(一帧或几帧)。 from http://blog.csdn.net/blwinner/article/details/51096911

后续添上对Marker的处理,当Marker为true时才停止以获得完整帧。


/*********************** 2016/11/16 更新 ***********************/

到今天为止,接收到的数据以及能用Stream Eye正常播放了。根据与原始H264文件对比,发现需要在sps和pps前都加入前导码,然后在之后的每个Nalu单元前加入前导码,之前代码有问题,Nalu前的前导码加的有问题。所以正确的格式应该是 StartCode + SPS + StartCode + PPS + StartCode + Nalu1 + StartCode + Nalu2 + ... + StartCode + Nalun. 这样的格式就可以用Stream Eye播放了。


未完待续


参考:
http://www.cppblog.com/czanyou/archive/2008/11/26/67940.html
https://my.oschina.net/u/1431835/blog/393315

http://www.cnweblog.com/fly2700/archive/2012/02/23/319718.html

原创粉丝点击