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>
311 lines
11 KiB
Rust
311 lines
11 KiB
Rust
//! RaptorQ FEC encoder — accumulates source symbols into blocks and generates repair symbols.
|
|
|
|
use raptorq::{EncodingPacket, ObjectTransmissionInformation, PayloadId, SourceBlockEncoder};
|
|
use wzp_proto::FecEncoder;
|
|
use wzp_proto::error::FecError;
|
|
|
|
/// Maximum symbol size in bytes. Audio frames are typically < 200 bytes,
|
|
/// but we pad to a uniform size within a block.
|
|
/// Each symbol carries a 2-byte length prefix so recovered frames can be trimmed.
|
|
const DEFAULT_MAX_SYMBOL_SIZE: u16 = 256;
|
|
|
|
/// Length prefix size (u16 little-endian).
|
|
const LEN_PREFIX: usize = 2;
|
|
|
|
/// RaptorQ-based FEC encoder that groups audio frames into blocks
|
|
/// and generates fountain-code repair symbols.
|
|
pub struct RaptorQFecEncoder {
|
|
/// Current block ID (wraps at u16).
|
|
block_id: u16,
|
|
/// Maximum source symbols per block.
|
|
frames_per_block: usize,
|
|
/// Accumulated source symbols for the current block.
|
|
source_symbols: Vec<Vec<u8>>,
|
|
/// Symbol size used for encoding (all symbols padded to this size).
|
|
symbol_size: u16,
|
|
/// True if at least one source symbol in the current block is a keyframe.
|
|
has_keyframe: bool,
|
|
/// Repair ratio to use when the block contains a keyframe.
|
|
/// If zero, the nominal ratio passed to [`generate_repair`] is used.
|
|
keyframe_ratio: f32,
|
|
}
|
|
|
|
impl RaptorQFecEncoder {
|
|
/// Create a new encoder.
|
|
///
|
|
/// * `frames_per_block` — number of source symbols per FEC block.
|
|
/// * `symbol_size` — max byte length of any single source symbol (frames are zero-padded).
|
|
pub fn new(frames_per_block: usize, symbol_size: u16) -> Self {
|
|
Self {
|
|
block_id: 0,
|
|
frames_per_block,
|
|
source_symbols: Vec::with_capacity(frames_per_block),
|
|
symbol_size,
|
|
has_keyframe: false,
|
|
keyframe_ratio: 0.0,
|
|
}
|
|
}
|
|
|
|
/// Set the repair ratio to use for blocks that contain at least one
|
|
/// keyframe source symbol.
|
|
///
|
|
/// When `keyframe_ratio > 0.0` and [`has_keyframe`](Self::has_keyframe)
|
|
/// is true, [`generate_repair`](FecEncoder::generate_repair) uses this
|
|
/// ratio instead of the nominal ratio passed by the caller.
|
|
pub fn set_keyframe_ratio(&mut self, ratio: f32) {
|
|
self.keyframe_ratio = ratio.max(0.0);
|
|
}
|
|
|
|
/// Returns true if the current block contains a keyframe source symbol.
|
|
pub fn has_keyframe(&self) -> bool {
|
|
self.has_keyframe
|
|
}
|
|
|
|
/// Create with default symbol size (256 bytes).
|
|
pub fn with_defaults(frames_per_block: usize) -> Self {
|
|
Self::new(frames_per_block, DEFAULT_MAX_SYMBOL_SIZE)
|
|
}
|
|
|
|
/// Build a contiguous data buffer from the accumulated source symbols,
|
|
/// each prefixed with a 2-byte length and zero-padded to `symbol_size`.
|
|
fn build_block_data(&self) -> Vec<u8> {
|
|
let ss = self.symbol_size as usize;
|
|
let mut data = vec![0u8; self.source_symbols.len() * ss];
|
|
for (i, sym) in self.source_symbols.iter().enumerate() {
|
|
let max_payload = ss - LEN_PREFIX;
|
|
let payload_len = sym.len().min(max_payload);
|
|
let offset = i * ss;
|
|
// Write 2-byte little-endian length prefix.
|
|
data[offset..offset + LEN_PREFIX].copy_from_slice(&(payload_len as u16).to_le_bytes());
|
|
// Write payload after prefix.
|
|
data[offset + LEN_PREFIX..offset + LEN_PREFIX + payload_len]
|
|
.copy_from_slice(&sym[..payload_len]);
|
|
}
|
|
data
|
|
}
|
|
}
|
|
|
|
impl FecEncoder for RaptorQFecEncoder {
|
|
fn add_source_symbol(&mut self, data: &[u8]) -> Result<(), FecError> {
|
|
if self.source_symbols.len() >= self.frames_per_block {
|
|
return Err(FecError::BlockFull {
|
|
max: self.frames_per_block,
|
|
});
|
|
}
|
|
self.source_symbols.push(data.to_vec());
|
|
Ok(())
|
|
}
|
|
|
|
fn add_source_symbol_with_keyframe(
|
|
&mut self,
|
|
data: &[u8],
|
|
is_keyframe: bool,
|
|
) -> Result<(), FecError> {
|
|
self.add_source_symbol(data)?;
|
|
if is_keyframe {
|
|
self.has_keyframe = true;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_repair(&mut self, ratio: f32) -> Result<Vec<(u16, Vec<u8>)>, FecError> {
|
|
if self.source_symbols.is_empty() {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
let effective_ratio = if self.has_keyframe && self.keyframe_ratio > 0.0 {
|
|
self.keyframe_ratio
|
|
} else {
|
|
ratio
|
|
};
|
|
|
|
let block_data = self.build_block_data();
|
|
let config =
|
|
ObjectTransmissionInformation::with_defaults(block_data.len() as u64, self.symbol_size);
|
|
let encoder = SourceBlockEncoder::new((self.block_id & 0xFF) as u8, &config, &block_data);
|
|
|
|
let num_source = self.source_symbols.len() as u32;
|
|
let num_repair = ((num_source as f32) * effective_ratio).ceil() as u32;
|
|
if num_repair == 0 {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
// Generate repair packets starting from offset 0 (ESIs begin at num_source).
|
|
let repair_packets: Vec<EncodingPacket> = encoder.repair_packets(0, num_repair);
|
|
|
|
let result: Vec<(u16, Vec<u8>)> = repair_packets
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, pkt): (usize, EncodingPacket)| {
|
|
let idx = (num_source as u16).wrapping_add(i as u16);
|
|
(idx, pkt.data().to_vec())
|
|
})
|
|
.collect();
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
fn finalize_block(&mut self) -> Result<u16, FecError> {
|
|
let completed = self.block_id;
|
|
self.block_id = self.block_id.wrapping_add(1);
|
|
self.source_symbols.clear();
|
|
self.has_keyframe = false;
|
|
Ok(completed)
|
|
}
|
|
|
|
fn current_block_id(&self) -> u16 {
|
|
self.block_id
|
|
}
|
|
|
|
fn current_block_size(&self) -> usize {
|
|
self.source_symbols.len()
|
|
}
|
|
}
|
|
|
|
/// Build a length-prefixed, padded block data buffer from raw symbols.
|
|
/// This matches what the encoder produces internally.
|
|
fn build_prefixed_block_data(symbols: &[Vec<u8>], symbol_size: u16) -> Vec<u8> {
|
|
let ss = symbol_size as usize;
|
|
let mut data = vec![0u8; symbols.len() * ss];
|
|
for (i, sym) in symbols.iter().enumerate() {
|
|
let max_payload = ss - LEN_PREFIX;
|
|
let payload_len = sym.len().min(max_payload);
|
|
let offset = i * ss;
|
|
data[offset..offset + LEN_PREFIX].copy_from_slice(&(payload_len as u16).to_le_bytes());
|
|
data[offset + LEN_PREFIX..offset + LEN_PREFIX + payload_len]
|
|
.copy_from_slice(&sym[..payload_len]);
|
|
}
|
|
data
|
|
}
|
|
|
|
/// Helper: build source `EncodingPacket`s for a given block. Useful for
|
|
/// the decoder tests and interleaving.
|
|
pub fn source_packets_for_block(
|
|
block_id: u16,
|
|
symbols: &[Vec<u8>],
|
|
symbol_size: u16,
|
|
) -> Vec<EncodingPacket> {
|
|
let ss = symbol_size as usize;
|
|
let data = build_prefixed_block_data(symbols, symbol_size);
|
|
(0..symbols.len())
|
|
.map(|i| {
|
|
let offset = i * ss;
|
|
let sym_data = data[offset..offset + ss].to_vec();
|
|
EncodingPacket::new(PayloadId::new((block_id & 0xFF) as u8, i as u32), sym_data)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Helper: generate repair packets for the given source symbols.
|
|
pub fn repair_packets_for_block(
|
|
block_id: u16,
|
|
symbols: &[Vec<u8>],
|
|
symbol_size: u16,
|
|
ratio: f32,
|
|
) -> Vec<EncodingPacket> {
|
|
let data = build_prefixed_block_data(symbols, symbol_size);
|
|
let config = ObjectTransmissionInformation::with_defaults(data.len() as u64, symbol_size);
|
|
let encoder = SourceBlockEncoder::new((block_id & 0xFF) as u8, &config, &data);
|
|
let num_source = symbols.len() as u32;
|
|
let num_repair = ((num_source as f32) * ratio).ceil() as u32;
|
|
encoder.repair_packets(0, num_repair)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn add_symbols_and_finalize() {
|
|
let mut enc = RaptorQFecEncoder::with_defaults(5);
|
|
assert_eq!(enc.current_block_id(), 0);
|
|
assert_eq!(enc.current_block_size(), 0);
|
|
|
|
for i in 0..5 {
|
|
enc.add_source_symbol(&[i as u8; 100]).unwrap();
|
|
}
|
|
assert_eq!(enc.current_block_size(), 5);
|
|
|
|
// Block full
|
|
assert!(enc.add_source_symbol(&[0u8; 100]).is_err());
|
|
|
|
let repair = enc.generate_repair(0.5).unwrap();
|
|
assert!(!repair.is_empty());
|
|
// 5 source * 0.5 = 3 repair (ceil)
|
|
assert_eq!(repair.len(), 3);
|
|
|
|
let id = enc.finalize_block().unwrap();
|
|
assert_eq!(id, 0);
|
|
assert_eq!(enc.current_block_id(), 1);
|
|
assert_eq!(enc.current_block_size(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn block_id_wraps_u16() {
|
|
let mut enc = RaptorQFecEncoder::with_defaults(1);
|
|
// Advance 300 blocks and verify no panic + monotonic increment.
|
|
for expected in 0..300u16 {
|
|
assert_eq!(enc.current_block_id(), expected);
|
|
enc.add_source_symbol(&[0u8; 10]).unwrap();
|
|
enc.finalize_block().unwrap();
|
|
}
|
|
// Explicitly test wrap at u16 boundary.
|
|
let mut enc2 = RaptorQFecEncoder::with_defaults(1);
|
|
enc2.block_id = u16::MAX;
|
|
enc2.add_source_symbol(&[0u8; 10]).unwrap();
|
|
let id = enc2.finalize_block().unwrap();
|
|
assert_eq!(id, u16::MAX);
|
|
assert_eq!(enc2.current_block_id(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn keyframe_boost_uses_higher_ratio() {
|
|
// Non-keyframe block with nominal ratio 0.2 → ceil(5 * 0.2) = 1 repair.
|
|
let mut enc_normal = RaptorQFecEncoder::with_defaults(5);
|
|
enc_normal.set_keyframe_ratio(0.8);
|
|
for i in 0..5 {
|
|
enc_normal
|
|
.add_source_symbol_with_keyframe(&[i as u8; 100], false)
|
|
.unwrap();
|
|
}
|
|
let normal_repair = enc_normal.generate_repair(0.2).unwrap();
|
|
assert_eq!(normal_repair.len(), 1);
|
|
|
|
// Keyframe block with same nominal ratio but boost to 0.8 → ceil(5 * 0.8) = 4 repairs.
|
|
let mut enc_key = RaptorQFecEncoder::with_defaults(5);
|
|
enc_key.set_keyframe_ratio(0.8);
|
|
for i in 0..5 {
|
|
enc_key
|
|
.add_source_symbol_with_keyframe(&[i as u8; 100], i == 2)
|
|
.unwrap();
|
|
}
|
|
let keyframe_repair = enc_key.generate_repair(0.2).unwrap();
|
|
assert_eq!(keyframe_repair.len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn non_keyframe_block_uses_nominal_ratio() {
|
|
let mut enc = RaptorQFecEncoder::with_defaults(5);
|
|
enc.set_keyframe_ratio(0.8);
|
|
|
|
for i in 0..5 {
|
|
enc.add_source_symbol_with_keyframe(&[i as u8; 100], false)
|
|
.unwrap();
|
|
}
|
|
|
|
let repair = enc.generate_repair(0.2).unwrap();
|
|
assert_eq!(repair.len(), 1); // ceil(5 * 0.2) = 1
|
|
}
|
|
|
|
#[test]
|
|
fn finalize_clears_keyframe_flag() {
|
|
let mut enc = RaptorQFecEncoder::with_defaults(2);
|
|
enc.add_source_symbol_with_keyframe(&[0u8; 10], true)
|
|
.unwrap();
|
|
assert!(enc.has_keyframe());
|
|
|
|
enc.finalize_block().unwrap();
|
|
assert!(!enc.has_keyframe());
|
|
}
|
|
}
|