1use super::acknowledgement::{Acknowledgement, PacketReport};
4use rtcp::transport_feedbacks::cc_feedback_report::Ecn;
5use std::collections::HashMap;
6use std::time::{Duration, Instant};
7
8#[derive(Debug, Default)]
14pub struct History {
15 next_id: u64,
17 packets: HashMap<u64, PacketReport>,
18 twcc_index: HashMap<u16, u64>,
19 ssrc_sequence_index: HashMap<(u32, u16), u64>,
20 highest_acknowledged: Option<u64>,
22 next_to_report: u64,
24}
25
26impl History {
27 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn len(&self) -> usize {
34 self.packets.len()
35 }
36
37 pub fn is_empty(&self) -> bool {
39 self.packets.is_empty()
40 }
41
42 #[allow(clippy::too_many_arguments)]
49 pub fn add_outgoing(
50 &mut self,
51 ssrc: u32,
52 rtp_sequence_number: u16,
53 is_twcc: bool,
54 twcc_sequence_number: u16,
55 size: usize,
56 departure: Instant,
57 ) -> u64 {
58 let id = self.next_id;
59 self.next_id += 1;
60
61 if is_twcc {
62 self.twcc_index.insert(twcc_sequence_number, id);
63 }
64 self.ssrc_sequence_index
65 .insert((ssrc, rtp_sequence_number), id);
66
67 self.packets.insert(
68 id,
69 PacketReport {
70 ssrc,
71 id,
72 rtp_sequence_number,
73 is_twcc,
74 twcc_sequence_number,
75 size,
76 arrived: false,
77 departure,
78 arrival: None,
79 ecn: Ecn::NotEct,
80 },
81 );
82
83 id
84 }
85
86 pub fn on_twcc_feedback(
91 &mut self,
92 received_at: Instant,
93 acknowledgement: Acknowledgement,
94 ) -> Option<Duration> {
95 let id = *self.twcc_index.get(&acknowledgement.sequence_number)?;
96 self.apply(received_at, id, acknowledgement)
97 }
98
99 pub fn on_ccfb_feedback(
101 &mut self,
102 received_at: Instant,
103 ssrc: u32,
104 acknowledgement: Acknowledgement,
105 ) -> Option<Duration> {
106 let id = *self
107 .ssrc_sequence_index
108 .get(&(ssrc, acknowledgement.sequence_number))?;
109 self.apply(received_at, id, acknowledgement)
110 }
111
112 fn apply(
113 &mut self,
114 received_at: Instant,
115 id: u64,
116 acknowledgement: Acknowledgement,
117 ) -> Option<Duration> {
118 let packet = self.packets.get_mut(&id)?;
119
120 packet.arrived = acknowledgement.arrived;
121 packet.arrival = acknowledgement.arrival;
122 packet.ecn = acknowledgement.ecn;
123
124 if packet.arrived {
125 self.highest_acknowledged = Some(match self.highest_acknowledged {
126 Some(highest) => highest.max(id),
127 None => id,
128 });
129 }
130
131 Some(received_at.saturating_duration_since(packet.departure))
134 }
135
136 pub fn take_reports(&mut self) -> Vec<PacketReport> {
142 let Some(highest) = self.highest_acknowledged else {
143 return Vec::new();
144 };
145 if self.next_to_report > highest {
146 return Vec::new();
147 }
148
149 let mut reports = Vec::new();
150 for id in self.next_to_report..=highest {
151 if let Some(packet) = self.packets.remove(&id) {
152 if packet.is_twcc {
153 self.twcc_index.remove(&packet.twcc_sequence_number);
154 }
155 self.ssrc_sequence_index
156 .remove(&(packet.ssrc, packet.rtp_sequence_number));
157 reports.push(packet);
158 }
159 }
160 self.next_to_report = highest + 1;
161
162 reports
163 }
164
165 pub fn prune_before(&mut self, cutoff: Instant) {
170 let stale: Vec<u64> = self
171 .packets
172 .iter()
173 .filter(|(_, packet)| packet.departure < cutoff && !packet.arrived)
174 .map(|(&id, _)| id)
175 .collect();
176
177 for id in stale {
178 if let Some(packet) = self.packets.remove(&id) {
179 if packet.is_twcc {
183 self.twcc_index.remove(&packet.twcc_sequence_number);
184 }
185 self.ssrc_sequence_index
186 .remove(&(packet.ssrc, packet.rtp_sequence_number));
187 }
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 fn history_with_three_packets(now: Instant) -> History {
197 let mut history = History::new();
198 for sequence_number in 0..3u16 {
199 history.add_outgoing(1, 100 + sequence_number, true, sequence_number, 1200, now);
200 }
201 history
202 }
203
204 #[test]
205 fn feedback_for_an_unknown_packet_is_ignored() {
206 let now = Instant::now();
207 let mut history = History::new();
208
209 assert_eq!(
210 None,
211 history.on_twcc_feedback(now, Acknowledgement::received(5, None, Ecn::NotEct)),
212 "nothing was sent with that sequence number"
213 );
214 assert!(history.take_reports().is_empty());
215 }
216
217 #[test]
218 fn twcc_feedback_matches_by_transport_wide_sequence_number() {
219 let now = Instant::now();
220 let mut history = history_with_three_packets(now);
221
222 let rtt = history.on_twcc_feedback(
223 now + Duration::from_millis(80),
224 Acknowledgement::received(1, Some(Duration::from_millis(5)), Ecn::NotEct),
225 );
226 assert_eq!(Some(Duration::from_millis(80)), rtt);
227
228 let reports = history.take_reports();
229 assert_eq!(2, reports.len(), "everything up to the acknowledgement");
230 assert_eq!(101, reports[1].rtp_sequence_number);
231 assert!(reports[1].arrived);
232 assert!(!reports[0].arrived, "never acknowledged, so a loss");
233 }
234
235 #[test]
238 fn ccfb_feedback_matches_by_ssrc_and_sequence_number() {
239 let now = Instant::now();
240 let mut history = History::new();
241 history.add_outgoing(1, 500, false, 0, 1000, now);
242 history.add_outgoing(2, 500, false, 0, 1000, now);
243
244 history.on_ccfb_feedback(
245 now + Duration::from_millis(50),
246 2,
247 Acknowledgement::received(500, Some(Duration::from_millis(1)), Ecn::Ce),
248 );
249
250 let reports = history.take_reports();
251 assert_eq!(2, reports.len());
252 assert!(
253 !reports[0].arrived,
254 "stream 1's packet was not acknowledged"
255 );
256 assert!(reports[1].arrived, "stream 2's was");
257 assert_eq!(Ecn::Ce, reports[1].ecn);
258 }
259
260 #[test]
268 fn ccfb_feedback_for_a_sequence_number_another_stream_sent_does_not_match() {
269 let now = Instant::now();
270 let mut history = History::new();
271 history.add_outgoing(1, 500, false, 0, 1000, now);
272
273 assert_eq!(
274 None,
275 history.on_ccfb_feedback(
276 now + Duration::from_millis(50),
277 2,
278 Acknowledgement::received(500, Some(Duration::from_millis(1)), Ecn::NotEct),
279 ),
280 "stream 2 never sent sequence 500"
281 );
282 assert!(
283 history.take_reports().is_empty(),
284 "and nothing was acknowledged, so stream 1's packet is still outstanding"
285 );
286 }
287
288 #[test]
289 fn nothing_is_reported_until_something_is_acknowledged() {
290 let now = Instant::now();
291 let mut history = history_with_three_packets(now);
292
293 assert!(
294 history.take_reports().is_empty(),
295 "sent but not yet acknowledged"
296 );
297 assert_eq!(3, history.len(), "and still tracked");
298 }
299
300 #[test]
303 fn packets_are_reported_once() {
304 let now = Instant::now();
305 let mut history = history_with_three_packets(now);
306
307 history.on_twcc_feedback(now, Acknowledgement::received(2, None, Ecn::NotEct));
308 assert_eq!(3, history.take_reports().len());
309 assert!(history.take_reports().is_empty());
310 assert!(history.is_empty(), "and released");
311 }
312
313 #[test]
314 fn later_feedback_reports_only_what_follows() {
315 let now = Instant::now();
316 let mut history = History::new();
317 history.add_outgoing(1, 100, true, 0, 1200, now);
318 history.add_outgoing(1, 101, true, 1, 1200, now);
319
320 history.on_twcc_feedback(now, Acknowledgement::received(0, None, Ecn::NotEct));
321 assert_eq!(1, history.take_reports().len());
322
323 history.on_twcc_feedback(now, Acknowledgement::received(1, None, Ecn::NotEct));
324 let reports = history.take_reports();
325 assert_eq!(1, reports.len());
326 assert_eq!(101, reports[0].rtp_sequence_number);
327 }
328
329 #[test]
332 fn a_loss_alone_does_not_advance_the_window() {
333 let now = Instant::now();
334 let mut history = history_with_three_packets(now);
335
336 history.on_twcc_feedback(now, Acknowledgement::lost(1));
337 assert!(
338 history.take_reports().is_empty(),
339 "nothing has been acknowledged as arrived"
340 );
341 assert_eq!(3, history.len());
342 }
343
344 #[test]
347 fn the_round_trip_is_measured_locally() {
348 let now = Instant::now();
349 let mut history = History::new();
350 history.add_outgoing(1, 100, true, 0, 1200, now);
351
352 assert_eq!(
353 Some(Duration::from_millis(120)),
354 history.on_twcc_feedback(
355 now + Duration::from_millis(120),
356 Acknowledgement::received(0, Some(Duration::from_secs(9999)), Ecn::NotEct),
357 ),
358 "the receiver's arrival clock does not affect it"
359 );
360 }
361
362 #[test]
369 fn releasing_a_non_twcc_packet_does_not_evict_a_twcc_packets_index_entry() {
370 let now = Instant::now();
371 let mut history = History::new();
372
373 history.add_outgoing(2, 200, false, 0, 1200, now);
377 history.add_outgoing(1, 100, true, 0, 1200, now);
379
380 history.on_ccfb_feedback(now, 2, Acknowledgement::received(200, None, Ecn::NotEct));
382 history.take_reports();
383
384 assert!(
385 history
386 .on_twcc_feedback(now, Acknowledgement::received(0, None, Ecn::NotEct))
387 .is_some(),
388 "the TWCC packet is still matchable by its transport-wide sequence number"
389 );
390 }
391
392 #[test]
394 fn pruning_a_non_twcc_packet_does_not_evict_a_twcc_packets_index_entry() {
395 let now = Instant::now();
396 let mut history = History::new();
397
398 history.add_outgoing(2, 200, false, 0, 1200, now);
400 history.add_outgoing(1, 100, true, 0, 1200, now + Duration::from_secs(5));
402
403 history.prune_before(now + Duration::from_secs(1));
404
405 assert!(
406 history
407 .on_twcc_feedback(
408 now + Duration::from_secs(5),
409 Acknowledgement::received(0, None, Ecn::NotEct)
410 )
411 .is_some(),
412 "pruning the unrelated packet must not take the TWCC index entry with it"
413 );
414 }
415
416 #[test]
419 fn an_acknowledged_packet_is_not_pruned_before_it_is_reported() {
420 let now = Instant::now();
421 let mut history = History::new();
422 history.add_outgoing(1, 100, true, 0, 1200, now);
423
424 history.on_twcc_feedback(
425 now + Duration::from_millis(50),
426 Acknowledgement::received(0, None, Ecn::NotEct),
427 );
428
429 history.prune_before(now + Duration::from_secs(30));
431
432 let reports = history.take_reports();
433 assert_eq!(1, reports.len(), "the arrival survived to be reported");
434 assert!(reports[0].arrived);
435 }
436
437 #[test]
438 fn old_unacknowledged_packets_are_pruned() {
439 let now = Instant::now();
440 let mut history = History::new();
441 history.add_outgoing(1, 100, true, 0, 1200, now);
442 history.add_outgoing(1, 101, true, 1, 1200, now + Duration::from_secs(5));
443
444 history.prune_before(now + Duration::from_secs(1));
445
446 assert_eq!(1, history.len(), "only the recent one survives");
447 assert_eq!(
448 None,
449 history.on_twcc_feedback(now, Acknowledgement::received(0, None, Ecn::NotEct)),
450 "and the pruned one is no longer matchable"
451 );
452 }
453}