对于从事于流媒体开发的朋友来说,经常需要对接各种协议的处理。下面我们主要讲一下rtp解包成aac时,对应的重复包,乱序,丢包是如何处理的。
一 C语言的思路
并不是说这个思路不能用于C++,只不过C++有其它更好的方法,而这种思路更适合更方便C语言进行处理。
1 本项目对应的重复包,乱序,丢包的思路是如何处理的:
- 1)重复包:unpack文件中解rtp包时看到,只要有seq不相等,那么就会为该帧记录丢包的标志,把该帧丢弃,即使后面可能收到完整的一帧,实际上是不可能的,因为udp是不会重传的。
- 2)乱序:unpack解rtp包时看到,只要有seq不相等,那么就会为该帧记录丢包的标志,把该帧丢弃,乱序是仍可能收到完整一帧的,不过这里忽略这种情况进行处理了。
- 3)丢包:直接记录该帧丢包,丢弃该帧即可。
2 代码解释:
每一次从网络中拿到数据并且反序列化(大端转小端)后,都会调rtp_payload_check对重复包,乱序,丢包进行检查。
/**
* 参1:实际就是对应的解码结构体,例如类似h264解码时的rtp_decode_h264_t,
* 用于缓存一帧数据,内部的seq与timestamp一直递增,一直保存上一个rtp包的seq与timestamp。
* 参2:反序列化后的临时数据,保存着本次rtp包的相关信息,内部只是简单的指针指向。
*
* 返回值:作用不大,这里没意义,可写成void代表不关心返回值。
*/
int rtp_payload_check(struct rtp_payload_helper_t* helper, const struct rtp_packet_t* pkt)
{
// 1 检查第一个包是否丢包,整个程序有且只会进一次。因为只会在第一个包才会是-1,因为在ctx->decoder->create初始化为-1.
// 这里的作用是:由于收到第一个包时,helper是没有上一包的数据,所以需要人为去赋值制造条件。
// 赋值目的:1)使pkt->rtp.seq减1,目的为了让首包满足条件2,禁止它标注为丢包。
// 2)pkt->rtp.timestamp加1,目的为了满足条件3,不让它相等,让其调用rtp_payload_onframe对上一帧的size和lost进行清0,防止影响下一帧的判断。
// 实际上第一帧size和lost本来就是0,所以应该加不加都一样。试过了就是一样的,并且都能播放。
if (-1 == helper->__flags)
{
// printf("首包,并且全程只会进来一次\n");
helper->__flags = 0; // 在这里时:代表所有rtp的第一个包没有丢包。
helper->seq = (uint16_t)(pkt->rtp.seq - 1);
helper->timestamp = pkt->rtp.timestamp + 1;
}
// 2 检查序列号:本rtp包与上一个rtp包的seq + 1进行比较,若等说明没丢,否则丢包。
int lost = 0; // 本帧的丢包标志
if ((uint16_t)pkt->rtp.seq != (uint16_t)(helper->seq + 1))
{
lost = 1;
//helper->size = 0;
helper->lost = 1;
//helper->flags |= RTP_PAYLOAD_FLAG_PACKET_LOST;
//helper->timestamp = pkt->rtp.timestamp;
}
helper->seq = (uint16_t)pkt->rtp.seq;// 更新本包的seq为上一个rtp包的seq,为下一次比较做准备,本帧丢包也这样处理,因为已经记录lost=1了。
// 3 检查timestamp。这里检查时间戳主要是为了:1)重置上一帧的lost与size数据情况。2)对首包丢包和尾包丢包的情况进行重新记录,防止1)误重置了该帧的丢包情况。
// 注:只有在两帧交界处才会进入该if条件,即有这两种情况:正常两帧时间戳更替(包含了首包丢包情况)以及尾包丢包的情况。
// 同一帧的rtp包是不会进入3的if,因为时间戳是一样的。
// printf("++++++++++++check timestamp++++++++++++helper timestamp:%u, timestamp: %u\n", helper->timestamp, pkt->rtp.timestamp);
if (pkt->rtp.timestamp != helper->timestamp)
{
// printf("check, helper->size:%d, helper->lost:%d\n", helper->size, helper->lost);
rtp_payload_onframe(helper); // 清除上一帧的内容,因为只有两帧交替时才会进来3的if。
// 3.1 对if(0 != lost)的解释:
// 原来英文对if(0 != lost)注释翻译:包丢失在时间戳改变时,不能知道丢失的包是在旧包的尾部还是在新包的开始,所以两个包标记为包丢失。
// if(0 != lost)是因为:若上面2的seq条件不满足,则本包标记丢包,但是调用rtp_payload_onframe之后会清除了该标志,所以必须重新置本帧数据为丢包。
// 1)例如假设第二帧的第一个包丢了,导致上一帧(描述上一包更准确,只不过这个例子称为上一帧也行)的seq+1!=本包,那么刚好又不是同一帧,
// 所以会进入3的if条件。rtp_payload_onframe清掉后必须重置,表示该帧为丢过包的一帧。这个例子好理解,关键是尾包的例子。
//
// 2)假设第二帧的前两2个包没丢,第3个包(尾包,只有3个包)丢了的时候,由于上一包序列号seq+1!=本包seq,并且由于是尾包丢失,所以本包必定是新的一帧的包,
// 故时间戳是不一样的,所以必定会进入3的if,也就是说if(0 != lost)这种情况,会发生在首包丢包或者尾包丢包的情况。
// 不过首包丢还是尾包丢都不重要,因为都是本帧丢包而已。
if(0 != lost)
helper->lost = lost;
}
helper->timestamp = pkt->rtp.timestamp;// helper->timestamp只会保存上一个rtp的时间戳。当一帧分开多个rtp包发送时,实际上这两者的值是一样的。
return 0;
}
rtp_payload_onframe函数实际不用看都行,只要理解上面就基本懂得如何处理重复包,乱序,丢包的了,这里给出是为了方便大家更容易理解。
/**
* 缓存完整的一帧后,调用本函数进行处理,例如这里是写入文件。
* 本函数实际有两个作用:
* 1)从rtp_payload_check进入时:处理丢包的情况或者清除上一帧的数据。
* 2)从pkt.rtp.m进入时:处理没丢包时的一帧数据或者上一帧丢包时的数据清理,但不会清掉RTP_PAYLOAD_FLAG_PACKET_LOST,
* 因为上一帧丢包时,由于在rtp_payload_check检测到丢包,__flags被置为RTP_PAYLOAD_FLAG_PACKET_LOST,并且此时lost=1
* 所以当到遇到m=1时,调用本函数,是不会进入packet处理包并清掉RTP_PAYLOAD_FLAG_PACKET_LOST的。
*
* 参1:缓存着aac完整一帧数据,
* 并且保存着连续递增的seq(1开始按1递增)与timestamp(初始值0按1024递增)的结构体,因为helper始终保存上一个rtp包的seq、timestamp。
*/
int rtp_payload_onframe(struct rtp_payload_helper_t *helper)
{
int r;
r = 0;
// 1 缓存完整一帧后,如果没有丢包,则发送已经缓存好的一帧。rtp_payload_check的时候不会进来if,因为下面return之前size置为0了。
// printf("helper->size:%d, helper->lost:%d\n", helper->size, helper->lost);
if (helper->size > 0
#if !defined(RTP_ENABLE_COURRUPT_PACKET) // 该宏没有定义。但有啥用???
&& 0 == helper->lost // 没有丢包才处理该帧
#endif
)
{
// (这里是没有丢包的情况才会进来。)
// 1.1 拿到完整一帧,调用户函数进行写入,packet是rtp_decode_packet用户解码函数。
// 这里看到,一帧数据是从helper的->ptr中取出的。
r = helper->handler.packet(helper->cbparam, helper->ptr, helper->size,
helper->timestamp, // 用户写入时并未用到时间戳
helper->__flags | (helper->lost ? RTP_PAYLOAD_FLAG_PACKET_CORRUPT : 0));
//0代表没有丢包;否则是丢包。如果他或上__flags,代表首包丢包(flag=-1)或者上一帧丢包(第9bit=1)的话,
//前者的话会弃掉该帧,后者会弃掉上一帧以及随后的下一帧(并未丢包)后,才会继续处理接下来的帧数据。
//建议不用或上__flags,这样即使上一帧丢包也不会影响到没有丢包的下一帧的正常处理,lost=1应该是不包含首包丢包的情况。
//因为首包刚好helper->seq=0时,rtp.seq=1.即0+1=1.这个可以在这里加个if(__flags==-1){不处理包;清除上一帧内容;}
//来解决首包丢包的情况即可。
// 1.2 清除上一帧丢包的标志位,因为本帧是成功发送的。
// 0x0100(16进制)=00000001 00000000(2进制),取反后:11111110 11111111
// 与运算后:xxx & 11111110 11111111;实际上就是清除第9bit的标志位。或者说__flags就是使用int的一个bit。
// 一般该语句运算后都是0。
helper->__flags &= ~RTP_PAYLOAD_FLAG_PACKET_LOST;
}
// (这里是只有在rtp_payload_check时才会进来.)
// 2 如果检测到丢包,在下一帧设置丢包标志(set packet lost flag on next frame),
// 换句话说,就是在下一帧中记录了上一帧是有丢包的,所以上一帧是被舍弃的,可以被用于调整时间戳,__flags具有这个作用,
// 此外还有就是记录了程序第一个rtp包是否丢包的作用。
// 注:这里只有检测到丢包并且helper代表上一帧的情况(时间戳不同了)即rtp_payload_check时才会进来,因为遇到marker=1时是完整收到一帧,helper->lost不会=1
if(helper->lost)
helper->__flags |= RTP_PAYLOAD_FLAG_PACKET_LOST;
// 3 清丢包标志和上一帧缓存(只需清size即可,不需请内存,留着继续使用),用于准备下一帧的缓存。
helper->lost = 0;
helper->size = 0;
return r;
}
二 C++的思路
1 C++可以有更简单方便的思路实现,因为C++有map这种容器给我们调用。
-
1)重复包:通过本次传进来的包的seq与本帧已经保存的全部seq进行比较(所以C++的map.find合适),若存在说明重复,丢弃即可。
-
2)乱序:由于map可以根据first自动排序,所以可以通过遇到marker收完一帧的时候,用for进行遍历是否是按序即可,实际上这一步和下面的3)是一模一样的,所以这一步在C++不需要考虑。
-
3)丢包:同样通过rtp包的marker标志(或者rtp包时间戳,这个不熟),当为1说明该帧结束,然后for循环进行判断,seq末是本末尾包的seq,起始一般是1.然后依次与之前保存的seq比较(例如C++的map的first),seq个个存在说明没丢包。
-
注意:重复包、乱序与丢包的判断都是对一帧数据进行判断,判断完毕就清除本帧的内存,进行下一帧的判断。
代码实例参考这里即可:rtp协议丢包以及包重复判断。