feat(video+desktop): camera capture, video UI, E2E AEAD wiring, test fixes
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>
This commit is contained in:
@@ -29,9 +29,9 @@ pub enum DecoderBlockState {
|
||||
/// Manages encoder-side block tracking.
|
||||
pub struct EncoderBlockManager {
|
||||
/// Current block ID being built.
|
||||
current_id: u8,
|
||||
current_id: u16,
|
||||
/// State of known blocks.
|
||||
blocks: HashMap<u8, EncoderBlockState>,
|
||||
blocks: HashMap<u16, EncoderBlockState>,
|
||||
}
|
||||
|
||||
impl EncoderBlockManager {
|
||||
@@ -45,7 +45,7 @@ impl EncoderBlockManager {
|
||||
}
|
||||
|
||||
/// Get the next block ID (advances the current building block).
|
||||
pub fn next_block_id(&mut self) -> u8 {
|
||||
pub fn next_block_id(&mut self) -> u16 {
|
||||
let old = self.current_id;
|
||||
// Mark old block as pending.
|
||||
self.blocks.insert(old, EncoderBlockState::Pending);
|
||||
@@ -57,23 +57,23 @@ impl EncoderBlockManager {
|
||||
}
|
||||
|
||||
/// Current block ID being built.
|
||||
pub fn current_id(&self) -> u8 {
|
||||
pub fn current_id(&self) -> u16 {
|
||||
self.current_id
|
||||
}
|
||||
|
||||
/// Mark a block as fully sent.
|
||||
pub fn mark_sent(&mut self, block_id: u8) {
|
||||
pub fn mark_sent(&mut self, block_id: u16) {
|
||||
self.blocks.insert(block_id, EncoderBlockState::Sent);
|
||||
}
|
||||
|
||||
/// Mark a block as acknowledged by the peer.
|
||||
pub fn mark_acknowledged(&mut self, block_id: u8) {
|
||||
pub fn mark_acknowledged(&mut self, block_id: u16) {
|
||||
self.blocks
|
||||
.insert(block_id, EncoderBlockState::Acknowledged);
|
||||
}
|
||||
|
||||
/// Get the state of a block.
|
||||
pub fn state(&self, block_id: u8) -> Option<EncoderBlockState> {
|
||||
pub fn state(&self, block_id: u16) -> Option<EncoderBlockState> {
|
||||
self.blocks.get(&block_id).copied()
|
||||
}
|
||||
|
||||
@@ -93,9 +93,9 @@ impl Default for EncoderBlockManager {
|
||||
/// Manages decoder-side block tracking.
|
||||
pub struct DecoderBlockManager {
|
||||
/// State of known blocks.
|
||||
blocks: HashMap<u8, DecoderBlockState>,
|
||||
blocks: HashMap<u16, DecoderBlockState>,
|
||||
/// Set of completed block IDs.
|
||||
completed: HashSet<u8>,
|
||||
completed: HashSet<u16>,
|
||||
}
|
||||
|
||||
impl DecoderBlockManager {
|
||||
@@ -107,43 +107,43 @@ impl DecoderBlockManager {
|
||||
}
|
||||
|
||||
/// Register that we are receiving symbols for a block.
|
||||
pub fn touch(&mut self, block_id: u8) {
|
||||
pub fn touch(&mut self, block_id: u16) {
|
||||
self.blocks
|
||||
.entry(block_id)
|
||||
.or_insert(DecoderBlockState::Assembling);
|
||||
}
|
||||
|
||||
/// Mark a block as successfully decoded.
|
||||
pub fn mark_complete(&mut self, block_id: u8) {
|
||||
pub fn mark_complete(&mut self, block_id: u16) {
|
||||
self.blocks.insert(block_id, DecoderBlockState::Complete);
|
||||
self.completed.insert(block_id);
|
||||
}
|
||||
|
||||
/// Mark a block as expired.
|
||||
pub fn mark_expired(&mut self, block_id: u8) {
|
||||
pub fn mark_expired(&mut self, block_id: u16) {
|
||||
self.blocks.insert(block_id, DecoderBlockState::Expired);
|
||||
self.completed.remove(&block_id);
|
||||
}
|
||||
|
||||
/// Check if a block has been fully decoded.
|
||||
pub fn is_block_complete(&self, block_id: u8) -> bool {
|
||||
pub fn is_block_complete(&self, block_id: u16) -> bool {
|
||||
self.completed.contains(&block_id)
|
||||
}
|
||||
|
||||
/// Get the state of a block.
|
||||
pub fn state(&self, block_id: u8) -> Option<DecoderBlockState> {
|
||||
pub fn state(&self, block_id: u16) -> Option<DecoderBlockState> {
|
||||
self.blocks.get(&block_id).copied()
|
||||
}
|
||||
|
||||
/// Expire all blocks older than the given block_id (using wrapping distance).
|
||||
pub fn expire_before(&mut self, block_id: u8) {
|
||||
let to_expire: Vec<u8> = self
|
||||
pub fn expire_before(&mut self, block_id: u16) {
|
||||
let to_expire: Vec<u16> = self
|
||||
.blocks
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|&id| {
|
||||
let distance = block_id.wrapping_sub(id);
|
||||
distance > 0 && distance <= 128
|
||||
distance > 0 && distance <= 32768
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -207,7 +207,7 @@ mod tests {
|
||||
#[test]
|
||||
fn decoder_expire_before() {
|
||||
let mut mgr = DecoderBlockManager::new();
|
||||
for i in 0..5u8 {
|
||||
for i in 0..5u16 {
|
||||
mgr.touch(i);
|
||||
}
|
||||
mgr.mark_complete(1);
|
||||
@@ -231,11 +231,11 @@ mod tests {
|
||||
#[test]
|
||||
fn next_block_id_wraps() {
|
||||
let mut mgr = EncoderBlockManager::new();
|
||||
// Start at 0, advance to 255 then wrap
|
||||
for _ in 0..255 {
|
||||
// Start at 0, advance to u16::MAX then wrap
|
||||
for _ in 0..65535 {
|
||||
mgr.next_block_id();
|
||||
}
|
||||
assert_eq!(mgr.current_id(), 255);
|
||||
assert_eq!(mgr.current_id(), u16::MAX);
|
||||
let next = mgr.next_block_id();
|
||||
assert_eq!(next, 0);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ struct BlockState {
|
||||
/// RaptorQ-based FEC decoder that handles multiple concurrent blocks.
|
||||
pub struct RaptorQFecDecoder {
|
||||
/// Per-block decoder state, keyed by block_id.
|
||||
blocks: HashMap<u8, BlockState>,
|
||||
blocks: HashMap<u16, BlockState>,
|
||||
/// Symbol size (must match encoder).
|
||||
symbol_size: u16,
|
||||
/// Number of source symbols per block (from encoder config).
|
||||
@@ -57,7 +57,7 @@ impl RaptorQFecDecoder {
|
||||
Self::new(frames_per_block, 256)
|
||||
}
|
||||
|
||||
fn get_or_create_block(&mut self, block_id: u8) -> &mut BlockState {
|
||||
fn get_or_create_block(&mut self, block_id: u16) -> &mut BlockState {
|
||||
self.blocks.entry(block_id).or_insert_with(|| BlockState {
|
||||
num_source_symbols: Some(self.frames_per_block),
|
||||
packets: Vec::new(),
|
||||
@@ -72,7 +72,7 @@ impl RaptorQFecDecoder {
|
||||
impl FecDecoder for RaptorQFecDecoder {
|
||||
fn add_symbol(
|
||||
&mut self,
|
||||
block_id: u8,
|
||||
block_id: u16,
|
||||
symbol_index: u16,
|
||||
_is_repair: bool,
|
||||
data: &[u8],
|
||||
@@ -104,13 +104,13 @@ impl FecDecoder for RaptorQFecDecoder {
|
||||
padded[..len].copy_from_slice(&data[..len]);
|
||||
|
||||
let esi = symbol_index as u32;
|
||||
let packet = EncodingPacket::new(PayloadId::new(block_id, esi), padded);
|
||||
let packet = EncodingPacket::new(PayloadId::new((block_id & 0xFF) as u8, esi), padded);
|
||||
block.packets.push(packet);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_decode(&mut self, block_id: u8) -> Result<Option<Vec<Vec<u8>>>, FecError> {
|
||||
fn try_decode(&mut self, block_id: u16) -> Result<Option<Vec<Vec<u8>>>, FecError> {
|
||||
let frames_per_block = self.frames_per_block;
|
||||
let block = match self.blocks.get_mut(&block_id) {
|
||||
Some(b) => b,
|
||||
@@ -125,7 +125,7 @@ impl FecDecoder for RaptorQFecDecoder {
|
||||
let block_length = (num_source as u64) * (block.symbol_size as u64);
|
||||
|
||||
let config = ObjectTransmissionInformation::with_defaults(block_length, block.symbol_size);
|
||||
let mut decoder = SourceBlockDecoder::new(block_id, &config, block_length);
|
||||
let mut decoder = SourceBlockDecoder::new((block_id & 0xFF) as u8, &config, block_length);
|
||||
|
||||
let decoded = decoder.decode(block.packets.clone());
|
||||
|
||||
@@ -156,15 +156,15 @@ impl FecDecoder for RaptorQFecDecoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn expire_before(&mut self, block_id: u8) {
|
||||
fn expire_before(&mut self, block_id: u16) {
|
||||
// Remove blocks with IDs "older" than block_id.
|
||||
// With wrapping u8 IDs, we consider a block old if its distance
|
||||
// (in the forward direction) to block_id is > 128.
|
||||
// With wrapping u16 IDs, we consider a block old if its distance
|
||||
// (in the forward direction) to block_id is > 32768.
|
||||
self.blocks.retain(|&id, _| {
|
||||
let distance = block_id.wrapping_sub(id);
|
||||
// If distance is 0 or > 128, the block is current or "ahead" — keep it.
|
||||
// If distance is 1..=128, the block is behind — remove it.
|
||||
distance == 0 || distance > 128
|
||||
// If distance is 0 or > 32768, the block is current or "ahead" — keep it.
|
||||
// If distance is 1..=32768, the block is behind — remove it.
|
||||
distance == 0 || distance > 32768
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -263,9 +263,9 @@ mod tests {
|
||||
let mut decoder = RaptorQFecDecoder::new(FRAMES_PER_BLOCK, SYMBOL_SIZE);
|
||||
|
||||
// Add symbols to blocks 0, 1, 2
|
||||
for block_id in 0..3u8 {
|
||||
for block_id in 0..3u16 {
|
||||
decoder
|
||||
.add_symbol(block_id, 0, false, &[block_id; 50])
|
||||
.add_symbol(block_id, 0, false, &[block_id as u8; 50])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ 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 u8).
|
||||
block_id: u8,
|
||||
/// 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.
|
||||
@@ -122,7 +122,7 @@ impl FecEncoder for RaptorQFecEncoder {
|
||||
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, &config, &block_data);
|
||||
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;
|
||||
@@ -145,7 +145,7 @@ impl FecEncoder for RaptorQFecEncoder {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn finalize_block(&mut self) -> Result<u8, FecError> {
|
||||
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();
|
||||
@@ -153,7 +153,7 @@ impl FecEncoder for RaptorQFecEncoder {
|
||||
Ok(completed)
|
||||
}
|
||||
|
||||
fn current_block_id(&self) -> u8 {
|
||||
fn current_block_id(&self) -> u16 {
|
||||
self.block_id
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ fn build_prefixed_block_data(symbols: &[Vec<u8>], symbol_size: u16) -> Vec<u8> {
|
||||
/// Helper: build source `EncodingPacket`s for a given block. Useful for
|
||||
/// the decoder tests and interleaving.
|
||||
pub fn source_packets_for_block(
|
||||
block_id: u8,
|
||||
block_id: u16,
|
||||
symbols: &[Vec<u8>],
|
||||
symbol_size: u16,
|
||||
) -> Vec<EncodingPacket> {
|
||||
@@ -191,21 +191,21 @@ pub fn source_packets_for_block(
|
||||
.map(|i| {
|
||||
let offset = i * ss;
|
||||
let sym_data = data[offset..offset + ss].to_vec();
|
||||
EncodingPacket::new(PayloadId::new(block_id, i as u32), sym_data)
|
||||
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: u8,
|
||||
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, &config, &data);
|
||||
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)
|
||||
@@ -241,15 +241,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_id_wraps() {
|
||||
fn block_id_wraps_u16() {
|
||||
let mut enc = RaptorQFecEncoder::with_defaults(1);
|
||||
for expected in 0..=255u8 {
|
||||
// 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(&[expected; 10]).unwrap();
|
||||
enc.add_source_symbol(&[0u8; 10]).unwrap();
|
||||
enc.finalize_block().unwrap();
|
||||
}
|
||||
// After 256 blocks, wraps back to 0
|
||||
assert_eq!(enc.current_block_id(), 0);
|
||||
// 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]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! rather than one block fatally.
|
||||
|
||||
/// A symbol ready for transmission: (block_id, symbol_index, is_repair, data).
|
||||
pub type Symbol = (u8, u8, bool, Vec<u8>);
|
||||
pub type Symbol = (u16, u16, bool, Vec<u8>);
|
||||
|
||||
/// Temporal interleaver that mixes symbols across multiple FEC blocks.
|
||||
pub struct Interleaver {
|
||||
@@ -64,13 +64,13 @@ mod tests {
|
||||
let interleaver = Interleaver::with_default_depth();
|
||||
|
||||
let block_a: Vec<Symbol> = (0..3)
|
||||
.map(|i| (0u8, i as u8, false, vec![0xA0 + i as u8]))
|
||||
.map(|i| (0u16, i as u16, false, vec![0xA0 + i as u8]))
|
||||
.collect();
|
||||
let block_b: Vec<Symbol> = (0..3)
|
||||
.map(|i| (1u8, i as u8, false, vec![0xB0 + i as u8]))
|
||||
.map(|i| (1u16, i as u16, false, vec![0xB0 + i as u8]))
|
||||
.collect();
|
||||
let block_c: Vec<Symbol> = (0..3)
|
||||
.map(|i| (2u8, i as u8, false, vec![0xC0 + i as u8]))
|
||||
.map(|i| (2u16, i as u16, false, vec![0xC0 + i as u8]))
|
||||
.collect();
|
||||
|
||||
let result = interleaver.interleave(&[block_a, block_b, block_c]);
|
||||
@@ -96,10 +96,10 @@ mod tests {
|
||||
let interleaver = Interleaver::new(2);
|
||||
|
||||
let block_a: Vec<Symbol> = (0..3)
|
||||
.map(|i| (0u8, i as u8, false, vec![0xA0 + i as u8]))
|
||||
.map(|i| (0u16, i as u16, false, vec![0xA0 + i as u8]))
|
||||
.collect();
|
||||
let block_b: Vec<Symbol> = (0..1)
|
||||
.map(|i| (1u8, i as u8, false, vec![0xB0 + i as u8]))
|
||||
.map(|i| (1u16, i as u16, false, vec![0xB0 + i as u8]))
|
||||
.collect();
|
||||
|
||||
let result = interleaver.interleave(&[block_a, block_b]);
|
||||
@@ -128,7 +128,7 @@ mod tests {
|
||||
let blocks: Vec<Vec<Symbol>> = (0..3)
|
||||
.map(|b| {
|
||||
(0..6)
|
||||
.map(|i| (b as u8, i as u8, false, vec![b as u8; 10]))
|
||||
.map(|i| (b as u16, i as u16, false, vec![b as u8; 10]))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
|
||||
Reference in New Issue
Block a user