- QualityDirective signal roundtrip tests (with/without reason) - ParticipantQuality unit tests (initial tier, degradation, weakest-link) - Updated PROGRESS.md with desktop adaptive quality, relay coordinated switching, Oboe state polling entries - Updated ARCHITECTURE.md SFU fan-out rules with QualityDirective - Updated PRD-coordinated-codec.md with implementation status - 312 tests passing across all modified crates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8.8 KiB
PRD: Coordinated Codec Switching (Relay-Judged Quality)
Problem
The current adaptive quality system (QualityAdapter in call.rs) exists but isn't wired into either engine. Clients encode at a fixed quality chosen at call start. When network conditions change mid-call, audio degrades instead of gracefully stepping down. When conditions improve, clients stay on low quality unnecessarily.
Additionally, in SFU mode with multiple participants, uncoordinated codec switching creates asymmetry: if client A upgrades to 64k while B stays on 24k, bandwidth is wasted. Participants should switch together.
Solution
The relay acts as the quality judge since it sees both sides of every connection. It monitors packet loss, jitter, and RTT per participant, then signals quality recommendations. Clients react to these signals with coordinated codec switches.
Architecture
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Client A │◄──────►│ Relay │◄──────►│ Client B │
│ │ │ (judge) │ │ │
│ Encoder │ │ │ │ Encoder │
│ Decoder │ │ Monitor │ │ Decoder │
└─────────┘ │ per-peer│ └─────────┘
│ quality │
└────┬────┘
│
Quality Signals:
- StableSignal (conditions good)
- DegradeSignal (conditions bad)
- UpgradeProposal (try higher quality?)
- UpgradeConfirm (all agreed, switch at T)
Quality Classification (Relay-Side)
The relay monitors each participant's connection quality:
| Condition | Classification | Action |
|---|---|---|
| loss >= 15% OR RTT >= 200ms | Critical | Immediate downgrade signal |
| loss >= 5% OR RTT >= 100ms | Degraded | Downgrade signal after 3 reports |
| loss < 2% AND RTT < 80ms | Good | Stable signal |
| loss < 1% AND RTT < 50ms for 30s | Excellent | Upgrade proposal |
| loss < 0.5% AND RTT < 30ms for 60s | Studio | Studio upgrade proposal |
Coordinated Switching Protocol
Downgrade (fast, safety-first)
- Relay detects degradation for ANY participant
- Relay sends
QualityUpdate { recommended_profile: DEGRADED }to ALL participants - ALL participants immediately switch encoder to the recommended profile
- No negotiation — downgrade is mandatory and instant
Upgrade (slow, consensual)
- Relay detects sustained good conditions for ALL participants (threshold: 30s stable)
- Relay sends
UpgradeProposal { target_profile, switch_timestamp }to all - Each client responds:
UpgradeAcceptorUpgradeReject - If ALL accept within 5s → Relay sends
UpgradeConfirm { profile, switch_at_ms } - All clients switch encoder at the agreed timestamp (relative to session clock)
- If ANY rejects or times out → upgrade cancelled, stay on current profile
Asymmetric Encoding (SFU optimization)
In SFU mode, each client encodes independently. The relay could allow:
- Client A (strong connection): encode at 64k
- Client B (weak connection): encode at 6k
- Relay forwards A's 64k to B's decoder (auto-switch handles it)
- B benefits from A's quality without needing to send at 64k
This requires NO protocol changes — just each client independently following the relay's recommendation for their own encoding quality. The decoder already handles any codec.
Split Network Consideration
If participant A has great quality but participant C has terrible quality:
- Option 1: Match weakest link — everyone encodes at C's level (current approach, simple)
- Option 2: Per-participant recommendations — A encodes at 64k, C encodes at 6k. B (good connection) receives and decodes both. Works because decoders auto-switch per packet.
- Option 3: Relay transcoding — relay re-encodes A's 64k as 6k for C. Adds CPU on relay, but saves bandwidth for C. Future feature.
Recommended: start with Option 1 (match weakest), add Option 2 later.
Signal Messages (New/Modified)
/// Quality signal from relay to client
QualityDirective {
/// Recommended profile to use for encoding
recommended_profile: QualityProfile,
/// Reason for the recommendation
reason: QualityReason,
}
enum QualityReason {
/// Network conditions require this quality level
NetworkCondition,
/// Coordinated upgrade — all participants agreed
CoordinatedUpgrade,
/// Coordinated downgrade — weakest link determines level
CoordinatedDowngrade,
}
/// Upgrade proposal from relay
UpgradeProposal {
target_profile: QualityProfile,
/// Milliseconds from now when the switch would happen
switch_delay_ms: u32,
}
/// Client response to upgrade proposal
UpgradeResponse {
accepted: bool,
}
/// Confirmed upgrade — all clients switch at this time
UpgradeConfirm {
profile: QualityProfile,
/// Session-relative timestamp to switch (ms since call start)
switch_at_session_ms: u64,
}
Relay-Side Implementation
Per-Participant Quality Tracking
struct ParticipantQuality {
/// Sliding window of recent observations
loss_samples: VecDeque<f32>, // last 30 seconds
rtt_samples: VecDeque<u32>, // last 30 seconds
jitter_samples: VecDeque<u32>,
/// Current classification
classification: QualityClass,
/// How long current classification has been stable
stable_since: Instant,
}
Quality Monitor Task (on relay)
Runs alongside the SFU forwarding loop:
- Every 1 second, compute per-participant quality from QUIC connection stats
- Classify each participant
- If ANY participant degrades → send downgrade to ALL
- If ALL participants stable for threshold → propose upgrade
- Track upgrade negotiation state
Integration with Existing Code
The relay already has access to:
QuinnTransport::path_quality()→ loss, RTT, jitter, bandwidth estimatesQualityReportembedded in media packet headers- Per-session metrics in
RelayMetrics
The quality monitor just needs to read these existing metrics and produce signals.
Client-Side Implementation
Handling Quality Signals
In the recv loop (both Android engine and desktop engine):
SignalMessage::QualityDirective { recommended_profile, .. } => {
// Immediate: switch encoder to recommended profile
encoder.set_profile(recommended_profile)?;
fec_enc = create_encoder(&recommended_profile);
frame_samples = frame_samples_for(&recommended_profile);
info!(codec = ?recommended_profile.codec, "quality directive: switched");
}
P2P Quality (simpler case)
For P2P calls (no relay), both clients directly observe quality:
- Each client runs its own
QualityAdapteron the direct connection - When quality changes, client proposes to peer via signal
- Simpler negotiation: only 2 parties, no relay middleman
- Same coordinated switching logic, just peer-to-peer signals
Backporting P2P → Relay
The quality monitoring and codec switching logic is identical:
- P2P: client observes quality directly → proposes switch to peer
- Relay: relay observes quality → proposes switch to all clients
The only difference is WHO makes the decision (client vs relay) and HOW many participants need to agree (2 vs N).
Implementation strategy: build for P2P first (simpler, 2 parties), then wrap the same logic with relay-mediated signals for SFU mode.
Milestones
| Phase | Scope | Effort |
|---|---|---|
| 1 | Relay-side quality monitor (per-participant tracking) | 1 day |
| 2 | Downgrade signal (immediate, match weakest) | 1 day |
| 3 | Client handling of QualityDirective | 1 day (both engines) |
| 4 | Upgrade proposal + negotiation protocol | 2 days |
| 5 | P2P quality adaptation (direct observation) | 1 day |
| 6 | Per-participant asymmetric encoding (Option 2) | 1 day |
Implementation Status (2026-04-12)
Phases 1-2 are now implemented:
What was built
QualityDirectivesignal (crates/wzp-proto/src/packet.rs): NewSignalMessagevariant withrecommended_profileand optionalreasonParticipantQuality(crates/wzp-relay/src/room.rs): Per-participant quality tracking usingAdaptiveQualityController, created on join, removed on leave- Weakest-link broadcast:
observe_quality()method computes room-wide worst tier, broadcastsQualityDirectiveto all participants when tier changes - Desktop engine handling (
desktop/src-tauri/src/engine.rs):AdaptiveQualityControllerin recv task,pending_profileAtomicU8 bridge to send task, auto-mode profile switching
Phases 3-4 remaining
- Phase 3: Client-side handling of
QualityDirective(reacting to relay-pushed profile) - Phase 4: Upgrade proposal/negotiation protocol for quality recovery