From 395a0c557e22d423ab6629dffcd992a83b4f8572 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Wed, 8 Apr 2026 12:03:20 +0400 Subject: [PATCH] feat: TX/RX codec badges on desktop call screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Desktop now shows codec badges like Android: - Green TX badge: e.g. "Opus64k" - Blue RX badge: e.g. "Opus24k" Displayed in the stats line below the call controls. Engine tracks tx_codec (set on encoder init) and rx_codec (updated from incoming packet headers). Passed through EngineStatus → CallStatus → frontend. Co-Authored-By: Claude Opus 4.6 (1M context) --- desktop/src-tauri/src/engine.rs | 19 +++++++++++++++++++ desktop/src-tauri/src/main.rs | 6 ++++++ desktop/src/main.ts | 5 ++++- desktop/src/style.css | 21 +++++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 692159d..4247238 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -57,6 +57,8 @@ pub struct EngineStatus { pub audio_level: u32, pub call_duration_secs: f64, pub fingerprint: String, + pub tx_codec: String, + pub rx_codec: String, } pub struct CallEngine { @@ -67,6 +69,8 @@ pub struct CallEngine { frames_sent: Arc, frames_received: Arc, audio_level: Arc, + tx_codec: Arc>, + rx_codec: Arc>, transport: Arc, start_time: Instant, fingerprint: String, @@ -187,6 +191,8 @@ impl CallEngine { let frames_sent = Arc::new(AtomicU64::new(0)); let frames_received = Arc::new(AtomicU64::new(0)); let audio_level = Arc::new(AtomicU32::new(0)); + let tx_codec = Arc::new(Mutex::new(String::new())); + let rx_codec = Arc::new(Mutex::new(String::new())); // Send task let send_t = transport.clone(); @@ -196,6 +202,7 @@ impl CallEngine { let send_level = audio_level.clone(); let send_drops = Arc::new(AtomicU64::new(0)); let send_quality = quality.clone(); + let send_tx_codec = tx_codec.clone(); tokio::spawn(async move { let profile = resolve_quality(&send_quality); let config = match profile { @@ -212,6 +219,7 @@ impl CallEngine { }; let frame_samples = (config.profile.frame_duration_ms as usize) * 48; info!(codec = ?config.profile.codec, frame_samples, "send task starting"); + *send_tx_codec.lock().await = format!("{:?}", config.profile.codec); let mut encoder = CallEncoder::new(&config); encoder.set_aec_enabled(false); // OS AEC or none let mut buf = vec![0i16; frame_samples]; @@ -259,6 +267,7 @@ impl CallEngine { let recv_r = running.clone(); let recv_spk = spk_muted.clone(); let recv_fr = frames_received.clone(); + let recv_rx_codec = rx_codec.clone(); tokio::spawn(async move { let initial_profile = resolve_quality(&quality).unwrap_or(QualityProfile::GOOD); let mut decoder = wzp_codec::create_decoder(initial_profile); @@ -278,6 +287,12 @@ impl CallEngine { { Ok(Ok(Some(pkt))) => { if !pkt.header.is_repair && pkt.header.codec_id != CodecId::ComfortNoise { + // Track RX codec + { + let mut rx = recv_rx_codec.lock().await; + let codec_name = format!("{:?}", pkt.header.codec_id); + if *rx != codec_name { *rx = codec_name; } + } // Auto-switch decoder if incoming codec differs if pkt.header.codec_id != current_codec { let new_profile = match pkt.header.codec_id { @@ -373,6 +388,8 @@ impl CallEngine { transport, start_time: Instant::now(), fingerprint, + tx_codec, + rx_codec, _audio_handle: SyncWrapper(audio_handle), }) } @@ -410,6 +427,8 @@ impl CallEngine { audio_level: self.audio_level.load(Ordering::Relaxed), call_duration_secs: self.start_time.elapsed().as_secs_f64(), fingerprint: self.fingerprint.clone(), + tx_codec: self.tx_codec.lock().await.clone(), + rx_codec: self.rx_codec.lock().await.clone(), } } diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 1ce3568..0ea9255 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -32,6 +32,8 @@ struct CallStatus { audio_level: u32, call_duration_secs: f64, fingerprint: String, + tx_codec: String, + rx_codec: String, } struct AppState { @@ -204,6 +206,8 @@ async fn get_status(state: tauri::State<'_, Arc>) -> Result>) -> Result${escapeHtml((st as any).tx_codec)}` : ""; + const rxBadge = (st as any).rx_codec ? `${escapeHtml((st as any).rx_codec)}` : ""; + statsDiv.innerHTML = `${txBadge} ${rxBadge} TX: ${st.encode_fps} | RX: ${st.recv_fps}`; } catch {} } diff --git a/desktop/src/style.css b/desktop/src/style.css index 33984be..30489be 100644 --- a/desktop/src/style.css +++ b/desktop/src/style.css @@ -470,6 +470,27 @@ button.primary:disabled { opacity: 0.5; cursor: not-allowed; } .relay-dot-small.green { background: var(--green); } .relay-dot-small.blue { background: #60a5fa; } +/* ── Codec badges ── */ +.codec-badge { + display: inline-block; + font-size: 10px; + font-weight: 600; + padding: 1px 6px; + border-radius: 4px; + font-family: monospace; + margin: 0 2px; +} + +.codec-badge.tx { + background: #22c55e30; + color: #4ade80; +} + +.codec-badge.rx { + background: #3b82f630; + color: #60a5fa; +} + /* ── Controls ── */ .controls { display: flex;