Blockers 4 & 5: browser getUserMedia → JPEG IPC → Rust I420 pipeline; remote video strip renders decoded frames via canvas; EncryptingTransport wraps QuinnTransport so WZP AEAD is applied to all media (C2 fix). Test fixes: HandshakeResult.session destructuring across relay/client/crypto integration tests; video_codecs field added to all CallOffer/CallAnswer structs; wzp-video pipeline_roundtrip integration tests added. PRD docs: five Kimi-ready specs for E2E encryption, Android NDK 0.9 migration, quality upgrade flow, wire-format hardening, and clippy debt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
5.0 KiB
Rust
156 lines
5.0 KiB
Rust
//! Temporal interleaving — spreads symbols from multiple FEC blocks across
|
|
//! transmission slots so that burst losses damage multiple blocks lightly
|
|
//! rather than one block fatally.
|
|
|
|
/// A symbol ready for transmission: (block_id, symbol_index, is_repair, data).
|
|
pub type Symbol = (u16, u16, bool, Vec<u8>);
|
|
|
|
/// Temporal interleaver that mixes symbols across multiple FEC blocks.
|
|
pub struct Interleaver {
|
|
/// Number of blocks to interleave across (spread depth).
|
|
depth: usize,
|
|
}
|
|
|
|
impl Interleaver {
|
|
/// Create an interleaver with the given spread depth.
|
|
pub fn new(depth: usize) -> Self {
|
|
Self { depth }
|
|
}
|
|
|
|
/// Create with default depth of 3 blocks.
|
|
pub fn with_default_depth() -> Self {
|
|
Self::new(3)
|
|
}
|
|
|
|
/// Spread depth (number of blocks mixed together).
|
|
pub fn depth(&self) -> usize {
|
|
self.depth
|
|
}
|
|
|
|
/// Interleave symbols from multiple blocks into a single transmission sequence.
|
|
///
|
|
/// Each inner `Vec` contains the symbols for one FEC block.
|
|
/// The output interleaves them in round-robin fashion: symbol 0 from block A,
|
|
/// symbol 0 from block B, symbol 0 from block C, symbol 1 from block A, etc.
|
|
///
|
|
/// This ensures a burst loss of N consecutive packets only destroys at most
|
|
/// ceil(N/depth) symbols from any single block.
|
|
pub fn interleave(&self, blocks: &[Vec<Symbol>]) -> Vec<Symbol> {
|
|
if blocks.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let max_len = blocks.iter().map(|b| b.len()).max().unwrap_or(0);
|
|
let mut output = Vec::with_capacity(blocks.iter().map(|b| b.len()).sum());
|
|
|
|
for slot in 0..max_len {
|
|
for block in blocks {
|
|
if slot < block.len() {
|
|
output.push(block[slot].clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn interleave_mixes_blocks() {
|
|
let interleaver = Interleaver::with_default_depth();
|
|
|
|
let block_a: Vec<Symbol> = (0..3)
|
|
.map(|i| (0u16, i as u16, false, vec![0xA0 + i as u8]))
|
|
.collect();
|
|
let block_b: Vec<Symbol> = (0..3)
|
|
.map(|i| (1u16, i as u16, false, vec![0xB0 + i as u8]))
|
|
.collect();
|
|
let block_c: Vec<Symbol> = (0..3)
|
|
.map(|i| (2u16, i as u16, false, vec![0xC0 + i as u8]))
|
|
.collect();
|
|
|
|
let result = interleaver.interleave(&[block_a, block_b, block_c]);
|
|
|
|
assert_eq!(result.len(), 9);
|
|
|
|
// Round-robin: A0, B0, C0, A1, B1, C1, A2, B2, C2
|
|
assert_eq!(result[0].0, 0); // block A
|
|
assert_eq!(result[1].0, 1); // block B
|
|
assert_eq!(result[2].0, 2); // block C
|
|
assert_eq!(result[3].0, 0); // block A
|
|
assert_eq!(result[4].0, 1); // block B
|
|
assert_eq!(result[5].0, 2); // block C
|
|
|
|
// Verify symbol indices cycle correctly
|
|
assert_eq!(result[0].1, 0); // sym 0 from A
|
|
assert_eq!(result[3].1, 1); // sym 1 from A
|
|
assert_eq!(result[6].1, 2); // sym 2 from A
|
|
}
|
|
|
|
#[test]
|
|
fn interleave_unequal_lengths() {
|
|
let interleaver = Interleaver::new(2);
|
|
|
|
let block_a: Vec<Symbol> = (0..3)
|
|
.map(|i| (0u16, i as u16, false, vec![0xA0 + i as u8]))
|
|
.collect();
|
|
let block_b: Vec<Symbol> = (0..1)
|
|
.map(|i| (1u16, i as u16, false, vec![0xB0 + i as u8]))
|
|
.collect();
|
|
|
|
let result = interleaver.interleave(&[block_a, block_b]);
|
|
|
|
// A0, B0, A1, A2
|
|
assert_eq!(result.len(), 4);
|
|
assert_eq!(result[0].0, 0); // A0
|
|
assert_eq!(result[1].0, 1); // B0
|
|
assert_eq!(result[2].0, 0); // A1
|
|
assert_eq!(result[3].0, 0); // A2
|
|
}
|
|
|
|
#[test]
|
|
fn interleave_empty() {
|
|
let interleaver = Interleaver::with_default_depth();
|
|
let result = interleaver.interleave(&[]);
|
|
assert!(result.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn burst_loss_distributed() {
|
|
// With 3-block interleaving and a burst of 6 consecutive losses,
|
|
// each block loses at most 2 symbols.
|
|
let interleaver = Interleaver::new(3);
|
|
|
|
let blocks: Vec<Vec<Symbol>> = (0..3)
|
|
.map(|b| {
|
|
(0..6)
|
|
.map(|i| (b as u16, i as u16, false, vec![b as u8; 10]))
|
|
.collect()
|
|
})
|
|
.collect();
|
|
|
|
let interleaved = interleaver.interleave(&blocks);
|
|
assert_eq!(interleaved.len(), 18);
|
|
|
|
// Simulate burst loss of 6 consecutive packets starting at index 3
|
|
let lost_range = 3..9;
|
|
let mut losses_per_block = [0u32; 3];
|
|
for idx in lost_range {
|
|
let block_id = interleaved[idx].0 as usize;
|
|
losses_per_block[block_id] += 1;
|
|
}
|
|
|
|
// Each block should lose exactly 2 (6 losses / 3 blocks)
|
|
for &loss in &losses_per_block {
|
|
assert_eq!(
|
|
loss, 2,
|
|
"Each block should lose at most 2 symbols from a burst of 6"
|
|
);
|
|
}
|
|
}
|
|
}
|