use rtc_interceptor::{
Attribute, AttributedPacket, BandwidthEstimator, CongestionControlBuilder, Interceptor,
PacerBuilder, Packet, PacketReport, RTCPFeedback, RTPHeaderExtension, Registry, Slot,
StreamInfo, TaggedPacket, TwccSenderBuilder,
};
use sansio::Protocol;
use shared::TransportContext;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
mod rtcp_to_application;
use rtcp_to_application::DeliverRtcp;
const TRANSPORT_CC_URI: &str =
"http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01";
const SSRC: u32 = 0x0A0B_0C0D;
const BITRATE: f64 = 1_200_000.0;
const PAYLOAD_BYTES: usize = 1188;
#[derive(Clone, Default)]
struct Recorder {
seen: Arc<Mutex<Vec<PacketReport>>>,
target: Arc<Mutex<f64>>,
}
impl Recorder {
fn new(target: f64) -> Self {
Self {
seen: Arc::new(Mutex::new(Vec::new())),
target: Arc::new(Mutex::new(target)),
}
}
fn reports(&self) -> Vec<PacketReport> {
self.seen.lock().unwrap().clone()
}
fn set_target(&self, target: f64) {
*self.target.lock().unwrap() = target;
}
}
impl BandwidthEstimator for Recorder {
fn on_reports(&mut self, _now: Instant, reports: &[PacketReport]) {
self.seen.lock().unwrap().extend_from_slice(reports);
}
fn target_bitrate(&self) -> f64 {
*self.target.lock().unwrap()
}
}
fn chain(estimator: Recorder) -> impl Interceptor {
Registry::new()
.with(Slot::from(14_000), DeliverRtcp::new())
.with(
Slot::CongestionControl,
CongestionControlBuilder::new(estimator).build(),
)
.with(Slot::TwccSender, TwccSenderBuilder::new().build())
.with(
Slot::Pacer,
PacerBuilder::new()
.with_target_bitrate(BITRATE)
.with_burst_bits(12_000.0)
.build(),
)
.build()
}
fn stream() -> StreamInfo {
StreamInfo {
ssrc: SSRC,
clock_rate: 90_000,
mime_type: "video/VP8".to_owned(),
payload_type: 96,
rtcp_feedback: vec![RTCPFeedback {
typ: "transport-cc".to_owned(),
parameter: String::new(),
}],
rtp_header_extensions: vec![RTPHeaderExtension {
uri: TRANSPORT_CC_URI.to_owned(),
id: 5,
}],
..Default::default()
}
}
fn rtp(now: Instant, sequence_number: u16) -> TaggedPacket {
TaggedPacket {
now,
transport: TransportContext::default(),
message: AttributedPacket::new(Packet::Rtp(rtp::Packet {
header: rtp::header::Header {
version: 2,
payload_type: 96,
sequence_number,
timestamp: u32::from(sequence_number) * 3_000,
ssrc: SSRC,
..Default::default()
},
payload: vec![0xAB; PAYLOAD_BYTES].into(),
..Default::default()
})),
}
}
fn twcc_feedback(now: Instant, base: u16, count: u16) -> TaggedPacket {
use rtcp::transport_feedbacks::transport_layer_cc::{
PacketStatusChunk, RecvDelta, RunLengthChunk, StatusChunkTypeTcc, SymbolTypeTcc,
TransportLayerCc,
};
let feedback = TransportLayerCc {
sender_ssrc: 0,
media_ssrc: SSRC,
base_sequence_number: base,
packet_status_count: count,
reference_time: 1,
fb_pkt_count: 0,
packet_chunks: vec![PacketStatusChunk::RunLengthChunk(RunLengthChunk {
type_tcc: StatusChunkTypeTcc::RunLengthChunk,
packet_status_symbol: SymbolTypeTcc::PacketReceivedSmallDelta,
run_length: count,
})],
recv_deltas: (0..count)
.map(|_| RecvDelta {
type_tcc_packet: SymbolTypeTcc::PacketReceivedSmallDelta,
delta: 250,
})
.collect(),
};
TaggedPacket {
now,
transport: TransportContext::default(),
message: AttributedPacket::new(Packet::Rtcp(vec![Box::new(feedback)])),
}
}
fn send_and_drain(chain: &mut impl Interceptor, epoch: Instant, count: u16) -> Vec<Instant> {
for sequence_number in 0..count {
chain
.handle_write(rtp(epoch, sequence_number))
.expect("write");
}
let mut released = Vec::new();
for step in 0..200u32 {
let now = epoch + Duration::from_millis(u64::from(step));
chain.handle_timeout(now).expect("timeout");
while let Some(packet) = chain.poll_write() {
if matches!(packet.message.packet, Packet::Rtp(_)) {
released.push(packet.now);
}
}
if released.len() == usize::from(count) {
break;
}
}
released
}
#[test]
fn every_departing_packet_is_recorded_at_its_release_instant() {
let epoch = Instant::now();
let estimator = Recorder::new(BITRATE);
let mut chain = chain(estimator.clone());
chain.bind_local_stream(&stream());
let released = send_and_drain(&mut chain, epoch, 5);
assert_eq!(5, released.len(), "the pacer eventually releases all five");
assert!(
released.last().unwrap() > &epoch,
"the pacer released everything at once, so this test cannot tell enqueue from release: \
{released:?}"
);
chain
.handle_read(twcc_feedback(epoch + Duration::from_millis(200), 0, 5))
.expect("read");
while chain.poll_read().is_some() {}
let reports = estimator.reports();
assert_eq!(5, reports.len(), "one report per packet sent");
let departures: Vec<Instant> = reports.iter().map(|report| report.departure).collect();
assert_eq!(
released, departures,
"the history must record the release instant — the pacer's queueing delay counted as \
network delay is exactly what makes a delay-based estimate collapse"
);
}
#[test]
fn reports_carry_the_transport_wide_sequence_numbers() {
let epoch = Instant::now();
let estimator = Recorder::new(BITRATE);
let mut chain = chain(estimator.clone());
chain.bind_local_stream(&stream());
send_and_drain(&mut chain, epoch, 4);
chain
.handle_read(twcc_feedback(epoch + Duration::from_millis(200), 0, 4))
.expect("read");
while chain.poll_read().is_some() {}
let reports = estimator.reports();
assert!(
reports.iter().all(|report| report.is_twcc),
"a stream that negotiated transport-cc must be tracked by its transport-wide number"
);
assert_eq!(
vec![0, 1, 2, 3],
reports
.iter()
.map(|report| report.twcc_sequence_number)
.collect::<Vec<_>>()
);
assert!(
reports.iter().all(|report| report.arrived),
"the feedback said every one arrived"
);
}
#[test]
fn a_changed_estimate_rides_out_on_the_feedback_packet() {
let epoch = Instant::now();
let estimator = Recorder::new(BITRATE);
let mut chain = chain(estimator.clone());
chain.bind_local_stream(&stream());
send_and_drain(&mut chain, epoch, 3);
chain
.handle_read(twcc_feedback(epoch + Duration::from_millis(200), 0, 3))
.expect("read");
let unchanged = chain.poll_read().expect("the feedback packet carries on");
assert!(
!unchanged.message.has(&Attribute::TargetBitrateChanged {
bits_per_second: 0.0
}),
"an estimate that did not move must not re-announce itself"
);
while chain.poll_read().is_some() {}
estimator.set_target(BITRATE / 2.0);
send_and_drain(&mut chain, epoch + Duration::from_millis(300), 3);
chain
.handle_read(twcc_feedback(epoch + Duration::from_millis(500), 3, 3))
.expect("read");
let mut announced = None;
while let Some(packet) = chain.poll_read() {
if let Some(Attribute::TargetBitrateChanged { bits_per_second }) =
packet.message.get(&Attribute::TargetBitrateChanged {
bits_per_second: 0.0,
})
{
announced = Some(*bits_per_second);
}
}
assert_eq!(
Some(BITRATE / 2.0),
announced,
"a moved estimate must leave on the feedback packet that produced it — that is the only \
leg on which it can reach the pacer"
);
}
#[test]
fn an_idle_interceptor_asks_for_no_wakeup() {
let mut chain = Registry::new()
.with(
Slot::CongestionControl,
CongestionControlBuilder::new(Recorder::new(BITRATE)).build(),
)
.build();
assert_eq!(None, chain.poll_timeout());
chain.handle_timeout(Instant::now()).expect("timeout");
assert_eq!(
None,
chain.poll_timeout(),
"a congestion controller with a timerless estimator must not wake the chain"
);
}
#[test]
fn unacknowledged_packets_are_written_off_after_the_prune_horizon() {
let epoch = Instant::now();
let horizon = Duration::from_millis(500);
let interceptor = CongestionControlBuilder::new(Recorder::new(BITRATE))
.with_prune_horizon(horizon)
.build();
let mut chain = Registry::new()
.with(Slot::CongestionControl, interceptor)
.with(Slot::TwccSender, TwccSenderBuilder::new().build())
.build();
chain.bind_local_stream(&stream());
for sequence_number in 0..4 {
chain
.handle_write(rtp(epoch, sequence_number))
.expect("write");
}
while chain.poll_write().is_some() {}
chain.handle_timeout(epoch + horizon * 4).expect("timeout");
chain
.handle_read(twcc_feedback(epoch + horizon * 4, 0, 4))
.expect("read");
while chain.poll_read().is_some() {}
}
#[test]
fn a_retransmission_is_recorded_as_a_separate_departure() {
use rtc_interceptor::NackResponderBuilder;
let epoch = Instant::now();
let estimator = Recorder::new(BITRATE);
let mut chain = Registry::new()
.with(Slot::from(14_000), DeliverRtcp::new())
.with(
Slot::CongestionControl,
CongestionControlBuilder::new(estimator.clone()).build(),
)
.with(Slot::TwccSender, TwccSenderBuilder::new().build())
.with(Slot::NackResponder, NackResponderBuilder::new().build())
.build();
let mut info = stream();
info.rtcp_feedback.push(RTCPFeedback {
typ: "nack".to_owned(),
parameter: String::new(),
});
chain.bind_local_stream(&info);
chain.handle_write(rtp(epoch, 0)).expect("write");
while chain.poll_write().is_some() {}
let nack = rtcp::transport_feedbacks::transport_layer_nack::TransportLayerNack {
sender_ssrc: 0,
media_ssrc: SSRC,
nacks: vec![rtcp::transport_feedbacks::transport_layer_nack::NackPair {
packet_id: 0,
lost_packets: 0,
}],
};
chain
.handle_read(TaggedPacket {
now: epoch + Duration::from_millis(50),
transport: TransportContext::default(),
message: AttributedPacket::new(Packet::Rtcp(vec![Box::new(nack)])),
})
.expect("read");
while chain.poll_read().is_some() {}
let mut retransmitted = 0;
while let Some(packet) = chain.poll_write() {
if let Packet::Rtp(_) = packet.message.packet {
assert!(
packet.message.has(&Attribute::Retransmission),
"CC-PRE-02: the responder must tag what it retransmits"
);
retransmitted += 1;
}
}
assert_eq!(1, retransmitted, "the responder retransmitted the packet");
chain
.handle_read(twcc_feedback(epoch + Duration::from_millis(100), 0, 2))
.expect("read");
while chain.poll_read().is_some() {}
let reports = estimator.reports();
assert_eq!(
2,
reports.len(),
"the original and the retransmission are two departures, two entries — an estimator that \
saw only one would under-count the bytes on the wire exactly when the path is lossy"
);
assert_eq!(
vec![0, 1],
reports
.iter()
.map(|report| report.twcc_sequence_number)
.collect::<Vec<_>>(),
"each transmission gets its own transport-wide number"
);
}
#[test]
fn the_pacer_follows_the_estimate() {
let epoch = Instant::now();
let estimator = Recorder::new(BITRATE);
let mut chain = chain(estimator.clone());
chain.bind_local_stream(&stream());
let before = send_and_drain(&mut chain, epoch, 4);
let spacing_before = before[3].duration_since(before[0]) / 3;
estimator.set_target(BITRATE / 2.0);
chain
.handle_read(twcc_feedback(epoch + Duration::from_millis(200), 0, 4))
.expect("read");
while chain.poll_read().is_some() {}
let after = send_and_drain(&mut chain, epoch + Duration::from_millis(300), 4);
let spacing_after = after[3].duration_since(after[0]) / 3;
assert!(
spacing_after > spacing_before,
"halving the target must slow the pacer: {spacing_before:?} → {spacing_after:?}"
);
let expected = spacing_before * 2;
assert!(
spacing_after.abs_diff(expected) <= Duration::from_millis(1),
"half the rate should be twice the spacing: expected about {expected:?}, got \
{spacing_after:?}"
);
}