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

@@ -42,6 +42,7 @@ pub async fn accept_handshake(
supported_profiles,
caller_alias,
protocol_version,
caller_video_codecs,
) = match offer {
SignalMessage::CallOffer {
identity_pub,
@@ -51,6 +52,7 @@ pub async fn accept_handshake(
alias,
protocol_version,
supported_versions: _,
video_codecs,
..
} => (
identity_pub,
@@ -59,6 +61,7 @@ pub async fn accept_handshake(
supported_profiles,
alias,
protocol_version,
video_codecs,
),
other => {
return Err(anyhow::anyhow!(
@@ -108,6 +111,9 @@ pub async fn accept_handshake(
// Choose the best supported profile (prefer GOOD > DEGRADED > CATASTROPHIC)
let chosen_profile = choose_profile(&supported_profiles);
// Pick the first video codec the caller supports (relay forwards all video).
let video_codec = caller_video_codecs.into_iter().next();
// 6. Send CallAnswer
let answer = SignalMessage::CallAnswer {
version: default_signal_version(),
@@ -115,6 +121,7 @@ pub async fn accept_handshake(
ephemeral_pub,
signature,
chosen_profile,
video_codec,
};
transport.send_signal(&answer).await?;
@@ -147,6 +154,7 @@ fn choose_profile(_supported: &[QualityProfile]) -> QualityProfile {
#[cfg(test)]
mod tests {
use super::*;
use wzp_proto::CodecId;
#[test]
fn choose_profile_picks_highest_bitrate() {
@@ -164,4 +172,35 @@ mod tests {
let chosen = choose_profile(&[]);
assert_eq!(chosen, QualityProfile::GOOD);
}
// ── Video codec negotiation ───────────────────────────────────────
#[test]
fn video_codec_picks_first_offered() {
let codecs = vec![CodecId::Av1Main, CodecId::H264Baseline, CodecId::H265Main];
let chosen: Option<CodecId> = codecs.into_iter().next();
assert_eq!(chosen, Some(CodecId::Av1Main));
}
#[test]
fn video_codec_none_when_no_codecs_offered() {
let codecs: Vec<CodecId> = vec![];
let chosen: Option<CodecId> = codecs.into_iter().next();
assert_eq!(chosen, None);
}
#[test]
fn video_codec_single_codec_is_selected() {
let codecs = vec![CodecId::H265Main];
let chosen: Option<CodecId> = codecs.into_iter().next();
assert_eq!(chosen, Some(CodecId::H265Main));
}
#[test]
fn video_codec_order_is_preserved() {
// The relay must pick the FIRST codec as-offered, not sort or re-rank.
let codecs = vec![CodecId::H264Baseline, CodecId::Av1Main];
let chosen: Option<CodecId> = codecs.into_iter().next();
assert_eq!(chosen, Some(CodecId::H264Baseline));
}
}

View File

@@ -110,7 +110,7 @@ impl RelayPipeline {
// Feed packet into FEC decoder
let header = &packet.header;
let _ = self.fec_decoder.add_symbol(
(header.fec_block & 0xFF) as u8,
header.fec_block,
header.fec_block >> 8,
header.is_repair(),
&packet.payload,
@@ -118,7 +118,7 @@ impl RelayPipeline {
// Try to decode the FEC block
let mut output = Vec::new();
if let Ok(Some(frames)) = self.fec_decoder.try_decode((header.fec_block & 0xFF) as u8) {
if let Ok(Some(frames)) = self.fec_decoder.try_decode(header.fec_block) {
debug!(
block = header.fec_block,
frames = frames.len(),

View File

@@ -87,7 +87,7 @@ async fn handshake_succeeds() {
let callee_handle =
tokio::spawn(async move { accept_handshake(server_t.as_ref(), &callee_seed).await });
let caller_session = perform_handshake(client_transport.as_ref(), &caller_seed, None)
let caller_hs = perform_handshake(client_transport.as_ref(), &caller_seed, None)
.await
.expect("perform_handshake should succeed");
@@ -102,7 +102,7 @@ async fn handshake_succeeds() {
let plaintext = b"hello warzone";
let mut ciphertext = Vec::new();
let mut caller_session = caller_session;
let mut caller_session = caller_hs.session;
let mut callee_session = callee_session;
caller_session
@@ -156,6 +156,7 @@ async fn handshake_rejects_v1_protocol_version() {
alias: None,
protocol_version: 1,
supported_versions: vec![1, 2],
video_codecs: vec![],
};
client_transport
@@ -221,7 +222,7 @@ async fn handshake_verifies_identity() {
let callee_handle =
tokio::spawn(async move { accept_handshake(server_t.as_ref(), &callee_seed).await });
let caller_session = perform_handshake(client_transport.as_ref(), &caller_seed, None)
let caller_hs = perform_handshake(client_transport.as_ref(), &caller_seed, None)
.await
.expect("handshake must succeed even with different identities");
@@ -235,7 +236,7 @@ async fn handshake_verifies_identity() {
let plaintext = b"identity verified";
let mut ct = Vec::new();
let mut caller_session = caller_session;
let mut caller_session = caller_hs.session;
let mut callee_session = callee_session;
caller_session
@@ -301,7 +302,7 @@ async fn auth_then_handshake() {
.await
.expect("send AuthToken");
let caller_session = perform_handshake(client_transport.as_ref(), &caller_seed, None)
let caller_hs = perform_handshake(client_transport.as_ref(), &caller_seed, None)
.await
.expect("perform_handshake after auth");
@@ -315,7 +316,7 @@ async fn auth_then_handshake() {
let plaintext = b"post-auth payload";
let mut ct = Vec::new();
let mut caller_session = caller_session;
let mut caller_session = caller_hs.session;
let mut callee_session = callee_session;
caller_session
@@ -373,6 +374,7 @@ async fn handshake_rejects_bad_signature() {
alias: None,
protocol_version: 2,
supported_versions: vec![2],
video_codecs: vec![],
};
client_transport