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:
Siavash Sameni
2026-05-25 15:30:26 +04:00
parent 01f55caa96
commit 06253fdeeb
44 changed files with 3221 additions and 163 deletions

View File

@@ -0,0 +1,242 @@
# PRD: Wire Format Hardening — FEC block_id u16, SignalMessage version byte, FEC repair index wrap
> **Status:** proposed
> **Resolves:** Three small wire-format defects (H2, M1, M4) that compound over time into silent data corruption or protocol breakage.
> **Depends on:** Nothing — purely mechanical changes to `wzp-fec` and `wzp-proto`.
## Problem
Three independent issues:
**H2 — `fec_block_id` u8 wraps too fast.** The `block_id` field in
`RaptorQFecEncoder` (and `RaptorQFecDecoder`) is `u8`. At 5 audio frames
per block and 50 fps this wraps every ~51 seconds. A slow receiver or a
mid-session join can receive packets from two different blocks with the same
`block_id`, silently corrupting FEC recovery.
**M1 — Some `SignalMessage` variants lack a `version` byte.** Most variants
have `#[serde(default = "default_signal_version")] version: u8`. The unit
variant `Reflect` (and potentially others added recently) does not. Future
protocol changes that key on `version` will silently misparse old messages
from peers without the field.
**M4 — FEC repair index can silently wrap at 255.** In
`crates/wzp-fec/src/encoder.rs` line 140:
```rust
let idx = (num_source as u16).wrapping_add(i as u16);
```
(The line was already fixed to `u16` — verify it is `u16`, not `u8`. If it
is still `u8`, the fix is below.)
If the line currently reads `(num_source as u8).wrapping_add(i as u8)`, then
when `num_source + repair_count > 255` the repair symbol indices wrap silently,
producing incorrect ESI values that the decoder cannot correlate to source
blocks.
## Goals
- **H2**: Widen `block_id` in encoder and decoder from `u8` to `u16`.
Update `finalize_block` return type and `current_block_id` return type in
the trait (`wzp-proto`) and implementations (`wzp-fec`).
- **M1**: Audit every `SignalMessage` variant; add
`#[serde(default = "default_signal_version")] version: u8` to any that
are missing it.
- **M4**: Confirm the repair index uses `u16`; fix it if it is still `u8`.
Update the decoder's `add_symbol` call site if the index type changes.
- `cargo test -p wzp-fec -p wzp-proto` passes; no existing tests broken.
## Non-goals
- Changing the wire encoding of `MediaHeaderV2::fec_block` — it is already
`u16` on the wire. This PRD only widens the **internal counter** to match.
- Multi-block decode concurrency or block expiry policy.
- Any crate outside `wzp-fec` and `wzp-proto`.
## Design
### Item A — `fec_block_id` u8 → u16
**Files**:
- `crates/wzp-proto/src/traits.rs``FecEncoder` and `FecDecoder` traits
- `crates/wzp-fec/src/encoder.rs``RaptorQFecEncoder`
- `crates/wzp-fec/src/decoder.rs``RaptorQFecDecoder`
**Trait changes** (`traits.rs`):
```rust
// Before:
fn finalize_block(&mut self) -> Result<u8, FecError>;
fn current_block_id(&self) -> u8;
fn add_symbol(&mut self, block_id: u8, ...) -> Result<(), FecError>;
fn try_decode(&mut self, block_id: u8) -> Result<...>;
fn expire_before(&mut self, block_id: u8);
```
```rust
// After:
fn finalize_block(&mut self) -> Result<u16, FecError>;
fn current_block_id(&self) -> u16;
fn add_symbol(&mut self, block_id: u16, ...) -> Result<(), FecError>;
fn try_decode(&mut self, block_id: u16) -> Result<...>;
fn expire_before(&mut self, block_id: u16);
```
**Encoder changes** (`encoder.rs`):
- Change `block_id: u8` field to `block_id: u16`.
- Update `self.block_id.wrapping_add(1)` (already u16 semantics; keep as is).
- Update `finalize_block` to return `u16`.
- Update `current_block_id` to return `u16`.
- Update all tests that assert `block_id == 0u8``== 0u16`, and the
wrap test (`block_id_wraps`) to iterate to `u16::MAX` (65535) — or reduce
it to 300 iterations to keep it fast, asserting the wrap at 65536.
The wrap test at 256 iterations (`0..=255u8`) must be updated; a full
`u16` wrap test at 65536 iterations is too slow for CI. Change to:
```rust
#[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);
}
```
Note: `block_id` is a private field; expose a test helper or set it in a
`#[cfg(test)]` `impl` block.
**Decoder changes** (`decoder.rs`):
- Change `blocks: HashMap<u8, BlockState>` to `HashMap<u16, BlockState>`.
- Update `get_or_create_block(block_id: u8)``get_or_create_block(block_id: u16)`.
- Update `add_symbol`, `try_decode`, `expire_before` signatures to `u16`.
- The `SourceBlockEncoder::new(self.block_id, ...)` call in `encoder.rs` passes
`block_id` to `raptorq`. RaptorQ uses `u8` for source block number internally.
Cast it: `(block_id & 0xFF) as u8` or `(block_id % 256) as u8` — the `raptorq`
crate's source block ID is a logical identifier within a single object
transmission, not a global counter. The u16 is our session counter; truncate
to u8 when calling into raptorq.
### Item B — `SignalMessage` version byte audit
**File**: `crates/wzp-proto/src/packet.rs`
Read every variant in the `SignalMessage` enum (lines 5551241) and check
for the presence of:
```rust
#[serde(default = "default_signal_version")]
version: u8,
```
The `Reflect` variant at line 974 is a **unit variant** (no fields). Unit
variants cannot carry a `version` field without becoming struct variants.
Change it to a struct variant:
```rust
// Before:
Reflect,
// After:
Reflect {
#[serde(default = "default_signal_version")]
version: u8,
},
```
This is a wire-compatible change: serde JSON struct variants serialize as
`{"Reflect": {"version": 1}}` whereas unit variants serialize as
`"Reflect"`. These are **not** backward-compatible formats. Since `Reflect`
is sent client → relay only and the relay immediately responds, upgrading
both sides atomically is acceptable. Add a serde test to confirm round-trip.
For any other variants missing `version`, follow the same pattern as all
existing variants.
Verify by grepping the enum for variants that do NOT have `version`:
```bash
grep -A3 "^\s*[A-Z][A-Za-z]*\s*{" crates/wzp-proto/src/packet.rs | \
grep -B1 -v "serde.*default_signal_version\|version:"
```
### Item C — FEC repair index wrap (M4)
**File**: `crates/wzp-fec/src/encoder.rs`, line ~140.
Current code:
```rust
let idx = (num_source as u16).wrapping_add(i as u16);
```
If this line already uses `u16` (as shown in the file at line 140), M4 is
already fixed. Verify by reading the current file. If it still reads
`u8`, apply:
```rust
let idx = (num_source as u16).wrapping_add(i as u16);
```
**Decoder** (`crates/wzp-fec/src/decoder.rs`): `add_symbol` already accepts
`symbol_index: u16` (per the trait). Confirm the parameter flows through to
`PayloadId::new(block_id_u8, symbol_index as u32)` without truncation.
## Implementation steps
1. Read `crates/wzp-proto/src/traits.rs` lines 60116 (FecEncoder/FecDecoder
trait definitions) to confirm current signatures.
2. Read `crates/wzp-fec/src/encoder.rs` and `decoder.rs` (full files).
3. Apply Item C fix first (smallest change, easiest to verify).
4. Apply Item A: widen `block_id` from u8 to u16 in traits, encoder, decoder.
Update all callers by running `cargo check -p wzp-fec -p wzp-proto` and
fixing each E0308/E0308 error.
5. Apply Item B: read every variant, add missing `version` fields.
Change `Reflect` to a struct variant.
6. Run tests.
## Files to read before implementing
- `crates/wzp-proto/src/traits.rs` lines 60116 (trait signatures)
- `crates/wzp-fec/src/encoder.rs` (full)
- `crates/wzp-fec/src/decoder.rs` (full)
- `crates/wzp-proto/src/packet.rs` lines 5551241 (all `SignalMessage` variants)
## Verify
```bash
cargo test -p wzp-fec -p wzp-proto
```
Expected: all tests pass, 0 failures. Also run:
```bash
cargo check --workspace
```
to catch any call sites outside `wzp-fec` and `wzp-proto` that passed `u8`
block IDs to the trait methods.
## Done when
- `cargo test -p wzp-fec -p wzp-proto` exits 0.
- `block_id` is `u16` in `RaptorQFecEncoder`, `RaptorQFecDecoder`, and the
`FecEncoder`/`FecDecoder` traits.
- Every non-unit `SignalMessage` variant has a `version: u8` field with
`#[serde(default = "default_signal_version")]`.
- Repair index in `encoder.rs` is computed with `u16` arithmetic.
- No existing tests are broken.