音视频入门基础:RTCP专题(5)——《RFC 3550》的附录A
一、引言
本文对应《RFC 3550》的附录A(Appendix A. Algorithms)。
二、Appendix A. Algorithms
根据《RFC 3550》第62页,《RFC 3550》提供了有关RTP发送方和接收方算法的C代码示例。在特定的运行环境下,可能还有其他更快或更有优势的实现方法。这些实施说明仅供参考,旨在阐明RTP规范。
以下定义用于所有示例;为简洁明了起见,结构定义(structure definitions)仅适用于32位大八位(32-bit big-endian,最重要八位在前)架构。比特字段(Bit fields)假定按大十进制比特顺序(big-endian bit order)紧密排列,没有额外的填充。要构建可移植的实现,需要进行修改:
/*
* rtp.h -- RTP header file
*/
#include <sys/types.h>/*
* The type definitions below are valid for 32-bit architectures and
* may have to be adjusted for 16- or 64-bit architectures.
*/
typedef unsigned char u_int8;
typedef unsigned short u_int16;
typedef unsigned int u_int32;
typedef short int16;/*
* Current protocol version.
*/
#define RTP_VERSION 2
#define RTP_SEQ_MOD (1<<16)
#define RTP_MAX_SDES 255 /* maximum text length for SDES */typedef enum {RTCP_SR = 200,RTCP_RR = 201,RTCP_SDES = 202,RTCP_BYE = 203,RTCP_APP = 204
} rtcp_type_t;typedef enum {RTCP_SDES_END = 0,RTCP_SDES_CNAME = 1,RTCP_SDES_NAME = 2,RTCP_SDES_EMAIL = 3,RTCP_SDES_PHONE = 4,RTCP_SDES_LOC = 5,RTCP_SDES_TOOL = 6,RTCP_SDES_NOTE = 7,RTCP_SDES_PRIV = 8
} rtcp_sdes_type_t;/*
* RTP data header
*/
typedef struct {unsigned int version:2; /* protocol version */unsigned int p:1; /* padding flag */unsigned int x:1; /* header extension flag */unsigned int cc:4; /* CSRC count */unsigned int m:1; /* marker bit */unsigned int pt:7; /* payload type */unsigned int seq:16; /* sequence number */u_int32 ts; /* timestamp */u_int32 ssrc; /* synchronization source */u_int32 csrc[1]; /* optional CSRC list */
} rtp_hdr_t;/*
* RTCP common header word
*/
typedef struct {unsigned int version:2; /* protocol version */unsigned int p:1; /* padding flag */unsigned int count:5; /* varies by packet type */unsigned int pt:8; /* RTCP packet type */u_int16 length; /* pkt len in words, w/o this word */
} rtcp_common_t;/*
* Big-endian mask for version, padding bit and packet type pair
*/
#define RTCP_VALID_MASK (0xc000 | 0x2000 | 0xfe)
#define RTCP_VALID_VALUE ((RTP_VERSION << 14) | RTCP_SR)
/*
* Reception report block
*/
typedef struct {u_int32 ssrc; /* data source being reported */unsigned int fraction:8; /* fraction lost since last SR/RR */int lost:24; /* cumul. no. pkts lost (signed!) */u_int32 last_seq; /* extended last seq. no. received */u_int32 jitter; /* interarrival jitter */u_int32 lsr; /* last SR packet from this source */u_int32 dlsr; /* delay since last SR packet */
} rtcp_rr_t;/*
* SDES item
*/
typedef struct {u_int8 type; /* type of item (rtcp_sdes_type_t) */u_int8 length; /* length of item (in octets) */char data[1]; /* text, not null-terminated */
} rtcp_sdes_item_t;/*
* One RTCP packet
*/
typedef struct {rtcp_common_t common; /* common header */union {/* sender report (SR) */struct {u_int32 ssrc; /* sender generating this report */u_int32 ntp_sec; /* NTP timestamp */u_int32 ntp_frac;u_int32 rtp_ts; /* RTP timestamp */u_int32 psent; /* packets sent */u_int32 osent; /* octets sent */rtcp_rr_t rr[1]; /* variable-length list */} sr;/* reception report (RR) */struct {u_int32 ssrc; /* receiver generating this report */rtcp_rr_t rr[1]; /* variable-length list */} rr;/* source description (SDES) */struct rtcp_sdes {u_int32 src; /* first SSRC/CSRC */rtcp_sdes_item_t item[1]; /* list of SDES items */} sdes;/* BYE */struct {u_int32 src[1]; /* list of sources *//* can’t express trailing text for reason */} bye;} r;
} rtcp_t;typedef struct rtcp_sdes rtcp_sdes_t;/*
* Per-source state information
*/
typedef struct {u_int16 max_seq; /* highest seq. number seen */u_int32 cycles; /* shifted count of seq. number cycles */u_int32 base_seq; /* base seq number */u_int32 bad_seq; /* last ’bad’ seq number + 1 */u_int32 probation; /* sequ. packets till source is valid */u_int32 received; /* packets received */u_int32 expected_prior; /* packet expected at last interval */u_int32 received_prior; /* packet received at last interval */u_int32 transit; /* relative trans time for prev pkt */u_int32 jitter; /* estimated jitter */
/* ... */
} source;
三、RTP Data Header Validity Checks
根据《RFC 3550》第65页,RTP接收器应检查传入数据包上RTP标头(RTP header)的有效性(validity ),因为这些数据包可能被加密,或可能来自不同的应用程序,而这些应用程序恰好被错误编址。同样,如果启用了《RFC 3550》第 9节所述的加密方法,则需要进行报头有效性检查,以验证传入数据包是否已正确解密,尽管报头有效性检查(header validity check)失败(如未知有效载荷类型)不一定表示解密失败:
1.RTP版本字段(RTP version field)必须等于2。
2.必须知道有效载荷类型(payload type),尤其不能等于SR或RR。
3.如果设置了P位(P bit),那么数据包的最后一个八位位组必须包含有效的八位位组计数,特别是小于数据包总长度减去报头(header)大小。
4.如果配置文件(profile)未指定可以使用报头扩展(header extension)机制,则X位(X bit)必须为0。否则,扩展长度(extension length)字段必须小于数据包总大小减去固定报头(fixed header)长度和填充。
5.数据包的长度必须与CC和有效载荷类型(payload type)一致(如果有效载荷有已知长度)。
后三项检查有些复杂,而且并不总是可行,因此只剩下前两项,总共只有几个比特。如果数据包中的SSRC标识符(SSRC identifier)是以前接收过的标识符,那么数据包可能是有效的,检查序列号是否在预期范围内可进一步验证。如果SSRC标识符以前从未出现过,那么携带该标识符的数据包可能会被视为无效,直到有少量带有连续序列号(sequence numbers)的数据包到达为止。这些无效数据包可能会被丢弃,也可能会被储存起来,在验证完成后再发送,如果由此造成的延迟可以接受的话。
下图所示的例程update_seq可确保只有在按顺序收到MIN_SEQUENTIAL数据包后,才宣布数据源有效。它还会验证新接收数据包的序列号seq,并更新s所指向结构中数据包源的序列状态。
当首次听到一个新信源时,即其SSRC标识不在表中(见《RFC 3550》第8.2节),并为其分配了每个信源状态时,s->probation将被设置为宣布信源有效前所需的连续数据包数(参数 MIN_SEQUENTIAL),其他变量也将被初始化:
init_seq(s, seq);
s->max_seq = seq - 1;
s->probation = MIN_SEQUENTIAL;
非0的s->probation会将源代码标记为无效,因此状态可能会在短时间超时后而不是长时间超时后被丢弃,《RFC 3550》第6.2.1节对此进行了讨论。
在源被视为有效后,如果序列号比s->max_seq超前不超过MAX_DROPOUT,也不落后于 MAX_MISORDER,则序列号被视为有效。如果新的序列号在 RTP 序列号范围(16 位)的模数 max_seq 之前,但小于 max_seq,则表示序列号已被包围,序列号周期的(移位)计数将递增。
否则,返回值为 0,表示验证失败,并存储坏序列号(bad sequence number)加 1。如果接收到的下一个数据包带有下一个更高的序列号,则该数据包被认为是新数据包序列的有效起始,可能是由于扩展的丢包或源重启造成的。由于可能遗漏了多个完整的序列号周期,因此数据包丢失统计(packet loss statistics)将被重置。
所示为参数的典型值,基于50个数据包/秒的最大错序时间为2 秒,最大丢包时间为1分钟。丢弃参数MAX_DROPOUT应为16位序列号空间的一小部分,以提供一个合理的概率,即重启后的新序列号不会落在重启前序列号的可接受范围内。
void init_seq(source *s, u_int16 seq)
{
s->base_seq = seq;
s->max_seq = seq;
s->bad_seq = RTP_SEQ_MOD + 1; /* so seq == bad_seq is false */
s->cycles = 0;
s->received = 0;
s->received_prior = 0;
s->expected_prior = 0;
/* other initialization */
}
int update_seq(source *s, u_int16 seq)
{
u_int16 udelta = seq - s->max_seq;
const int MAX_DROPOUT = 3000;
const int MAX_MISORDER = 100;
const int MIN_SEQUENTIAL = 2;
/*
* Source is not valid until MIN_SEQUENTIAL packets with
* sequential sequence numbers have been received.
*/
if (s->probation) {
/* packet is in sequence */
if (seq == s->max_seq + 1) {
s->probation--;
s->max_seq = seq;
if (s->probation == 0) {
init_seq(s, seq);
s->received++;
return 1;
}
} else {
s->probation = MIN_SEQUENTIAL - 1;
s->max_seq = seq;
}
return 0;
} else if (udelta < MAX_DROPOUT) {
/* in order, with permissible gap */
if (seq < s->max_seq) {
/*
* Sequence number wrapped - count another 64K cycle.
*/
s->cycles += RTP_SEQ_MOD;
}
s->max_seq = seq;
} else if (udelta <= RTP_SEQ_MOD - MAX_MISORDER) {
/* the sequence number made a very large jump */
if (seq == s->bad_seq) {
/*
* Two sequential packets -- assume that the other side
* restarted without telling us so just re-sync
* (i.e., pretend this was the first packet).
*/
init_seq(s, seq);
}
else {
s->bad_seq = (seq + 1) & (RTP_SEQ_MOD-1);
return 0;
}
} else {
/* duplicate or reordered packet */
}
s->received++;
return 1;
}
可以加强有效性检查,要求两个以上的数据包依次传送。其缺点是会有更多的初始数据包被丢弃(或在队列中延迟),而且高数据包丢失率可能会妨碍验证。不过,由于RTCP报头验证相对较强,如果在数据包之前收到来自源的RTCP数据包,则可以调整计数,使其只需要两个数据包的顺序。如果可以容忍几秒钟的初始数据丢失,应用程序可以选择丢弃来自数据源的所有数据包,直到收到来自该数据源的有效RTCP数据包。
根据应用和编码的不同,算法可能会利用有关有效载荷格式的其他知识进行进一步验证。对于所有数据包的时间戳增量都相同的有效载荷类型,可以利用序列号差值(假设有效载荷类型没有变化)从同一来源收到的前一个数据包中预测时间戳值。
可以进行强 “快速路径(fast-path)”检查,因为新接收到的RTP数据包头部的前四个八进制数很可能与来自同一SSRC的上一个数据包的前四个八进制数相同,只是序列号增加了一个。同样,在通常一次只从一个数据源接收数据的应用中,单入口高速缓存(single-entry cache)可用于加快SSRC查找速度。
四、RTCP Header Validity Checks
根据《RFC 3550》第69页,应对RTCP数据包进行以下检查。
1.RTP版本(RTP version)字段必须等于2。
2.复合数据包中第一个RTCP数据包的有效载荷类型(payload type )字段必须等于 SR或RR。
3.复合RTCP数据包的第一个数据包的填充位 (P) 应为0,因为只有在需要时才会在最后一个数据包中使用填充。
4.单个RTCP数据包的长度(length)字段必须与接收到的复合 RTCP 数据包的总长度相加。这是一种相当严格的检查。
下面的代码片段将执行所有这些检查。对后续数据包不检查数据包类型,因为可能存在未知的数据包类型,应予以忽略:
u_int32 len; /* length of compound RTCP packet in words */
rtcp_t *r; /* RTCP header */
rtcp_t *end; /* end of compound RTCP packet */
if ((*(u_int16 *)r & RTCP_VALID_MASK) != RTCP_VALID_VALUE) {
/* something wrong with packet format */
}
end = (rtcp_t *)((u_int32 *)r + len);
do r = (rtcp_t *)((u_int32 *)r + r->common.length + 1);
while (r < end && r->common.version == 2);
if (r != end) {
/* something wrong with packet format */
}
五、Determining Number of Packets Expected and Lost
根据《RFC 3550》第69页,为了计算数据包丢失率,需要使用下面代码中通过指针s引用的struct source中定义的每个数据源状态信息,了解每个数据源预计和实际接收到的RTP数据包数量。接收到的数据包数量只是数据包到达时的计数,包括任何延迟或重复的数据包。接收方可以根据收到的最高序列号(s->max_seq)和收到的第一个序列号(s->base_seq)之差来计算预计的数据包数量。由于序列号只有16 位并会缠绕(wrap around),因此有必要用序列号缠绕的(移位)次数(s->cycles)来扩展最高序列号。接收到的数据包计数和周期计数都由《RFC 3550》附录A.1中的RTP报头有效性检查(RTP header validity check)例程进行维护。
extended_max = s->cycles + s->max_seq;
expected = extended_max - s->base_seq + 1;
数据包丢失数(The number of packets lost)的定义是预期数据包数减去实际收到的数据包数:
lost = expected - s->received;
由于这个带符号的数字以24位为单位,因此,如果是正损耗,则应将其箝位(clamped at)在0x7fffff处,如果是负损耗,则应将其箝位在0x800000处,而不是绕来绕去。
上次报告间隔内(自上次SR或RR 数据包发送后)丢失的数据包数量是根据整个间隔内预计数据包数量和已接收数据包数量的差异计算得出的,其中预计数据包数量和已接收数据包数量是生成上次接收报告时保存的值:
expected_interval = expected - s->expected_prior;
s->expected_prior = expected;
received_interval = s->received - s->received_prior;
s->received_prior = s->received;
lost_interval = expected_interval - received_interval;
if (expected_interval == 0 || lost_interval <= 0) fraction = 0;
else fraction = (lost_interval << 8) / expected_interval;
得出的分数是一个8位定点数,二进制点位于左边缘。
六、Generating RTCP SDES Packets
该函数在缓冲区b中建立一个SDES块,缓冲区b由数组type、value和length中的argc项组成。它返回一个指向b中下一个可用位置的指针:
char *rtp_write_sdes(char *b, u_int32 src, int argc,
rtcp_sdes_type_t type[], char *value[],
int length[])
{
rtcp_sdes_t *s = (rtcp_sdes_t *)b;
rtcp_sdes_item_t *rsp;
int i;
int len;
int pad;
/* SSRC header */
s->src = src;
rsp = &s->item[0];
/* SDES items */
for (i = 0; i < argc; i++) {
rsp->type = type[i];
len = length[i];
if (len > RTP_MAX_SDES) {
/* invalid length, may want to take other action */
len = RTP_MAX_SDES;
}
rsp->length = len;
memcpy(rsp->data, value[i], len);
rsp = (rtcp_sdes_item_t *)&rsp->data[len];
}
/* terminate with end marker and pad to next 4-octet boundary */
len = ((char *) rsp) - b;
pad = 4 - (len & 0x3);
b = (char *) rsp;
while (pad--) *b++ = RTCP_SDES_END;
return b;
}
七、Parsing RTCP SDES Packets
该函数解析SDES数据包,调用function find_member() 和 member_sdes(),前者用于根据SSRC 标识找到指向会话成员信息的指针,后者用于存储该成员的新SDES信息。该函数需要一个指向 RTCP数据包头部的指针:
void rtp_read_sdes(rtcp_t *r)
{
int count = r->common.count;
rtcp_sdes_t *sd = &r->r.sdes;
rtcp_sdes_item_t *rsp, *rspn;
rtcp_sdes_item_t *end = (rtcp_sdes_item_t *)
((u_int32 *)r + r->common.length + 1);
source *s;
while (--count >= 0) {
rsp = &sd->item[0];
if (rsp >= end) break;
s = find_member(sd->src);
for (; rsp->type; rsp = rspn ) {
rspn = (rtcp_sdes_item_t *)((char*)rsp+rsp->length+2);
if (rspn >= end) {
rsp = rspn;
break;
}
member_sdes(s, rsp->type, rsp->data, rsp->length);
}
sd = (rtcp_sdes_t *)
((u_int32 *)sd + (((char *)rsp - (char *)sd) >> 2)+1);
}
if (count >= 0) {
/* invalid packet format */
}
}
八、Generating a Random 32-bit Identifier
下面的子程序使用《RFC 1321》中公布的MD5例程生成一个32位随机标识符。这些系统例程可能不存在于所有操作系统中,但它们可以作为提示,说明可以使用哪些类型的信息。其他适用的系统调用包括:
1.getdomainname(),
2.getwd(),或
3.getrusage().
“实时”视频或音频样本也是很好的随机数源,但必须注意避免使用关闭的麦克风或盲目的摄像头作为随机数源。
建议使用该例程或类似例程为随机数生成器生成RTCP周期的初始种子(如《RFC 3550》附录A.7 所示),生成序列号(sequence number)和时间戳(timestamp)的初始值,并生成SSRC值。由于该例程可能需要大量CPU资源,因此不适合直接用于生成RTCP周期(RTCP periods),因为可预测性不是问题。请注意,除非为类型参数提供不同的值,否则重复调用此例程将产生相同的结果,直到系统时钟的值发生变化:
/*
* Generate a random 32-bit quantity.
*/
#include <sys/types.h> /* u_long */
#include <sys/time.h> /* gettimeofday() */
#include <unistd.h> /* get..() */
#include <stdio.h> /* printf() */
#include <time.h> /* clock() */
#include <sys/utsname.h> /* uname() */
#include "global.h" /* from RFC 1321 */
#include "md5.h" /* from RFC 1321 */
#define MD_CTX MD5_CTX
#define MDInit MD5Init
#define MDUpdate MD5Update
#define MDFinal MD5Final
static u_long md_32(char *string, int length)
{
MD_CTX context;
union {
char c[16];
u_long x[4];
} digest;
u_long r;
int i;
MDInit (&context);
MDUpdate (&context, string, length);
MDFinal ((unsigned char *)&digest, &context);
r = 0;
for (i = 0; i < 3; i++) {
r ^= digest.x[i];
}
return r;
} /* md_32 */
/*
* Return random unsigned 32-bit quantity. Use ’type’ argument if
* you need to generate several different values in close succession.
*/
u_int32 random32(int type)
{
struct {
int type;
struct timeval tv;
clock_t cpu;
pid_t pid;
u_long hid;
uid_t uid;
gid_t gid;
struct utsname name;
} s;
gettimeofday(&s.tv, 0);
uname(&s.name);
s.type = type;
s.cpu = clock();
s.pid = getpid();
s.hid = gethostid();
s.uid = getuid();
s.gid = getgid();
/* also: system uptime */
return md_32((char *)&s, sizeof(s));
} /* random32 */
九、Computing the RTCP Transmission Interval
以下函数实现了《RFC 3550》第6.2 节中描述的 RTCP发送和接收规则。这些规则编码在多个函数中:
1.rtcp interval() :以秒为单位计算确定性计算的时间间隔。参数定义见《RFC 3550》第6.3节。
2.OnExpire():当RTCP传输计时器到期时,会调用OnExpire()。
3.OnReceive():每当接收到RTCP数据包时,就会调用OnReceive()。
OnExpire() 和OnReceive() 的参数都是事件e。这是该参与者的下一个预定事件,可以是RTCP报告,也可以是BYE数据包。假定以下函数可用:
1.Schedule(time t, event e) :安排事件e在时间t发生。
2.Reschedule(time t, event e) :将先前安排的事件e重新安排到时间t。
3.SendRTCPReport(event e):发送RTCP报告。
4.SendBYEPacket(event e) :发送BYE数据包。
5.TypeOfEvent(event e) :如果处理的事件是要发送BYE数据包,则返回EVENT BYE,否则返回 EVENT REPORT。
6.PacketType(p) :如果数据包p是RTCP报告(非 BYE),PacketType(p) 将返回PACKET RTCP REPORT;如果是BYE RTCP数据包,则返回PACKET BYE;如果是普通RTP数据包,则返回PACKET RTP。
7.ReceivedPacketSize() 和 SentPacketSize() :以八进制为单位返回引用数据包的大小。
8.NewMember(p) :如果发送信息包p的参与者当前不在成员列表中,NewMember(p) 返回1,否则返回0。需要注意的是,这个函数还不足以实现完整的功能,因为RTP数据包中的每个CSRC标识和BYE数据包中的每个SSRC都需要处理。
9.NewSender(p) :如果发送数据包p的参与者目前不在成员列表的发送者子列表中,NewSender(p) 返回1,否则返回0。
10.AddMember() 和 RemoveMember() :从成员名单中添加和删除参与者。
11.AddSender() 和 RemoveSender() :用于从成员列表的发件人子列表中添加和删除参与者。
必须对这些函数进行扩展,以实现允许将发送方和非发送方的RTCP带宽分数指定为明确参数,而不是25%和75%的固定值。如果其中一个参数为零,rtcp interval() 的扩展实现将需要避免除以零。
double rtcp_interval(int members,
int senders,
double rtcp_bw,
int we_sent,
double avg_rtcp_size,
int initial)
{
/*
* Minimum average time between RTCP packets from this site (in
* seconds). This time prevents the reports from ‘clumping’ when
* sessions are small and the law of large numbers isn’t helping
* to smooth out the traffic. It also keeps the report interval
* from becoming ridiculously small during transient outages like
* a network partition.
*/
double const RTCP_MIN_TIME = 5.;
/*
* Fraction of the RTCP bandwidth to be shared among active
* senders. (This fraction was chosen so that in a typical
* session with one or two active senders, the computed report
* time would be roughly equal to the minimum report time so that
* we don’t unnecessarily slow down receiver reports.) The
* receiver fraction must be 1 - the sender fraction.
*/
double const RTCP_SENDER_BW_FRACTION = 0.25;
double const RTCP_RCVR_BW_FRACTION = (1-RTCP_SENDER_BW_FRACTION);
/*
/* To compensate for "timer reconsideration" converging to a
* value below the intended average.
*/
double const COMPENSATION = 2.71828 - 1.5;
double t; /* interval */
double rtcp_min_time = RTCP_MIN_TIME;
int n; /* no. of members for computation */
/*
* Very first call at application start-up uses half the min
* delay for quicker notification while still allowing some time
* before reporting for randomization and to learn about other
* sources so the report interval will converge to the correct
* interval more quickly.
*/
if (initial) {
rtcp_min_time /= 2;
}
/*
* Dedicate a fraction of the RTCP bandwidth to senders unless
* the number of senders is large enough that their share is
* more than that fraction.
*/
n = members;
if (senders <= members * RTCP_SENDER_BW_FRACTION) {
if (we_sent) {
rtcp_bw *= RTCP_SENDER_BW_FRACTION;
n = senders;
} else {
rtcp_bw *= RTCP_RCVR_BW_FRACTION;
n -= senders;
}
}
/*
* The effective number of sites times the average packet size is
* the total number of octets sent when each site sends a report.
* Dividing this by the effective bandwidth gives the time
* interval over which those packets must be sent in order to
* meet the bandwidth target, with a minimum enforced. In that
* time interval we send one report so this time is also our
* average time between reports.
*/
t = avg_rtcp_size * n / rtcp_bw;
if (t < rtcp_min_time) t = rtcp_min_time;
/*
* To avoid traffic bursts from unintended synchronization with
* other sites, we then pick our actual next report interval as a
* random number uniformly distributed between 0.5*t and 1.5*t.
*/
t = t * (drand48() + 0.5);
t = t / COMPENSATION;
return t;
}
void OnExpire(event e,
int members,
int senders,
double rtcp_bw,
int we_sent,
double *avg_rtcp_size,
int *initial,
time_tp tc,
time_tp *tp,
int *pmembers)
{
/* This function is responsible for deciding whether to send an
* RTCP report or BYE packet now, or to reschedule transmission.
* It is also responsible for updating the pmembers, initial, tp,
* and avg_rtcp_size state variables. This function should be
* called upon expiration of the event timer used by Schedule().
*/
double t; /* Interval */
double tn; /* Next transmit time */
/* In the case of a BYE, we use "timer reconsideration" to
* reschedule the transmission of the BYE if necessary */
if (TypeOfEvent(e) == EVENT_BYE) {
t = rtcp_interval(members,
senders,
rtcp_bw,
we_sent,
*avg_rtcp_size,
*initial);
tn = *tp + t;
if (tn <= tc) {
SendBYEPacket(e);
exit(1);
} else {
Schedule(tn, e);
}
} else if (TypeOfEvent(e) == EVENT_REPORT) {
t = rtcp_interval(members,
senders,
rtcp_bw,
we_sent,
*avg_rtcp_size,
*initial);
tn = *tp + t;
if (tn <= tc) {
SendRTCPReport(e);
*avg_rtcp_size = (1./16.)*SentPacketSize(e) +
(15./16.)*(*avg_rtcp_size);
*tp = tc;
/* We must redraw the interval. Don’t reuse the
one computed above, since its not actually
distributed the same, as we are conditioned
on it being small enough to cause a packet to
be sent */
t = rtcp_interval(members,
senders,
rtcp_bw,
we_sent,
*avg_rtcp_size,
*initial);
Schedule(t+tc,e);
*initial = 0;
} else {
Schedule(tn, e);
}
*pmembers = members;
}
}
void OnReceive(packet p,
event e,
int *members,
int *pmembers,
int *senders,
double *avg_rtcp_size,
double *tp,
double tc,
double tn)
{
/* What we do depends on whether we have left the group, and are
* waiting to send a BYE (TypeOfEvent(e) == EVENT_BYE) or an RTCP
* report. p represents the packet that was just received. */
if (PacketType(p) == PACKET_RTCP_REPORT) {
if (NewMember(p) && (TypeOfEvent(e) == EVENT_REPORT)) {
AddMember(p);
*members += 1;
}
*avg_rtcp_size = (1./16.)*ReceivedPacketSize(p) +
(15./16.)*(*avg_rtcp_size);
} else if (PacketType(p) == PACKET_RTP) {
if (NewMember(p) && (TypeOfEvent(e) == EVENT_REPORT)) {
AddMember(p);
*members += 1;
}
if (NewSender(p) && (TypeOfEvent(e) == EVENT_REPORT)) {
AddSender(p);
*senders += 1;
}
} else if (PacketType(p) == PACKET_BYE) {
*avg_rtcp_size = (1./16.)*ReceivedPacketSize(p) +
(15./16.)*(*avg_rtcp_size);
if (TypeOfEvent(e) == EVENT_REPORT) {
if (NewSender(p) == FALSE) {
RemoveSender(p);
*senders -= 1;
}
if (NewMember(p) == FALSE) {
RemoveMember(p);
*members -= 1;
}
if (*members < *pmembers) {
tn = tc +
(((double) *members)/(*pmembers))*(tn - tc);
*tp = tc -
(((double) *members)/(*pmembers))*(tc - *tp);
/* Reschedule the next report for time tn */
Reschedule(tn, e);
*pmembers = *members;
}
} else if (TypeOfEvent(e) == EVENT_BYE) {
*members += 1;
}
}
}
十、Estimating the Interarrival Jitter
下面的代码片段实现了《RFC 3550》第6.4.1节中给出的算法,用于计算 RTP数据到达时间统计方差的估计值,并将其插入接收报告的到达时间抖动字段中。输入为r->ts,即传入数据包的时间戳,以及arrival,即当前时间(单位相同)。接收报告的抖动字段以时间戳为单位,用无符号整数表示,但抖动估计值用浮点数表示。每个数据包到达时,抖动估计值都会更新:
int transit = arrival - r->ts;
int d = transit - s->transit;
s->transit = transit;
if (d < 0) d = -d;
s->jitter += (1./16.) * ((double)d - s->jitter);
为该成员生成接收报告块(rr指向该报告块)时,将返回当前的抖动估计值:
rr->jitter = (u_int32) s->jitter;
或者,抖动估计值可以保留为整数,但可以按比例缩放以减少舍入误差。除最后一行外,计算方法相同:
s->jitter += d - ((s->jitter + 8) >> 4);
在这种情况下,接收报告的估计值取样为:
rr->jitter = s->jitter >> 4;
相关文章:
音视频入门基础:RTCP专题(5)——《RFC 3550》的附录A
一、引言 本文对应《RFC 3550》的附录A(Appendix A. Algorithms)。 二、Appendix A. Algorithms 根据《RFC 3550》第62页,《RFC 3550》提供了有关RTP发送方和接收方算法的C代码示例。在特定的运行环境下,可能还有其他更快或更有…...
qemu仿真调试esp32,以及安装版和vscode版配置区别
不得不说,乐鑫在官网的qemu介绍真的藏得很深 首先在首页的sdk的esp-idf页面里找找 然后页面拉倒最下面 入门指南 我这里选择esp32-s3 再点击api指南-》工具 才会看到qemu的介绍 QEMU 模拟器 - ESP32-C3 - — ESP-IDF 编程指南 latest 文档https://docs.espressi…...
协方差相关问题
为什么无偏估计用 ( n − 1 ) (n-1) (n−1) 而不是 n n n,区别是什么? 在统计学中,无偏估计是指估计量的期望值等于总体参数的真实值。当我们用样本数据估计总体方差或协方差时,分母使用 ( n − 1 ) (n-1) (n−1) 而不是 n n…...
Android OpenCV 人脸识别 识别人脸框 识别人脸控件自定义
先看效果 1.下载OpenCV 官网地址:opcv官网 找到Android 4.10.0版本下载 下载完毕 解压zip如图: 2.将OpenCV-android_sdk导入项目 我这里用的最新版的Android studio 如果是java开发 需要添加kotlin的支持。我用的studio比较新可以参考下,如果…...
深入解析Linux软硬链接:原理、区别与应用实践
Linux系列 文章目录 Linux系列前言一、软硬链接的概念引入1.1 硬链接1.2 软链接 二、软硬链接的使用场景2.1 软链接2.2 硬链接 三、总结 前言 上篇文章我们详细的介绍了文件系统的概念及底层实现原理,本篇我们就在此基础上探讨Linux系统中文件的软链接࿰…...
TDengine 与 taosAdapter 的结合(二)
五、开发实战步骤 (一)环境搭建 在开始 TDengine 与 taosAdapter 结合的 RESTful 接口开发之前,需要先完成相关环境的搭建,包括 TDengine 和 taosAdapter 的安装与配置,以及相关依赖的安装。 TDengine 安装…...
OBS 中如何设置固定码率(CBR)与可变码率(VBR)?
在使用 OBS 进行录制或推流时,设置“码率控制模式”(Rate Control)是非常重要的一步。常见的控制模式包括: CBR(固定码率):保持恒定的输出码率,适合直播场景。 VBR(可变码率):在允许的范围内动态调整码率,适合本地录制、追求画质。 一、CBR vs. VBR 的差异 项目CBR…...
优艾智合人形机器人“巡霄”,开启具身多模态新时代
近日,优艾智合-西安交大具身智能机器人研究院公布人形机器人矩阵,其中轮式人形机器人“巡霄”首次亮相。 “巡霄”集成移动导航、操作控制与智能交互技术,具备跨场景泛化能,适用于家庭日常服务、电力设备巡检、半导体精密操作及仓…...
蓝桥杯小白打卡第七天(第十四届真题)
小蓝的金属冶炼转换率问题 小蓝有一个神奇的炉子用于将普通金属 (O) 冶炼成为一种特殊金属 (X) 。 这个炉子有一个称作转换率的属性 (V) ,(V) 是一个正整数,这意味着消耗 (V) 个普通金属 (O) 恰好可以冶炼出一个特殊金属 (X) ,当普通金属 (…...
excel经验
Q:我现在有一个excel,有一列数据,大概两千多行。如何在这一列中 筛选出具有关键字的内容,并输出到另外一列中。 A: 假设数据在A列(A1开始),关键字为“ABC”在相邻空白列(如B1)输入公…...
【Pandas】pandas DataFrame astype
Pandas2.2 DataFrame Conversion 方法描述DataFrame.astype(dtype[, copy, errors])用于将 DataFrame 中的数据转换为指定的数据类型 pandas.DataFrame.astype pandas.DataFrame.astype 是一个方法,用于将 DataFrame 中的数据转换为指定的数据类型。这个方法非常…...
【Netty4核心原理④】【简单实现 Tomcat 和 RPC框架功能】
文章目录 一、前言二、 基于 Netty 实现 Tomcat1. 基于传统 IO 重构 Tomcat1.1 创建 MyRequest 和 MyReponse 对象1.2 构建一个基础的 Servlet1.3 创建用户业务代码1.4 完成web.properties 配置1.5 创建 Tomcat 启动类 2. 基于 Netty 重构 Tomcat2.1 创建 NettyRequest和 Netty…...
4.6学习总结
包装类 包装类:基本数据类型对应的引用数据类型 JDK5以后新增了自动装箱,自动拆箱 以后获取包装类方法,不需要new,直接调用方法,直接赋值即可 //1.把整数转成二进制,十六进制 String str1 Integer.toBin…...
MySQL学习笔记五
第七章数据过滤 7.1组合WHERE子句 7.1.1AND操作符 输入: SELECT first_name, last_name, salary FROM employees WHERE salary < 4800 AND department_id 60; 输出: 说明:MySQL允许使用多个WHERE子句,可以以AND子句或OR…...
成为社交场的导演而非演员
一、情绪的本质:社交信号而非自我牢笼 进化功能:情绪是人类进化出的原始社交工具。愤怒触发群体保护机制,悲伤唤起同情支持,喜悦巩固联盟关系。它们如同可见光谱,快速传递生存需求信号。双刃剑效应:情绪的…...
怎么使用vue3实现一个优雅的不定高虚拟列表
前言 很多同学将虚拟列表当做亮点写在简历上面,但是却不知道如何手写,那么这个就不是加分项而是减分项了。实际项目中更多的是不定高虚拟列表,这篇文章来教你不定高如何实现。 什么是不定高虚拟列表 不定高的意思很简单,就是不…...
LemonSqueezy: 1靶场渗透
LemonSqueezy: 1 来自 <LemonSqueezy: 1 ~ VulnHub> 1,将两台虚拟机网络连接都改为NAT模式 2,攻击机上做namp局域网扫描发现靶机 nmap -sn 192.168.23.0/24 那么攻击机IP为192.168.23.182,靶场IP192.168.23.225 3,对靶机进…...
2025 年山东保安员职业资格考试要点梳理
山东作为人口大省,保安市场规模庞大。2025 年考试报考条件常规。报名通过山东省各市公安机关指定的培训机构或政务服务窗口,提交资料与其他地区类似。 理论考试注重对山东地域文化特色相关安保知识的考查,如在孔庙等文化圣地安保中&#x…...
ARM处理器内核全解析:从Cortex到Neoverse的架构与区别
ARM处理器内核全解析:从Cortex到Neoverse的架构与区别 ARM作为全球领先的处理器架构设计公司,其内核产品线覆盖了从高性能计算到低功耗嵌入式应用的广泛领域。本文将全面解析ARM处理器的内核分类、架构特点、性能差异以及应用场景,帮助读者深…...
网络缓冲区
网络缓冲区分为内核缓冲区和用户态网络缓冲区 我们重点要实现用户态网络缓冲区 1.设计用户态网络缓冲区的原因 ①.生产者和消费者的速度不匹配问题, 需要缓存数据。 ②.粘包处理问题, 不能确保一次系统调用读取或写入完整数据包。 2.代码实现(cha…...
数据仓库的核心架构与关键技术(数据仓库系列二)
目录 一、引言 二、数据仓库的核心架构 三、数据仓库的关键技术 1 数据集成与治理 2 查询优化与性能提升 3 数据共享服务 BI:以Tableau为例 SQL2API:以麦聪QuickAPI为例 4 实时数据处理 四、技术的协同作用 五、总结与展望 六、预告 一、引言…...
基于PyQt5与OpenCV的图像处理系统设计与实现
1. 系统概述 本系统是一个集成了多种经典图像处理算法的图形用户界面(GUI)应用程序,采用Python语言开发,基于PyQt5框架构建用户界面,利用OpenCV库实现核心图像处理功能。 系统支持11种图像处理操作,每种操作都提供参数实时调节功能,并具备原始图像与处理后图像的双视图对…...
如何根据设计稿进行移动端适配:全面详解
如何根据设计稿进行移动端适配:全面详解 文章目录 如何根据设计稿进行移动端适配:全面详解1. **理解设计稿**1.1 设计稿的尺寸1.2 设计稿的单位 2. **移动端适配的核心技术**2.1 使用 viewport 元标签2.1.1 代码示例2.1.2 参数说明 2.2 使用相对单位2.2.…...
什么是大型语言模型(LLM)?哪个大模型更好用?
什么是 LLM? ChatGPT 是一种大型语言模型 (LLM),您可能对此并不陌生。它以非凡的能力而闻名,已证明能够出色地完成各种任务,例如通过考试、生成产品内容、解决问题,甚至在最少的输入提示下编写程序。 他们的实力现已…...
集合学习内容总结
集合简介 1、Scala 的集合有三大类:序列 Seq、集Set、映射 Map,所有的集合都扩展自 Iterable 特质。 2、对于几乎所有的集合类,Scala 都同时提供了可变和不可变的版本,分别位于以下两个包 不可变集合:scala.collect…...
使用typedef和不使用的区别
使用 typedef 定义的函数指针类型 typedef sensor_drv_params_t* (*load_sensor_drv_func)(); 不使用 typedef 的函数指针声明 sensor_drv_params_t* (*load_sensor_drv_func)(); 这两者看似相似,但在语义和用途上有显著区别。下面将详细解释这两种声明的区别、各…...
基于线性回归模型的汽车燃油效率预测
基于线性回归模型的汽车燃油效率预测 1.作者介绍2.线性回归介绍2.1 线性回归简介2.2 线性回归应用场景 3.基于线性回归模型的汽车燃油效率预测实验3.1 Auto MPG Data Set数据集3.2代码调试3.3完整代码3.4结果展示 4.问题分析 基于线性回归模型的汽车燃油效率预测 1.作者介绍 郝…...
Playwright之自定义浏览器目录访问出错:BrowserType.launch: Executable doesn‘t exist
Playwright之自定义浏览器目录访问出错:BrowserType.launch: Executable doesn’t exist 问题描述: 在使用playwright进行浏览器自动化的时候,配置了自定义的浏览器目录,当按照自定义的浏览器目录启动浏览器进行操作时,…...
如何拿到iframe中嵌入的游戏数据
在 iframe 中嵌入的游戏数据是否能被获取,取决于以下几个关键因素: 1. 同源策略 浏览器的同源策略是核心限制。如果父页面和 iframe 中的内容同源(即协议、域名和端口号完全相同),那么可以直接通过 JavaScript 访问 …...
优选算法第七讲:分治
优选算法第七讲:分治 1.分治_快排1.1颜色分类1.2排序数组1.3数组中第k个最大元素1.4库存管理II 2.分治_归并2.1排序数组2.2交易逆序对的总数2.3计算右侧小于当前元素的个数2.4翻转对 1.分治_快排 1.1颜色分类 1.2排序数组 1.3数组中第k个最大元素 1.4库存管理II 2.…...
OpenBMC:BmcWeb 处理http请求4 处理路由对象
OpenBMC:BmcWeb 处理http请求2 查找路由对象-CSDN博客 Router::handle通过findRoute获取了FindRouteResponse对象foundRoute void handle(const std::shared_ptr<Request>& req,const std::shared_ptr<bmcweb::AsyncResp>& asyncResp){FindRouteResponse …...
直流电能表计量解决方案适用于光伏储能充电桩基站等场景
多场景解决方案,准确测量 01 市场规模与增长动力 全球直流表市场预测: 2025年市场规模14亿美元,CAGR超15%。 驱动因素:充电桩、光伏/储能、基站、直流配电 市场增长引擎分析: 充电桩随新能源车迅猛增长ÿ…...
x-cmd install | Slumber - 告别繁琐,拥抱高效的终端 HTTP 客户端
目录 核心优势,一览无遗安装应用场景,无限可能示例告别 GUI,拥抱终端 还在为调试 API 接口,发送 HTTP 请求而苦恼吗?还在各种 GUI 工具之间切换,只为了发送一个简单的请求吗?现在,有…...
git修改已经push的commit的message
1.修改信息 2.修改message 3.强推...
STM32 基础2
STM32中断响应过程 1、中断源发出中断请求。 2、判断处理器是否允许中断,以及该中断源是否被屏蔽。 3、中断优先级排队。 4、处理器暂停当前程序,保护断点地址和处理器的当前状态,根据中断类型号,查找中断向量表,转到…...
【STL 之速通pair vector list stack queue set map 】
考list 的比较少 --双端的啦 pair 想下,程序是什么样的. 我是我们要带着自己的思考去学习DevangLic.. #include <iostream> #include <utility> #include <string>using namespace std;int main() {// 第一部分:创建并输出两个 pair …...
深度学习篇---LSTM+Attention模型
文章目录 前言1. LSTM深入原理剖析1.1 LSTM 架构的进化理解遗忘门简介数学表达式实际作用 输入门简介数学表达式后选候选值实际作用 输出门简介数学表达式最终输出实际作用 1.2 Attention 机制的动态特性内容感知位置无关可解释性数学本质 1.3 LSTM与Attention的协同效应组合优…...
React 多个 HOC 嵌套太深,会带来哪些隐患?
在 React 中,使用多个 高阶组件(HOC,Higher-Order Component) 可能会导致组件层级变深,这可能会带来以下几个影响: 一、带来的影响 1、调试困难 由于组件被多个 HOC 包裹,React 开发者工具&am…...
企业工厂生产线马达保护装置 功能参数介绍
安科瑞刘鸿鹏 摘要 工业生产中,电压暂降(晃电)是导致电动机停机、生产中断的主要原因之一,给企业带来巨大的经济损失。本文以安科瑞晃电再起动控制器为例,探讨抗晃电保护器在生产型企业工厂中的应用,分析…...
Redis 的五种数据类型面试回答
这里简单介绍一下面试回答、我之前有详细的去学习、但是一直都觉得太多内容了、太深入了 然后面试的时候不知道从哪里讲起、于是我写了这篇CSDN帮助大家面试回答、具体的深入解析下次再说 面试官你好 我来介绍一下Redis的五种基本数据类型 有String List Set ZSet Map 五种基…...
多线程代码案例(定时器) - 3
定时器,是我们日常开发所常用的组件工具,类似于闹钟,设定一个时间,当时间到了之后,定时器可以自动的去执行某个逻辑 目录 Timer 的基本使用 实现一个 Timer 通过这个类,来描述一个任务 通过这个类&…...
基于大模型的GCSE预测与治疗优化系统技术方案
目录 技术方案文档:基于大模型的GCSE预测与治疗优化系统1. 数据预处理模块功能:整合多模态数据(EEG、MRI、临床指标等),标准化并生成训练集。伪代码流程图2. 大模型架构(Transformer-GNN混合模型)功能:联合建模时序信号(EEG)与空间结构(脑网络)。伪代码流程图3. 术…...
IntelliJ IDEA 中 Continue 插件使用 DeepSeek-R1 模型指南
IntelliJ IDEA 中 Continue 插件使用 DeepSeek-R1 模型指南 Continue 是一款开源的 AI 编码助手插件,支持 IntelliJ IDEA 等 JetBrains 系列 IDE。它可以通过连接多种语言模型(如 DeepSeek-R1)提供实时代码生成、问题解答和单元测试生成等功…...
Valgrind——内存调试和性能分析工具
文章目录 一、Valgrind 介绍二、Valgrind 功能和使用1. 主要功能2. 基本用法2.1 常用选项2.2 内存泄漏检测2.3 详细报告2.4 性能分析2.5 多线程错误检测 三、在 Ubuntu 上安装 Valgrind四、示例1. 检测内存泄漏2. 使用未初始化的内存3. 内存读写越界4. 综合错误 五、工具集1. M…...
京东API智能风控引擎:基于行为分析识别恶意爬虫与异常调用
京东 API 智能风控引擎基于行为分析识别恶意爬虫与异常调用,主要通过以下几种方式实现: 行为特征分析 请求频率:正常用户对 API 的调用频率相对稳定,受到网络延迟、操作速度等因素限制。若发现某个 IP 地址或用户在短时间内对同一…...
Swift 解 LeetCode 250:搞懂同值子树,用递归写出权限系统检查器
文章目录 前言问题描述简单说:痛点分析:到底难在哪?1. 子树的概念搞不清楚2. 要不要“递归”?递归从哪开始?3. 怎么“边遍历边判断”?这套路不熟 后序遍历 全局计数器遍历过程解释一下:和实际场…...
Nginx搭建API网关服务教程-系统架构优化 API统一管理
超实用!用Nginx搭建API网关服务,让你的系统架构更稳更强大!🚀 亲们,今天来给大家种草一个超级实用的API网关搭建方案啦!👀 在如今的Web系统架构中,一个稳定、高性能、可扩展的API网…...
SQL2API是什么?SQL2API与BI为何对数据仓库至关重要?
目录 一、SQL2API是什么? 二、SQL2API的历史演变:从数据共享到服务化革命 1990年代:萌芽于数据仓库的数据共享需求 2010年代初:数据中台推动服务化浪潮 2022年左右:DaaS平台的兴起 2025年代:麦聪定义…...
CentOS 7无法上网问题解决
CentOS 7无法上网问题解决 问题 配置了桥接模式以后,能够ping通本地IP但是无法ping通www.baidu.com 这里的前提是VWare上已经对虚拟机桥接模式网卡做了正确的选择,比如我现在选择的就是当前能够上外网的网卡: 问题根因 DNS未正确配置。…...
优化 Web 性能:使用 WebP 图片(Uses WebP Images)
在 Web 开发中,图片资源的优化是提升页面加载速度和用户体验的关键。Google 的 Lighthouse 工具在性能审计中特别推荐“使用 WebP 图片”(Uses WebP Images),因为 WebP 格式在保持视觉质量的同时显著减少文件大小。本文将基于 Chr…...