Skip to main content

rtc_interceptor/flexfec/draft03/
encoder.rs

1//! FlexFEC draft-03 repair packet construction.
2
3use crate::flexfec::bit_array::BitArray;
4use crate::flexfec::coverage::ProtectionCoverage;
5use shared::marshal::{Marshal, MarshalSize};
6
7/// Bytes of RTP header a repair packet recovers from, and the offset its payload starts at.
8pub(crate) const BASE_RTP_HEADER_SIZE: usize = 12;
9
10/// The fixed part of a draft-03 repair payload: recovery fields, SSRC count, the protected SSRC,
11/// the base sequence number and the first packet mask.
12pub(crate) const BASE_HEADER_SIZE: usize = 20;
13
14/// Bytes added when the second packet mask is present.
15const MASK2_SIZE: usize = 4;
16
17/// Bytes added when the third packet mask is present.
18const MASK3_SIZE: usize = 8;
19
20/// Builds FlexFEC **draft-03** repair packets.
21///
22/// Draft-03 rather than [RFC 8627] because draft-03 is what browsers negotiate as
23/// `video/flexfec-03`. The RFC states its payload formats are not backward compatible with the
24/// earlier drafts, so the two are separate implementations with separate vectors — a draft-03
25/// round trip is evidence about browsers, not about the RFC.
26///
27/// ```text
28///  0                   1                   2                   3
29///  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
30/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
31/// |0|0| P|X|  CC  |M| PT recovery |         length recovery       |
32/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
33/// |                          TS recovery                          |
34/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
35/// |   SSRCCount   |                    reserved                   |
36/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
37/// |                             SSRC_i                            |
38/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
39/// |           SN base_i           |k|          Mask [0-14]        |
40/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
41/// |k|                   Mask [15-45] (optional)                   |
42/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
43/// |k|                                                             |
44/// +-+                   Mask [46-108] (optional)                  |
45/// |                                                               |
46/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
47/// ```
48///
49/// [RFC 8627]: https://www.rfc-editor.org/rfc/rfc8627
50#[derive(Debug)]
51pub struct FlexFec03Encoder {
52    payload_type: u8,
53    ssrc: u32,
54    next_sequence_number: u16,
55    coverage: Option<ProtectionCoverage>,
56}
57
58impl FlexFec03Encoder {
59    /// A repair-stream encoder sending `payload_type` on `ssrc`.
60    ///
61    /// The repair stream has its own SSRC and its own sequence-number space, both negotiated
62    /// separately from the media it protects.
63    pub fn new(payload_type: u8, ssrc: u32) -> Self {
64        Self {
65            payload_type,
66            ssrc,
67            next_sequence_number: 0,
68            coverage: None,
69        }
70    }
71
72    /// Start the repair stream's sequence numbers at `sequence_number`.
73    pub fn with_base_sequence_number(mut self, sequence_number: u16) -> Self {
74        self.next_sequence_number = sequence_number;
75        self
76    }
77
78    /// The sequence number the next repair packet will carry.
79    pub fn next_sequence_number(&self) -> u16 {
80        self.next_sequence_number
81    }
82
83    /// Build up to `num_fec_packets` repair packets protecting `media_packets`.
84    ///
85    /// Returns nothing when the media packets are not a **consecutive** run: a packet mask
86    /// describes positions relative to one base sequence number, so a gap would silently shift
87    /// every position after it and protect the wrong packets. A caller that has lost a packet
88    /// should start a new block rather than encode across the hole.
89    pub fn encode(
90        &mut self,
91        media_packets: &[rtp::Packet],
92        num_fec_packets: u32,
93    ) -> Vec<rtp::Packet> {
94        if media_packets.is_empty() || num_fec_packets == 0 {
95            return Vec::new();
96        }
97
98        let consecutive = media_packets.windows(2).all(|pair| {
99            pair[1].header.sequence_number == pair[0].header.sequence_number.wrapping_add(1)
100        });
101        if !consecutive {
102            return Vec::new();
103        }
104
105        let num_media_packets = media_packets.len() as u32;
106        match &mut self.coverage {
107            Some(coverage) => coverage.update(num_media_packets, num_fec_packets),
108            None => match ProtectionCoverage::new(num_media_packets, num_fec_packets) {
109                Some(coverage) => self.coverage = Some(coverage),
110                None => return Vec::new(),
111            },
112        }
113        let Some(coverage) = &self.coverage else {
114            return Vec::new();
115        };
116        if coverage.num_media_packets() != num_media_packets {
117            // The block is longer than the masks can describe; the caller must split it.
118            return Vec::new();
119        }
120
121        let num_fec_packets = coverage.num_fec_packets();
122        let base_sequence_number = media_packets[0].header.sequence_number;
123        let mut repair_packets = Vec::with_capacity(num_fec_packets as usize);
124        for fec_index in 0..num_fec_packets {
125            if let Some(packet) = self.encode_one(fec_index, base_sequence_number, media_packets) {
126                repair_packets.push(packet);
127            }
128        }
129        repair_packets
130    }
131
132    fn encode_one(
133        &mut self,
134        fec_index: u32,
135        base_sequence_number: u16,
136        media_packets: &[rtp::Packet],
137    ) -> Option<rtp::Packet> {
138        let coverage = self.coverage.as_ref()?;
139        let covered = coverage.covered_by(fec_index);
140        if covered.is_empty() {
141            // A repair packet protecting nothing carries no information.
142            return None;
143        }
144        let mask = *coverage.mask(fec_index)?;
145
146        let mask2 = mask.mask2();
147        let mask3 = mask.mask3_draft03();
148        let header_size = BASE_HEADER_SIZE
149            + if mask2 != 0 || mask3 != 0 {
150                MASK2_SIZE
151            } else {
152                0
153            }
154            + if mask3 != 0 { MASK3_SIZE } else { 0 };
155
156        // The repair payload must be long enough for the largest packet it protects: recovering a
157        // lost packet means XORing this back out, so anything shorter would truncate it.
158        let max_payload = covered
159            .iter()
160            .map(|&index| media_packets[index as usize].marshal_size() - BASE_RTP_HEADER_SIZE)
161            .max()?;
162
163        let mut payload = vec![0u8; header_size + max_payload];
164        let (header, repair) = payload.split_at_mut(header_size);
165
166        let mut protected_ssrc = None;
167        for &index in &covered {
168            let media_packet = &media_packets[index as usize];
169            let size = media_packet.marshal_size();
170            let mut buffer = vec![0u8; size];
171            media_packet.marshal_to(&mut buffer).ok()?;
172
173            protected_ssrc.get_or_insert(media_packet.header.ssrc);
174
175            // Recovery fields are the XOR of the corresponding media header bytes, so a receiver
176            // holding every packet but one can XOR the rest back out and be left with it.
177            header[0] ^= buffer[0];
178            header[1] ^= buffer[1];
179            // The first two bits are the RTP version, which is not recovered — it is always 2.
180            header[0] &= 0b0011_1111;
181
182            let length_recovery = (size - BASE_RTP_HEADER_SIZE) as u16;
183            header[2] ^= (length_recovery >> 8) as u8;
184            header[3] ^= length_recovery as u8;
185
186            // Timestamp recovery. The sequence number at bytes 2..4 of the media header is *not*
187            // recovered this way — its position is taken by length recovery, and a lost packet's
188            // sequence number is implied by its position in the mask.
189            for byte in 4..8 {
190                header[byte] ^= buffer[byte];
191            }
192
193            for (target, &source) in repair.iter_mut().zip(&buffer[BASE_RTP_HEADER_SIZE..]) {
194                *target ^= source;
195            }
196        }
197
198        header[8] = 1; // SSRCCount: draft-03 protects a single stream per repair packet.
199        header[9..12].fill(0); // reserved
200        header[12..16].copy_from_slice(&protected_ssrc?.to_be_bytes());
201        header[16..18].copy_from_slice(&base_sequence_number.to_be_bytes());
202        header[18..20].copy_from_slice(&mask.mask1().to_be_bytes());
203
204        // The k-bit marks the last mask present. It is the top bit of each mask word, which is
205        // why mask1 is 15 bits rather than 16 and mask3 is 63 rather than 64.
206        if mask2 == 0 && mask3 == 0 {
207            header[18] |= 0b1000_0000;
208        } else {
209            header[20..24].copy_from_slice(&mask2.to_be_bytes());
210            if mask3 == 0 {
211                header[20] |= 0b1000_0000;
212            } else {
213                header[24..32].copy_from_slice(&mask3.to_be_bytes());
214                header[24] |= 0b1000_0000;
215            }
216        }
217
218        let sequence_number = self.next_sequence_number;
219        self.next_sequence_number = self.next_sequence_number.wrapping_add(1);
220
221        Some(rtp::Packet {
222            header: rtp::header::Header {
223                version: 2,
224                payload_type: self.payload_type,
225                sequence_number,
226                // Upstream hardcodes a constant here. The repair stream is a stream in its own
227                // right, so it carries the media timestamp it was built from — which is at least
228                // monotonic with the media, rather than frozen for the life of the process.
229                timestamp: media_packets[covered[0] as usize].header.timestamp,
230                ssrc: self.ssrc,
231                csrc: Vec::new(),
232                ..Default::default()
233            },
234            payload: payload.into(),
235        })
236    }
237}
238
239/// The packet mask a repair packet declares, read back off the wire.
240///
241/// Used by the tests here and by the decoder; kept beside the encoder so the two cannot drift.
242pub(crate) fn parse_packet_mask(header: &[u8]) -> Option<(BitArray, usize)> {
243    if header.len() < BASE_HEADER_SIZE {
244        return None;
245    }
246
247    let mut mask = BitArray::new();
248    let mask1 = u16::from_be_bytes([header[18] & 0b0111_1111, header[19]]);
249    for bit in 0..15 {
250        if mask1 & (1 << (14 - bit)) != 0 {
251            mask.set_bit(bit);
252        }
253    }
254    if header[18] & 0b1000_0000 != 0 {
255        return Some((mask, BASE_HEADER_SIZE));
256    }
257
258    if header.len() < BASE_HEADER_SIZE + MASK2_SIZE {
259        return None;
260    }
261    let mask2 = u32::from_be_bytes([header[20] & 0b0111_1111, header[21], header[22], header[23]]);
262    for bit in 0..31 {
263        if mask2 & (1 << (30 - bit)) != 0 {
264            mask.set_bit(15 + bit);
265        }
266    }
267    if header[20] & 0b1000_0000 != 0 {
268        return Some((mask, BASE_HEADER_SIZE + MASK2_SIZE));
269    }
270
271    if header.len() < BASE_HEADER_SIZE + MASK2_SIZE + MASK3_SIZE {
272        return None;
273    }
274    let mut mask3_bytes = [0u8; 8];
275    mask3_bytes.copy_from_slice(&header[24..32]);
276    mask3_bytes[0] &= 0b0111_1111;
277    let mask3 = u64::from_be_bytes(mask3_bytes);
278    for bit in 0..63 {
279        if mask3 & (1 << (62 - bit)) != 0 {
280            mask.set_bit(46 + bit);
281        }
282    }
283    Some((mask, BASE_HEADER_SIZE + MASK2_SIZE + MASK3_SIZE))
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    const MEDIA_SSRC: u32 = 476_325_762;
291    const REPAIR_SSRC: u32 = 867_589_674;
292    const REPAIR_PT: u8 = 49;
293
294    fn media_packet(sequence_number: u16, payload: &[u8]) -> rtp::Packet {
295        rtp::Packet {
296            header: rtp::header::Header {
297                version: 2,
298                marker: true,
299                payload_type: 96,
300                sequence_number,
301                timestamp: 3_653_407_706,
302                ssrc: MEDIA_SSRC,
303                ..Default::default()
304            },
305            payload: payload.to_vec().into(),
306        }
307    }
308
309    fn run(count: u16) -> Vec<rtp::Packet> {
310        (0..count)
311            .map(|i| media_packet(100 + i, &[1, 2, 3, 4, 5, i as u8]))
312            .collect()
313    }
314
315    fn encoder() -> FlexFec03Encoder {
316        FlexFec03Encoder::new(REPAIR_PT, REPAIR_SSRC)
317    }
318
319    /// The repair packet is a stream of its own: its own SSRC, payload type and sequence numbers,
320    /// which is what distinguishes FlexFEC from RED-carried schemes.
321    #[test]
322    fn repair_packets_form_their_own_stream() {
323        let mut encoder = encoder().with_base_sequence_number(1000);
324        let repair = encoder.encode(&run(4), 2);
325
326        assert_eq!(2, repair.len());
327        for (offset, packet) in repair.iter().enumerate() {
328            assert_eq!(REPAIR_SSRC, packet.header.ssrc, "not the media SSRC");
329            assert_eq!(REPAIR_PT, packet.header.payload_type);
330            assert_eq!(1000 + offset as u16, packet.header.sequence_number);
331            assert_eq!(2, packet.header.version);
332        }
333        assert_eq!(1002, encoder.next_sequence_number());
334    }
335
336    /// The block is described relative to one base sequence number, so a gap would shift every
337    /// position after it and protect the wrong packets. Encoding across a hole is refused rather
338    /// than done wrongly.
339    #[test]
340    fn a_block_with_a_gap_is_refused() {
341        let mut packets = run(3);
342        packets[2].header.sequence_number = 105; // 100, 101, 105
343
344        assert!(encoder().encode(&packets, 1).is_empty());
345    }
346
347    #[test]
348    fn an_out_of_order_block_is_refused() {
349        let mut packets = run(2);
350        packets.swap(0, 1);
351
352        assert!(encoder().encode(&packets, 1).is_empty());
353    }
354
355    #[test]
356    fn a_block_that_wraps_the_sequence_space_is_accepted() {
357        let packets = vec![
358            media_packet(65534, &[1]),
359            media_packet(65535, &[2]),
360            media_packet(0, &[3]),
361        ];
362        assert_eq!(
363            1,
364            encoder().encode(&packets, 1).len(),
365            "0 follows 65535: consecutive, not a gap"
366        );
367    }
368
369    #[test]
370    fn nothing_to_protect_produces_nothing() {
371        assert!(encoder().encode(&[], 1).is_empty());
372        assert!(encoder().encode(&run(3), 0).is_empty());
373    }
374
375    /// A repair packet covering no media packet carries no information, so the surplus is dropped
376    /// rather than emitted as an empty packet.
377    #[test]
378    fn surplus_repair_packets_are_not_emitted() {
379        let repair = encoder().encode(&run(2), 4);
380        assert_eq!(
381            2,
382            repair.len(),
383            "two media packets can back two repair packets"
384        );
385    }
386
387    #[test]
388    fn a_block_longer_than_the_masks_can_describe_is_refused() {
389        let packets = run(crate::flexfec::coverage::MAX_MEDIA_PACKETS as u16 + 1);
390        assert!(
391            encoder().encode(&packets, 1).is_empty(),
392            "the caller must split the block"
393        );
394    }
395
396    // ---------------------------------------------------------------------------------------
397    // Header layout
398    // ---------------------------------------------------------------------------------------
399
400    #[test]
401    fn the_header_names_the_stream_and_block_it_protects() {
402        let repair = encoder().encode(&run(4), 1);
403        let payload = &repair[0].payload;
404
405        assert_eq!(1, payload[8], "SSRCCount: draft-03 protects one stream");
406        assert_eq!(&[0, 0, 0], &payload[9..12], "reserved");
407        assert_eq!(
408            MEDIA_SSRC.to_be_bytes(),
409            payload[12..16],
410            "the protected stream"
411        );
412        assert_eq!(
413            100u16.to_be_bytes(),
414            payload[16..18],
415            "the block's base sequence number"
416        );
417    }
418
419    /// The k-bit marks the last mask present, which is why mask1 is 15 bits and not 16.
420    #[test]
421    fn a_short_block_carries_one_mask_with_the_k_bit_set() {
422        let repair = encoder().encode(&run(4), 1);
423        let payload = &repair[0].payload;
424
425        assert_eq!(
426            BASE_HEADER_SIZE + 6,
427            payload.len(),
428            "20-byte header plus the longest protected payload"
429        );
430        assert_ne!(0, payload[18] & 0b1000_0000, "k-bit set: no further masks");
431
432        let (mask, header_size) = parse_packet_mask(payload).expect("parses");
433        assert_eq!(BASE_HEADER_SIZE, header_size);
434        assert_eq!(
435            vec![true, true, true, true],
436            (0..4).map(|bit| mask.bit(bit)).collect::<Vec<_>>(),
437            "all four media packets covered"
438        );
439    }
440
441    #[test]
442    fn a_longer_block_adds_the_second_mask() {
443        let repair = encoder().encode(&run(20), 1);
444        let payload = &repair[0].payload;
445
446        assert_eq!(
447            0,
448            payload[18] & 0b1000_0000,
449            "k-bit clear: another mask follows"
450        );
451        assert_ne!(0, payload[20] & 0b1000_0000, "and that one is the last");
452
453        let (mask, header_size) = parse_packet_mask(payload).expect("parses");
454        assert_eq!(BASE_HEADER_SIZE + MASK2_SIZE, header_size);
455        for bit in 0..20 {
456            assert!(mask.bit(bit), "media packet {bit} covered");
457        }
458        assert!(!mask.bit(20), "and nothing beyond the block");
459    }
460
461    #[test]
462    fn a_block_beyond_46_packets_adds_the_third_mask() {
463        let repair = encoder().encode(&run(60), 1);
464        let payload = &repair[0].payload;
465
466        assert_eq!(0, payload[18] & 0b1000_0000);
467        assert_eq!(0, payload[20] & 0b1000_0000);
468        assert_ne!(0, payload[24] & 0b1000_0000, "the third mask is the last");
469
470        let (mask, header_size) = parse_packet_mask(payload).expect("parses");
471        assert_eq!(BASE_HEADER_SIZE + MASK2_SIZE + MASK3_SIZE, header_size);
472        for bit in 0..60 {
473            assert!(mask.bit(bit), "media packet {bit} covered");
474        }
475    }
476
477    /// The mask on the wire has to name exactly the packets the coverage assigned, or a receiver
478    /// XORs the wrong set back out and "recovers" corruption.
479    #[test]
480    fn the_declared_mask_matches_the_interleaved_coverage() {
481        let repair = encoder().encode(&run(6), 2);
482        assert_eq!(2, repair.len());
483
484        let (first, _) = parse_packet_mask(&repair[0].payload).expect("parses");
485        let (second, _) = parse_packet_mask(&repair[1].payload).expect("parses");
486
487        assert_eq!(
488            vec![0, 2, 4],
489            (0..6).filter(|&bit| first.bit(bit)).collect::<Vec<_>>()
490        );
491        assert_eq!(
492            vec![1, 3, 5],
493            (0..6).filter(|&bit| second.bit(bit)).collect::<Vec<_>>()
494        );
495    }
496
497    // ---------------------------------------------------------------------------------------
498    // Recovery arithmetic
499    // ---------------------------------------------------------------------------------------
500
501    /// The recovery fields must be the XOR of the media bytes they stand for. Computing that XOR
502    /// here independently is what makes this a check on the encoder rather than a restatement of
503    /// it — and it is the property the decoder will rely on, before the decoder exists.
504    #[test]
505    fn recovery_fields_are_the_xor_of_the_protected_packets() {
506        let media = run(4);
507        let repair = encoder().encode(&media, 1);
508        let payload = &repair[0].payload;
509
510        let mut expected = [0u8; 8];
511        let mut expected_length = 0u16;
512        for packet in &media {
513            let mut buffer = vec![0u8; packet.marshal_size()];
514            packet.marshal_to(&mut buffer).expect("marshal");
515            expected[0] ^= buffer[0];
516            expected[1] ^= buffer[1];
517            for byte in 4..8 {
518                expected[byte] ^= buffer[byte];
519            }
520            expected_length ^= (packet.marshal_size() - BASE_RTP_HEADER_SIZE) as u16;
521        }
522        expected[0] &= 0b0011_1111;
523
524        assert_eq!(expected[0], payload[0], "flags and CC recovery");
525        assert_eq!(expected[1], payload[1], "marker and payload type recovery");
526        assert_eq!(
527            expected_length.to_be_bytes(),
528            payload[2..4],
529            "length recovery"
530        );
531        assert_eq!(expected[4..8], payload[4..8], "timestamp recovery");
532    }
533
534    #[test]
535    fn the_repair_payload_is_the_xor_of_the_protected_payloads() {
536        let media = run(4);
537        let repair = encoder().encode(&media, 1);
538        let header_size = parse_packet_mask(&repair[0].payload).expect("parses").1;
539        let repair_payload = &repair[0].payload[header_size..];
540
541        let mut expected = vec![0u8; media.iter().map(|p| p.payload.len()).max().unwrap()];
542        for packet in &media {
543            for (target, &source) in expected.iter_mut().zip(packet.payload.iter()) {
544                *target ^= source;
545            }
546        }
547
548        assert_eq!(expected.as_slice(), repair_payload);
549    }
550
551    /// Recovery XORs the repair payload back out, so it has to be as long as the largest packet
552    /// it protects — a shorter one would truncate whatever it recovers.
553    #[test]
554    fn the_repair_payload_is_as_long_as_the_largest_protected_packet() {
555        let media = vec![
556            media_packet(1, &[1, 2, 3]),
557            media_packet(2, &[1, 2, 3, 4, 5, 6, 7, 8]),
558            media_packet(3, &[9]),
559        ];
560        let repair = encoder().encode(&media, 1);
561        let header_size = parse_packet_mask(&repair[0].payload).expect("parses").1;
562
563        assert_eq!(8, repair[0].payload.len() - header_size);
564    }
565
566    /// The RTP version is fixed at 2 and never recovered, so the top two bits of the first
567    /// recovery byte are cleared rather than carrying XORed version bits.
568    #[test]
569    fn the_version_bits_are_not_recovered() {
570        let repair = encoder().encode(&run(3), 1);
571        assert_eq!(
572            0,
573            repair[0].payload[0] & 0b1100_0000,
574            "the two version bits are zeroed"
575        );
576    }
577}