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>
8.3 KiB
PRD: Quality Upgrade Flow — UpgradeProposal / Response / Confirm
Status: proposed Resolves: Four TODO comments in the signal task of
desktop/src-tauri/src/lib.rsthat leave quality upgrade messages unhandled. Audio quality never upgrades mid-call even when the network improves. Depends on:wzp_proto::SignalMessage::{UpgradeProposal, UpgradeResponse, UpgradeConfirm, QualityCapability}(already defined incrates/wzp-proto/src/packet.rs).
Problem
The signal receive task in lib.rs matches UpgradeProposal, UpgradeResponse,
UpgradeConfirm, and QualityCapability messages from the peer, logs them,
then hits a // TODO comment and does nothing. The 4 TODOs are at lines
1930, 1949, 1966, and 1985 of desktop/src-tauri/src/lib.rs.
Consequence: audio quality is frozen at the profile negotiated at call start. Even when the network improves, the encoder never upgrades.
Goals
UpgradeProposalauto-accepts and sendsUpgradeResponse { accepted: true }.- Accepted
UpgradeResponsesendsUpgradeConfirmand switches the local encoder. - Received
UpgradeConfirmswitches the local encoder. - Received
QualityCapabilitycaps the local encoder to the peer's max profile. - A unit test verifies the accept/confirm round-trip.
cargo check --manifest-path desktop/src-tauri/Cargo.tomlpasses.
Non-goals
- UI for manual accept/reject of upgrade proposals (auto-accept only).
- Sending
UpgradeProposalfrom our side (the outgoing path already exists inlib.rs; this PRD only handles receiving). - Downgrade negotiation.
- Persisting quality profiles across calls.
Design
New shared state
Add the following to AppState (or as captured variables in the signal task
closure — whichever is cleaner given the existing structure):
/// Pending outgoing upgrade: (call_id, proposal_id, profile).
/// Set when we send an UpgradeProposal, consumed when we receive an accepted UpgradeResponse.
pending_upgrade: Arc<Mutex<Option<(String, String, QualityProfile)>>>,
/// Current quality profile for the encoder. The audio send task reads this
/// at the start of each encode cycle.
active_quality: Arc<Mutex<QualityProfile>>,
/// Peer's reported maximum quality cap. The audio send task clamps to min(active, peer_max).
peer_max_quality: Arc<Mutex<Option<QualityProfile>>>,
If AppState already holds these fields (check lib.rs for the struct
definition), reuse them instead of adding duplicates.
Handler implementations
1. UpgradeProposal (line ~1930)
// Replace the TODO comment with:
let response = SignalMessage::UpgradeResponse {
version: wzp_proto::default_signal_version(),
call_id: call_id.clone(),
proposal_id: proposal_id.clone(),
accepted: true,
reason: None,
};
if let Err(e) = signal_transport.send_signal(&response).await {
tracing::warn!("failed to send UpgradeResponse: {e}");
}
signal_transport is whatever variable holds the signal Arc<dyn MediaTransport>
in scope at that match arm. Inspect the enclosing task to find the right name.
2. UpgradeResponse (line ~1949)
// Replace the TODO comment with:
if accepted {
// Retrieve the pending proposal to get the confirmed_profile.
let maybe_proposal = pending_upgrade.lock().unwrap().take();
if let Some((_cid, pid, profile)) = maybe_proposal {
if pid == proposal_id {
// Send UpgradeConfirm.
let confirm = SignalMessage::UpgradeConfirm {
version: wzp_proto::default_signal_version(),
call_id: call_id.clone(),
proposal_id: proposal_id.clone(),
confirmed_profile: profile.clone(),
};
if let Err(e) = signal_transport.send_signal(&confirm).await {
tracing::warn!("failed to send UpgradeConfirm: {e}");
}
// Switch our encoder.
*active_quality.lock().unwrap() = profile;
}
}
}
If pending_upgrade is a captured Arc<Mutex<...>> in the task closure, it
can be read/written without going through AppState.
3. UpgradeConfirm (line ~1966)
// Replace the TODO comment with:
*active_quality.lock().unwrap() = confirmed_profile;
The audio send task (in engine.rs) reads active_quality at the start of
each encode cycle and reconfigures the Opus encoder bitrate accordingly.
4. QualityCapability (line ~1985)
// Replace the TODO comment with:
*peer_max_quality.lock().unwrap() = Some(max_profile);
5. Audio send task changes (engine.rs)
The audio send task already runs in a loop. Add a quality-check at the top of each encode iteration:
// At the start of the encode loop body:
let effective_profile = {
let active = active_quality.lock().unwrap().clone();
let peer_cap = peer_max_quality.lock().unwrap().clone();
match peer_cap {
Some(cap) if cap.opus_bitrate_bps() < active.opus_bitrate_bps() => cap,
_ => active,
}
};
// Pass effective_profile to encoder if it changed since last iteration.
QualityProfile::opus_bitrate_bps() already exists (check
crates/wzp-proto/src/codec_id.rs). If QualityProfile does not have a
direct bitrate accessor, compare using the PartialOrd impl or a helper that
ranks profiles numerically.
To avoid calling encoder.set_bitrate() every single frame, cache the last
applied profile and only reconfigure on change:
let mut last_applied_profile: Option<QualityProfile> = None;
// Inside loop:
if Some(&effective_profile) != last_applied_profile.as_ref() {
encoder.set_bitrate(effective_profile.opus_bitrate_bps());
last_applied_profile = Some(effective_profile.clone());
}
encoder.set_bitrate(bps: u32) — add this method to OpusEncoder in
crates/wzp-codec/src/opus_enc.rs if it does not exist. It wraps
opus_encoder_ctl(OPUS_SET_BITRATE_REQUEST, bps).
Unit tests
Add a #[cfg(test)] module in lib.rs (or a dedicated test file) that:
- Creates a
LoopbackSignalTransportstub that records sentSignalMessages. - Calls the
UpgradeProposalhandler logic directly, asserts that anUpgradeResponse { accepted: true }was sent. - Calls the
UpgradeResponse { accepted: true }handler with a pre-populatedpending_upgrade, asserts thatUpgradeConfirmwas sent andactive_qualitywas updated.
These can be pure unit tests (no Tauri or audio), since the handlers are pure async functions over captured state.
Implementation steps
- Read
desktop/src-tauri/src/lib.rslines 1910–1990 (the four TODO blocks) and the surrounding signal task structure to identify the variable names forsignal_transport,app_state, and any existing quality-state fields. - Read
desktop/src-tauri/src/engine.rsforCallEnginestruct fields and the audio send task loop. - Read
crates/wzp-proto/src/codec_id.rsforQualityProfilemethods. - Add
pending_upgrade,active_quality,peer_max_qualityto the appropriate shared state (or as closure captures in the signal task). - Replace the 4 TODO comments with the handlers described above.
- Add
set_bitratetoOpusEncoderif missing. - Update the audio send task to read
active_quality/peer_max_qualityeach iteration. - Add unit tests.
- Run
cargo check --manifest-path desktop/src-tauri/Cargo.toml.
Files to read before implementing
desktop/src-tauri/src/lib.rs— grep forUpgradeProposalto find the exact lines; also read the surrounding signal task for variable names.crates/wzp-proto/src/packet.rslines 1130–1190 —UpgradeProposal,UpgradeResponse,UpgradeConfirm,QualityCapabilitystruct layouts.desktop/src-tauri/src/engine.rs—CallEnginestruct fields, audio send task loop.crates/wzp-proto/src/codec_id.rs—QualityProfilemethods.crates/wzp-codec/src/opus_enc.rs—OpusEncoderAPI.
Verify
cargo check --manifest-path desktop/src-tauri/Cargo.toml
cargo test -p wzp-desktop 2>/dev/null || cargo test --manifest-path desktop/src-tauri/Cargo.toml
Expected: 0 errors; unit tests pass.
Done when
- All 4 TODO comments replaced with real logic.
cargo check --manifest-path desktop/src-tauri/Cargo.tomlexits 0.- Unit test verifies:
UpgradeProposal→UpgradeResponse { accepted: true }sent;UpgradeResponse { accepted: true }→UpgradeConfirmsent +active_qualityupdated.