一,前言
在这篇文章我们对 rtp 和 rtcp 中的 Sender Report 与 Receiver Report 进行了简单介绍,本文介绍另外一种用于带宽评估的 rtp/rtcp 包 transport-cc 与 TransportPacketsFeedback 。transport-cc 又叫 TWCC(Transport Wide Congestion Control) 主要功能是用于在发送端用于带宽评估,它分为两部分
相对于 remb,google 更推荐这样做法, 带宽估计实现只依赖于发送方,在媒体服务器(如 SFU)中,这意味着更好地控制算法。
二,RTP Header Extension
rtp 的固定头部比较简单,能携带的信息有限,为了携带更多信息,因此就有了 RTP Header Extension。当 rtp 头部 X 位置 1,表示该 rtp 携带扩展信息,webrtc 定义的 RTP Header Extension 有下面这些。
enum RTPExtensionType : int {
kRtpExtensionNone,
kRtpExtensionTransmissionTimeOffset,
kRtpExtensionAudioLevel,
kRtpExtensionInbandComfortNoise,
kRtpExtensionAbsoluteSendTime,
kRtpExtensionAbsoluteCaptureTime,
kRtpExtensionVideoRotation,
kRtpExtensionTransportSequenceNumber,
kRtpExtensionTransportSequenceNumber02,
kRtpExtensionPlayoutDelay,
kRtpExtensionVideoContentType,
kRtpExtensionVideoLayersAllocation,
kRtpExtensionVideoTiming,
kRtpExtensionRtpStreamId,
kRtpExtensionRepairedRtpStreamId,
kRtpExtensionMid,
kRtpExtensionGenericFrameDescriptor00,
kRtpExtensionGenericFrameDescriptor = kRtpExtensionGenericFrameDescriptor00,
kRtpExtensionGenericFrameDescriptor02,
kRtpExtensionColorSpace,
kRtpExtensionNumberOfExtensions // Must be the last entity in the enum.
};
复制代码
三,transport-cc
transport-cc 是属于 RTP Header Extension, 对应 RTPExtensionType type 为 kRtpExtensionTransportSequenceNumber,webrtc 启用 transport-cc 流程如下:
创建 offer 时,将 webrtc::RtpExtension::kTransportSequenceNumberUri 作为默认选项添加到 audio/video 的能力列表中,见函数 WebRtcVoiceEngine::GetRtpHeaderExtensions() 和 WebRtcVideoEngine::GetRtpHeaderExtensions,此时生成的 sdp audio,video Session 中带有“a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01”字段,sdp 示例如下:
v=0
o=- 48624758483368499 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video
a=msid-semantic: WMS 1075052545
m=audio 9 RTP/AVPF 111 103 104 9 102 0 8 106 105 13 110 112 113 126
a=mid:audio
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
m=video 9 RTP/AVPF 96 97 98 99 100 101 121 127 120 125 119 124 107 108 109 35 36 123 118 122 37
a=mid:video
a=extmap:14 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:13 urn:3gpp:video-orientation
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
复制代码
当 app 层调用 SetLocalDescription/SetRemoteDescription sdp 协商一致,都支持 transport-cc ,webrtc 提取 RTP Extension 并存入到 RTPSender rtp_header_extension_map_中。
当采集的数据上来,会在 RTPSender 创建模板包,RTP Extension 在此时被加入到了模板包中, 然后根据模板进行封包,见函数 RTPSender::AllocatePacket。
std::unique_ptr<RtpPacketToSend> RTPSender::AllocatePacket() const {
static constexpr int kExtraCapacity = 16;
auto packet = std::make_unique<RtpPacketToSend>(
&rtp_header_extension_map_, max_packet_size_ + kExtraCapacity);
packet->SetSsrc(ssrc_);
packet->SetCsrcs(csrcs_);
// Reserve extensions, if registered, RtpSender set in SendToNetwork.
packet->ReserveExtension<AbsoluteSendTime>();
packet->ReserveExtension<TransmissionOffset>();
packet->ReserveExtension<TransportSequenceNumber>();
}
复制代码
当封包完成,数据进入 PacedSender 队列,由 PacedSender 发送数据,数据发送之前首先会进入 PacketRouter ,PacketRouter 会判断 Rtp Extension 是否支持 TransportSequenceNumber,如果支持就会打上 transport_seq_,transport_seq_从 0 开始递增,它与 rtp header Sequence Number 无关,peerconntion 与 PacketRouter 对象是 1 对 1 关系,当 peerconntion 有多个 media source,rtp packet 会在 PacketRouter 统一处理,所以我们可以得知评估带宽是针对 peerconntion 的。
void PacketRouter::SendPacket(std::unique_ptr<RtpPacketToSend> packet,
const PacedPacketInfo& cluster_info) {
if (packet->HasExtension<TransportSequenceNumber>()) {
packet->SetExtension<TransportSequenceNumber>((++transport_seq_) & 0xFFFF);
}
......
}
复制代码
我们通过 wireshark 看下 rtp 的结构
四,TransportPacketsFeedback
1,TransportPacketsFeedback Format
base sequence number: 16 bits,该 fb 包首个 rtp 包的 transport seq,非 rtp 包序列号。
packet status count:16 bits, 该 fb packet 包含 rtp 包个数。
reference time:24 bits,参考时间,fb 包首个 rtp 的到达时间/64。
feedback packet count:8bits,已发送 feedback 包计数器,可用于 fb packet 丢失检测。
packet chunk:16 bits ,描述 rtp 包 4 种状态(见:4.2),有 Run Length Chunk 和 Status Vector Chunk 两种格式。
recv delta:8bits,当 rtp 包的状态为 Packet received,通过 recv delta 记录其与前一个 rtp 包到达的时间间隔。
2,Rtp 到达状态
占 2 bits。
00 Packet not received (包未收到)
01 Packet received, small delta (包收到,小 delta )
10 Packet received, large or negative delta( 包收到,delta 很大或者为负数)
11 [Reserved](packet received, w/o recv delta 包收到,不带 delta ?)
3,packet chunk
packet chunk 对 rtp 的到达状态进行描述,它有两种类型
Run Length Chunk
Status Vector Chunk
通过首 bit 位标识了是哪种类型 0 :Run Length Chunk ,1 :Run Length Chunk 。
4,RLE(Run Length Chunk )科普
行程编码,编码原理是把数据看成一个线性序列,而这些数据序列组织方式分成两种情况:一种是连续的重复数据块,另一种是连续的不重复数据块。对于连续的重复数据快采用的压缩策略是用一个字节(我们称之为数据重数属性)表示数据块重复的次数,然后在这个数据重数属性字节后面存储对应的数据字节本身举个例子:
原始数据:A-A-A-A-A-B-B-C-D
压缩前:A-A-A-A-A-B-B-C-D(0x41-0x41-0x41-0x41-0x41-0x42-0x42-0x43-0x44)
压缩后: 5-A-2-B-1-C-1-D(0x05-0x41-0x02-0x42-0x01-0x43-0x01-0x44)
5,Run Length Chunk
T : chunk type,当为 Run Length Chunk ,此时值为 0。
S :packet status symbo,2 bits ,包的到达状态见:4.2
Run Length:多少个连续包状态相同。
S = 00,表示 “Packet not received ”
length = 221(0 0 0 0 0 1 1 0 1 1 1 0 1)
表示连续 221 包个未收到。
S = 11,表示 packet received, w/o recv delta
length = 24(00011000)
表示联系收到 24 个状态为“packet received, w/o recv delta” 包
6,Status Vector Chunk
T: chunk type, 1 bit 当为 Status Vector Chunk ,此时值为 1。
S: symbol size,1 bit 0: packet received,1:and "packet not received
Symbol list: 14bits ,描述了 x 个包的到达状态,x 的数量取决于 S 的值,当 S = 0, x = 14,S= 1 x=7。
S = 0,Symbol list 描述了 14 个包的状态,从第 3 位开始
1x "packet not received"
5x "packet received"
3x "packet not received"
3x "packet received"
2x "packet not received"
S = 1,此时 Symbol list 描述了 7 个包的状态,包状态占用 2 bits
1x "packet not received"
1x "packet received, w/o timestamp"
3x "packet received"
2x "packet not received"
7,Receive Delta
当包的状态为 Packet received,此时 fb packet 会通过 Receive Delta 记录其与前一个 RTP 包到达时间的间隔,单位是 250us.
8,Webrtc FB packet 构造
结合代码分析
int64_t RemoteEstimatorProxy::BuildFeedbackPacket(
uint8_t feedback_packet_count,
uint32_t media_ssrc,
int64_t base_sequence_number,
std::map<int64_t, int64_t>::const_iterator begin_iterator,
std::map<int64_t, int64_t>::const_iterator end_iterator,
rtcp::TransportFeedback* feedback_packet) {
//1. 设置ssrc
feedback_packet->SetMediaSsrc(media_ssrc);
//2. 设置base_sequence_number,reference time。
feedback_packet->SetBase(static_cast<uint16_t>(base_sequence_number & 0xFFFF),
begin_iterator->second * 1000);
//3. 设置packet status count
feedback_packet->SetFeedbackSequenceNumber(feedback_packet_count);
int64_t next_sequence_number = base_sequence_number;
for (auto it = begin_iterator; it != end_iterator; ++it) {
//4. 设置每个状态为“Packet received” rtp 包 的Receive Delta。
if (!feedback_packet->AddReceivedPacket(
static_cast<uint16_t>(it->first & 0xFFFF), it->second * 1000)) {
break;
}
//5. 下一个fb 包的base_sequence_number
next_sequence_number = it->first + 1;
}
return next_sequence_number;
}
复制代码
bool TransportFeedback::AddReceivedPacket(uint16_t sequence_number,
int64_t timestamp_us) {
// Set delta to zero if timestamps are not included, this will simplify the
// encoding process.
int16_t delta = 0;
// 1,include_timestamps_ 默认为true。
if (include_timestamps_) {
//2,计算与前一个RTP包到达时间的间隔。
int64_t delta_full =
(timestamp_us - last_timestamp_us_) % kTimeWrapPeriodUs;
if (delta_full > kTimeWrapPeriodUs / 2)
delta_full -= kTimeWrapPeriodUs;
delta_full +=
delta_full < 0 ? -(kDeltaScaleFactor / 2) : kDeltaScaleFactor / 2;
delta_full /= kDeltaScaleFactor;
delta = static_cast<int16_t>(delta_full);
// 3,delta 过大,需要用新的fb 包,这里直接返回。
if (delta != delta_full) {
RTC_LOG(LS_WARNING) << "Delta value too large ( >= 2^16 ticks )";
return false;
}
}
uint16_t next_seq_no = base_seq_no_ + num_seq_no_;
// 4,next_seq_no不等于sequence_number, 说明【next_seq_no , sequence_number】包未到达,
// 可能是丢包或者乱序导致的,此时不需要设置Receive Delta
if (sequence_number != next_seq_no) {
uint16_t last_seq_no = next_seq_no - 1;
if (!IsNewerSequenceNumber(sequence_number, last_seq_no))
return false;
for (; next_seq_no != sequence_number; ++next_seq_no) {
if (!AddDeltaSize(0))
return false;
if (include_lost_)
all_packets_.emplace_back(next_seq_no);
}
}
//5 根据delta 的值判断所需的空间,1 字节 or 2 字节。
DeltaSize delta_size = (delta >= 0 && delta <= 0xff) ? 1 : 2;
if (!AddDeltaSize(delta_size))
return false;
......
}
复制代码
五,webclient 评估带宽不准
最近对公司 rtc server unifield sdp 兼容开发时,测试人员报了一个 webclient 带宽评估不准的问题。先说下原因,我们公司的 rtc server sdp 在 server 端都是固定的,默认情况下 audio 是不参与带宽评估的,当 webclient 端启用 audio 的 transport-cc, 这样就导致了两端不对称,丢掉了很多 TransportPacketsFeedback ,因此导致了即时带宽评估器评估出的带宽不准(即时带宽评估器请参考这篇文章)。知道了问题原因,解决起来就比较的简单了,在 webclient 生成的 offer sdp 的去掉把 audio session 中“a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01”就可以了。
六,Reference
RTP Extensions for Transport-wide Congestion Control draft-holmer-rmcat-transport-wide-cc-extensions-01
webrtcglossary
评论