1、RTCP的封装
RTP需要RTCP为其服务器质量提供保证,周期性发送
RTCP的主要功能是:服务质量的监视、反馈(QoS)、媒体间的同步(Sync),以及多播组中成员的标识。在RTP会话期间,各参与者周期性地传送RTCP包。RTCP包中含有已发送的数据包的梳理、丢失的数据包数量等统计资料。
因此,各参与者可以利用这些信息动态地修改传输速率,甚至改变有效载荷类型。RTP和RTCP配合使用,它们能以有效的反馈和最小的开销使传输效率最佳化,因而特别适合传送网上的实时数据。
RTCP由如下五种分组类型:
200 | SR(Sender Report) | 发送端报告 |
201 | RR(Receiver Report) | 接收端报告 |
202 | SDES(Source Description Items) | 源点描述 |
203 | BYE | 结束传输 |
204 | APP | 特定应用 |
上述五种分组的封装大同小异,下面只讲诉SR类型,其他类型请参考RFC5330。
1.1、RTCP头部
版本(V) | 2B | RTCP版本号 |
填充标志(P) | 1B | 填充标志,如果P=1,则该RTCP包的尾部就包含附加的填充字节 |
接收报告计数器(RC) | 5B | 接收报告计数器,表示SR包中接收报告块的数目,可以为零 |
包类型(PT) | 8B | SR包是200 |
长度域(length) | 16B | RTCP包的长度,包括填充的内容 |
/**
* RTCP固定头
*/
typedef struct _rtcp_hdr_t {
uint32_t v:2; /* version */
uint32_t p:1; /* padding */
uint32_t rc:5; /* reception report count */
uint32_t pt:8; /* packet type */
uint32_t length:16; /* pkt len in words, w/o this word */
} rtcp_hdr_t;
2.2、RTCP报告
/**
* RTCP报告类型
*/
enum {
RTCP_FIR = 192, // 关键帧请求, RFC2032
RTCP_NACK = 193, // 丢包重传, RFC2032
RTCP_SMPTETC= 194, // RFC5484
RTCP_IJ = 195, // RFC5450
RTCP_SR = 200, // 发送者报告
RTCP_RR = 201, // 接受者报告
RTCP_SDES = 202, // 源点描述
RTCP_BYE = 203, // 结束传输
RTCP_APP = 204, // 特定应用
RTCP_RTPFB = 205, // RTP Feedback, RFC4585
RTCP_PSFB = 206, // PS Feedback, RFC4585
RTCP_XR = 207, // RFC3611
RTCP_AVB = 208,
RTCP_RSI = 209, // RFC5760
RTCP_TOKEN = 210, // RFC6284
RTCP_IDMS = 211, // RFC7272
RTCP_RGRS = 212, // RFC8861
RTCP_LIMIT = 223,
};
2.2.1、发送端报告(SR)
发送端报告分组SR(Sender Report)用来使发送端以多播方式向所有接收端报告发送情况。
发送者报告SR的PT值:
以下是发送者报告详细内容:
同步源(SSRC of sender) | 32B | SR包发送者的同步源标识符,与对应RTP包中的SSRC一样 |
NTP时间戳(NTP timestamp) | 64B | 表示发送此报告时以挂钟时间测量的时间点。 结合来自各个接收器的接收报告中返回的时间戳,它可用于估计往返于接收器的往返传播时间 |
RTP时间戳(RTP timestamp) | 32B | 与NTP时间戳对应,与RTP数据包中的RTP时间戳具有相同的单位和随机初始值 |
发送包总数(packet count) | 32B | 从开始发送包到产生这个SR包这段时间里,发送者发送的RTP数据包的总数,SSRC改变时,这个域清零 |
发送总字节数(octet count) | 32B | 从开始发送包到产生这个SR包这段时间里,发送者发送的净荷数据的总字节数(不包括头部和填充)发送者改变其SSRC时,这个域要清零 |
同步源N的SSRC标识符(SSRC_n) | 32B | 该报告块中包含的是从该源接收到的包的统计信息 |
丢包率(Fraction Lost) | 8B | 从开始接收到SSRC_n的包到发送SR,从SSRC_n传过来的RTP数据包的丢失总数 |
累计的包丢失数目(cumulative number of packets lost) | 24B | 从开始接收到SSRC_n的包到发送SR,从SSRC_n传过来的RTP数据包的丢失总数 |
收到的扩展最大序列号(extended highest sequence number received) | 32B | 从SSRC_n收到的RTP数据包中最大的序列号 |
接收抖动(interarrival jitter) | 32B | RTP数据包接受时间的统计方差估计 |
上次SR时间戳(LSR) | 32B | 取最近从SSRC_n收到的SR包中的NTP时间戳的中间32比特。如果目前还没收到SR包,则该域清零 |
上次SR以来的延时(DLSR) | 32B | 上次从SSRC_n收到SR包到发送本报告的延时 |
抓包截取的SR报文:
2.2.2、接收端报告(RR)
除包类型代码外,SR与RR间唯一的差别是源报告包含有一个20字节发送者信息段。活动源在发出最后一个数据包之后或前一个数据包与下一个数据包间隔期间发送SR;否则,就发送RR。
SR和RR包都可没有接收报告块也可以包括多个接收报告块,其发布报告表示的源不一定是在CSRC列表上的起作用的源,每个接收报告块提供从特殊源接收数据的统计。最大可有31个接收报告块嵌入在SR 或 RR包中。
接收端报告没有“SenderInfo”,剩余部分都是“Report Block”:
抓包截取的RR报文:
2.2.3、源点描述(SDES)
SDES源描述包提供了直观的文本信息来描述会话的参加者,包括CNAME、NAME、EMAIL、PHONE、LOC等源描述项,这些为接收方获取发送方的有关信息提供了方便。SDES 包由包头与数据块组成,数据块可以没有,也可有多个。
/**
* SDES报告类型
*/
enum
{
RTCP_SDES_END = 0,
RTCP_SDES_CNAME = 1, // 源唯一标识
RTCP_SDES_NAME = 2, // 描述源的名称
RTCP_SDES_EMAIL = 3, // email
RTCP_SDES_PHONE = 4, // 电话号码
RTCP_SDES_LOC = 5, // 位置
RTCP_SDES_TOOL = 6,
RTCP_SDES_NOTE = 7, // 备注信息
RTCP_SDES_PRIVATE = 8, // 私有扩展
};
例如:
抓包截取的SDES报文:
2.2.4、结束传输(BYE)
参与者发送 BYE 数据包以指示一个或多个源不再活动,可选择给出离开的理由。
作为可选项,BYE包可包括一个8位八进制计数,后跟文本信息,表示离开原因,如:"cameramalfunction"或"RTPloop detected"。字符串的编码与在SDES 项中所描述的相同。如字符串信息至BYE包下32位边界结束处,字符串就不以空结尾;否则,BYE包以空八进制填充。
抓包截取到的BYE报文:
2.2.5、特定应用(APP)
这里仅是RTCP报文的环路延时和丢包率的计算,实际的QOS用的不是这个机制。
2、SR, RR
2.1、环路时延(rtt)
// ArriveTime是发送者接收到RR的时间
// LSR为发送者发出SR的时间,DLSR为接收端处理SR报的耗时
RTT = ArriveTime - LSR - DLSR
// 上面等式等同于
RTT = (ST2 - ST1) - (RT2 - RT1)
// 真实的计算公式
RTT = (A - LSR - DLSR) >> 16 // 单位是秒
要知道,发送端和接收端的时钟是不同的,所以在计算NTP时,必须在同一个时钟上进行计算,而计算差值时可以跨时钟计算。
其中,这几个值的详细说明:
- NTP时间戳
NTP把当前时间(自1970.1.1以来的秒数)分为整数部分N和小数部分X
NTP高位 = 整数部分N + 2208988800UL(其中2208988800UL表示自1900.1.1到1970.1.1的秒数)
NTP低位 = 小数部分X * (2^32)(其中4294967296为2^32)
- LSR
取自上一个接收到的SR包中的NTP值,取ntp_msw的低16位和ntp_lsw的高16位。
ntp_msw&0xffff + ntp_lsw>>16
- DLSR
从接收到上一个SR包到发送此接收报告块之间的延时,以1/65536秒为单位。
在WebRTC里面函数实现如下:
// 处理接受者报告
void RTCPReceiver::HandleReportBlock(const ReportBlock& report_block,
PacketInformation* packet_information,
uint32_t remote_ssrc) {
// This will be called once per report block in the RTCP packet.
// We filter out all report blocks that are not for us.
// Each packet has max 31 RR blocks.
//
// We can calc RTT if we send a send report and get a report block back.
// `report_block.source_ssrc()` is the SSRC identifier of the source to
// which the information in this reception report block pertains.
// Filter out all report blocks that are not for us.
if (!registered_ssrcs_.contains(report_block.source_ssrc()))
return;
last_received_rb_ = clock_->CurrentTime();
ReportBlockData* report_block_data =
&received_report_blocks_[report_block.source_ssrc()];
RTCPReportBlock rtcp_report_block;
rtcp_report_block.sender_ssrc = remote_ssrc;
rtcp_report_block.source_ssrc = report_block.source_ssrc();
rtcp_report_block.fraction_lost = report_block.fraction_lost();
rtcp_report_block.packets_lost = report_block.cumulative_lost_signed();
if (report_block.extended_high_seq_num() >
report_block_data->report_block().extended_highest_sequence_number) {
// We have successfully delivered new RTP packets to the remote side after
// the last RR was sent from the remote side.
last_increased_sequence_number_ = last_received_rb_;
}
rtcp_report_block.extended_highest_sequence_number =
report_block.extended_high_seq_num();
rtcp_report_block.jitter = report_block.jitter();
rtcp_report_block.delay_since_last_sender_report =
report_block.delay_since_last_sr();
rtcp_report_block.last_sender_report_timestamp = report_block.last_sr();
// Number of seconds since 1900 January 1 00:00 GMT (see
// https://tools.ietf.org/html/rfc868).
report_block_data->SetReportBlock(
rtcp_report_block,
(clock_->CurrentNtpInMilliseconds() - rtc::kNtpJan1970Millisecs) *
rtc::kNumMicrosecsPerMillisec);
uint32_t send_time_ntp = report_block.last_sr();
// RFC3550, section 6.4.1, LSR field discription states:
// If no SR has been received yet, the field is set to zero.
// Receiver rtp_rtcp module is not expected to calculate rtt using
// Sender Reports even if it accidentally can.
if (send_time_ntp != 0) {
uint32_t delay_ntp = report_block.delay_since_last_sr();
// Local NTP time.
uint32_t receive_time_ntp =
CompactNtp(clock_->ConvertTimestampToNtpTime(last_received_rb_));
// RTT in 1/(2^16) seconds.
uint32_t rtt_ntp = receive_time_ntp - delay_ntp - send_time_ntp;
// Convert to 1/1000 seconds (milliseconds).
TimeDelta rtt = CompactNtpRttToTimeDelta(rtt_ntp);
report_block_data->AddRoundTripTimeSample(rtt.ms());
if (report_block.source_ssrc() == local_media_ssrc()) {
rtts_[remote_ssrc].AddRtt(rtt);
}
packet_information->rtt_ms = rtt.ms();
}
packet_information->report_blocks.push_back(
report_block_data->report_block());
packet_information->report_block_datas.push_back(*report_block_data);
}
2.2、丢包率(fraction lost)
以PeerA端为例,PeerA想要知道:
- PeerA发送给PeerB的RTP报文的丢包率和环路延时;
- PeerB发送给PeerA的RTP报文的丢包率;
接收端维护两个计数器,每收到一个RTP包都更新:
- transmitted,接收到的RTP包的总数;
- retransmitted,接收到重传RTP包的数量;
某时刻收到的有序包的数量count = transmitted-retransmitte,当前时刻为count2,上一时刻为count1;
接收端以一定的频率发送RTCP包(RR、REMB、NACK等)时,会统计两次发送间隔之间(fraction)的接收包信息:
// 两次发送间隔之间理论上应该收到的包数量 = 当前接收到的最大包序号 - 上个时刻最大有序包序号
uint16_t exp_since_last = (received_seq_max_ - last_report_seq_max_);
// 两次发送间隔之间实际接收到有序包的数量 = 当前时刻收到的有序包的数量 - 上一个时刻收到的有序包的数量
uint32_t rec_since_last = count2 - count1
// 丢包数 = 理论上应收的包数 - 实际收到的包数
int32_t missing = exp_since_last - rec_since_last
missing即为两次发送间隔之间的丢包数量,会累加并通过RR包通知发送端。
// 更新统计数
void StreamStatisticianImpl::UpdateCounters(const RtpPacketReceived& packet) {
RTC_DCHECK_EQ(ssrc_, packet.Ssrc());
// 当前时钟
int64_t now_ms = clock_->TimeInMilliseconds();
incoming_bitrate_.Update(packet.size(), now_ms);
receive_counters_.last_packet_received_timestamp_ms = now_ms;
receive_counters_.transmitted.AddPacket(packet);
--cumulative_loss_;
// 包序号
int64_t sequence_number =
seq_unwrapper_.UnwrapWithoutUpdate(packet.SequenceNumber());
// RR包
if (!ReceivedRtpPacket()) {
received_seq_first_ = sequence_number;
last_report_seq_max_ = sequence_number - 1;
received_seq_max_ = sequence_number - 1;
receive_counters_.first_packet_time_ms = now_ms;
} else if (UpdateOutOfOrder(packet, sequence_number, now_ms)) {
return;
}
cumulative_loss_ += sequence_number - received_seq_max_;
received_seq_max_ = sequence_number;
seq_unwrapper_.UpdateLast(sequence_number);
// 如果接收到新的时间戳和多个有序数据包,则计算新的抖动统计。
if (packet.Timestamp() != last_received_timestamp_ &&
(receive_counters_.transmitted.packets -
receive_counters_.retransmitted.packets) > 1) {
UpdateJitter(packet, now_ms);
}
last_received_timestamp_ = packet.Timestamp();
last_receive_time_ms_ = now_ms;
}
接收端发送的RR包中包含两个丢包,一个是fraction_lost,是两次统计间隔间的丢包率(以256为基数换算成8bit),一个是cumulative_lost,是总的累积丢包。
// RTCP包统计
RtcpStatistics StreamStatisticianImpl::CalculateRtcpStatistics() {
RtcpStatistics stats;
if (last_report_inorder_packets_ == 0) {
// First time we send a report.
last_report_seq_max_ = received_seq_first_ - 1;
}
// last_report_seq_max_ 上一次发送rr时的seq_max
// received_seq_max_ 当前的seq_max
// exp_since_last 期望能收到多少包
uint16_t exp_since_last = (received_seq_max_ - last_report_seq_max_);
// ......
// 当前收到的有序包的数量 = transmitted.packets - retransmitted.packets
// 上一次发送RR时收到包的数量: last_report_old_packets_
// 实际收到包的数量: rec_since_last
uint32_t rec_since_last =
(receive_counters_.transmitted.packets -
receive_counters_.retransmitted.packets) - last_report_inorder_packets_;
// With NACK we don't know the expected retransmissions during the last
// second. We know how many "old" packets we have received. We just count
// the number of old received to estimate the loss, but it still does not
// guarantee an exact number since we run this based on time triggered by
// sending of an RTP packet. This should have a minimum effect.
// With NACK we don't count old packets as received since they are
// re-transmitted. We use RTT to decide if a packet is re-ordered or
// re-transmitted.
// 计算从上一次rr到当前这段时间内,收到的重传包总数
uint32_t retransmitted_packets =
receive_counters_.retransmitted.packets - last_report_old_packets_;
// 实际丢包数加上重传丢包数
rec_since_last += retransmitted_packets;
// 计算丢包数:期望收到的包总数exp_since_last - 实际收到的包总数rec_since_last
int32_t missing = 0;
if (exp_since_last > rec_since_last) {
missing = (exp_since_last - rec_since_last);
}
// 丢包率 = 255 * 丢包数 / 预期收到的包总数
uint8_t local_fraction_lost = 0;
if (exp_since_last) {
// Scale 0 to 255, where 255 is 100% loss.
local_fraction_lost =
static_cast<uint8_t>(255 * missing / exp_since_last);
}
stats.fraction_lost = local_fraction_lost;
// We need a counter for cumulative loss too.
// TODO(danilchap): Ensure cumulative loss is below maximum value of 2^24.
// 累加丢包总数
cumulative_loss_ += missing;
stats.cumulative_lost = cumulative_loss_;
// ......
}
2.3、抖动计算(jitter)
Jitter是一个统计变量,它用来表征RTP数据包与包的传输时间之间的差异程度。具体的推算公式如下:
J(i) = J(i-1) + (传输预估差 - J(i-1)) / 16
J(i) = J(i-1) + (| D(i-1,i) | - J(i-1))/16
i时刻的jitter是由i-1时刻的jitter推出的。其中D(i-1,i)表达式如下:
D(i,j) = 接收时间差 - 发送时间差
D(i,j) = (Rj - Ri) - (Sj - Si) = (Rj - Sj) - (Ri - Si)
Si表示i包里的RTP timestamp,Ri表示i包到达目的端时的timestamp。
jitter的单位是RTP中timestamp的单位。RTP timestamp单位和RTP Data的采样频率有关,比如采用PCMU编码的audio包采样频率是8000Hz,那timestamp单位就是1/8000s;采用OPUS编码的audio包采样频率是48000Hz,那timestamp单位就是1/48000s。
whireshark抓包测试:
1)PCMU:
如图相隔1s的两个包,timestamp相减1441520544 - 1441512544结果等于8000,即PCMU采样频率;
2)OPUS:
相隔1s的两个包,timestamp相减879484868 - 879436868结果等于48000,即OPUS采样频率。
RTP接收端根据上面的公式推算出jitter值后,会通过RTCP发送给对端。以Receiver Report为例,jitter在RTCP中的存放位置如下:
Jitter计算实例
让我们以一个jitter的计算实例来加深对jitter的理解。我们假设发送端每隔20ms发送一个RTP包,并且理想的传输时间是10ms。为了便于理解,我们使用ms为jitter的计算单位(timestamp单位和时间单位可以换算)。RTCP规定timestamp的起始值是一个随机值,同样为了便于理解,我们起始值设为0。具体计算如下表:
根据公式:D(i,j) = (Rj - Ri) - (Sj - Si) = (Rj - Sj) - (Ri - Si)
- D(i,j) = (30 - 10) - (20 - 0) = 0
- D(i,j) = (49 - 30) - (40 - 20) = -1
- D(i,j) = (74 - 49) - (60 - 40) = 5
- D(i,j) = (90 - 74) - (80 - 60) = -4
从表中可以看出,当包与包的传输时间之间的差异变大时,jitter值也开始增长。同时,jitter并没有随着差异的急速变大而剧烈变化,这是由于有去噪增益1/16。当差异逐渐缩小趋于稳定时,jitter的推算值也趋近于一个预估的均值。
Jitter Buffer
因网络状况总是千变万化的,那么包在网络上传输的时间也就不断变化。对于RTC audio的应用场景,为了得到良好的音频效果,我们需要以一个固定的时间间隔来播放音频包。因此,在接收端,无论网络延时怎么变化,我们都需要将变化的延时转化成固定的延时。而这,可以通过jitter buffer来实现。
jitter buffer的实现很简单:我们可以创建一个buffer来存储100ms时长的audio包。假设audio的采样频率是8000Hz,那么100ms就对应800个sample。我们可以设定如果buffer的sample数达到总的一半了,就可以去取出来播放了。
当然,这样做并不能确保播放质量。因为有可能网络实在是太差了,我们想要去取下一个audio包,但是buffer已经空了;也可能buffer已经满了,但是又接收到了audio包(只能丢弃)。为了减少这种风险,我们可以增加buffer的大小,但这样同时也增加了音频的滞后:因为如果buffer里满了50ms的audio包才取出来播放,那audio音频其实滞后了50ms。能不能再改进呢?
能的,我们可以使用根据jitter值自适应调整大小的buffer。这样既能适应网络状况变换,又能兼顾音频滞后的负面影响。
Jitter的起因
除了因为网络状况导致的传输时间变化,jitter还可能是由发送端自身导致的。当发送端的audio包不是从系统声卡采集的,而是从其他应用软件或某个音频文件采集的,那么也可能造成较大的jitter。