feat: TX/RX codec badges on desktop call screen
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) <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,8 @@ pub struct EngineStatus {
|
|||||||
pub audio_level: u32,
|
pub audio_level: u32,
|
||||||
pub call_duration_secs: f64,
|
pub call_duration_secs: f64,
|
||||||
pub fingerprint: String,
|
pub fingerprint: String,
|
||||||
|
pub tx_codec: String,
|
||||||
|
pub rx_codec: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CallEngine {
|
pub struct CallEngine {
|
||||||
@@ -67,6 +69,8 @@ pub struct CallEngine {
|
|||||||
frames_sent: Arc<AtomicU64>,
|
frames_sent: Arc<AtomicU64>,
|
||||||
frames_received: Arc<AtomicU64>,
|
frames_received: Arc<AtomicU64>,
|
||||||
audio_level: Arc<AtomicU32>,
|
audio_level: Arc<AtomicU32>,
|
||||||
|
tx_codec: Arc<Mutex<String>>,
|
||||||
|
rx_codec: Arc<Mutex<String>>,
|
||||||
transport: Arc<wzp_transport::QuinnTransport>,
|
transport: Arc<wzp_transport::QuinnTransport>,
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
@@ -187,6 +191,8 @@ impl CallEngine {
|
|||||||
let frames_sent = Arc::new(AtomicU64::new(0));
|
let frames_sent = Arc::new(AtomicU64::new(0));
|
||||||
let frames_received = Arc::new(AtomicU64::new(0));
|
let frames_received = Arc::new(AtomicU64::new(0));
|
||||||
let audio_level = Arc::new(AtomicU32::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
|
// Send task
|
||||||
let send_t = transport.clone();
|
let send_t = transport.clone();
|
||||||
@@ -196,6 +202,7 @@ impl CallEngine {
|
|||||||
let send_level = audio_level.clone();
|
let send_level = audio_level.clone();
|
||||||
let send_drops = Arc::new(AtomicU64::new(0));
|
let send_drops = Arc::new(AtomicU64::new(0));
|
||||||
let send_quality = quality.clone();
|
let send_quality = quality.clone();
|
||||||
|
let send_tx_codec = tx_codec.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let profile = resolve_quality(&send_quality);
|
let profile = resolve_quality(&send_quality);
|
||||||
let config = match profile {
|
let config = match profile {
|
||||||
@@ -212,6 +219,7 @@ impl CallEngine {
|
|||||||
};
|
};
|
||||||
let frame_samples = (config.profile.frame_duration_ms as usize) * 48;
|
let frame_samples = (config.profile.frame_duration_ms as usize) * 48;
|
||||||
info!(codec = ?config.profile.codec, frame_samples, "send task starting");
|
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);
|
let mut encoder = CallEncoder::new(&config);
|
||||||
encoder.set_aec_enabled(false); // OS AEC or none
|
encoder.set_aec_enabled(false); // OS AEC or none
|
||||||
let mut buf = vec![0i16; frame_samples];
|
let mut buf = vec![0i16; frame_samples];
|
||||||
@@ -259,6 +267,7 @@ impl CallEngine {
|
|||||||
let recv_r = running.clone();
|
let recv_r = running.clone();
|
||||||
let recv_spk = spk_muted.clone();
|
let recv_spk = spk_muted.clone();
|
||||||
let recv_fr = frames_received.clone();
|
let recv_fr = frames_received.clone();
|
||||||
|
let recv_rx_codec = rx_codec.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let initial_profile = resolve_quality(&quality).unwrap_or(QualityProfile::GOOD);
|
let initial_profile = resolve_quality(&quality).unwrap_or(QualityProfile::GOOD);
|
||||||
let mut decoder = wzp_codec::create_decoder(initial_profile);
|
let mut decoder = wzp_codec::create_decoder(initial_profile);
|
||||||
@@ -278,6 +287,12 @@ impl CallEngine {
|
|||||||
{
|
{
|
||||||
Ok(Ok(Some(pkt))) => {
|
Ok(Ok(Some(pkt))) => {
|
||||||
if !pkt.header.is_repair && pkt.header.codec_id != CodecId::ComfortNoise {
|
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
|
// Auto-switch decoder if incoming codec differs
|
||||||
if pkt.header.codec_id != current_codec {
|
if pkt.header.codec_id != current_codec {
|
||||||
let new_profile = match pkt.header.codec_id {
|
let new_profile = match pkt.header.codec_id {
|
||||||
@@ -373,6 +388,8 @@ impl CallEngine {
|
|||||||
transport,
|
transport,
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
fingerprint,
|
fingerprint,
|
||||||
|
tx_codec,
|
||||||
|
rx_codec,
|
||||||
_audio_handle: SyncWrapper(audio_handle),
|
_audio_handle: SyncWrapper(audio_handle),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -410,6 +427,8 @@ impl CallEngine {
|
|||||||
audio_level: self.audio_level.load(Ordering::Relaxed),
|
audio_level: self.audio_level.load(Ordering::Relaxed),
|
||||||
call_duration_secs: self.start_time.elapsed().as_secs_f64(),
|
call_duration_secs: self.start_time.elapsed().as_secs_f64(),
|
||||||
fingerprint: self.fingerprint.clone(),
|
fingerprint: self.fingerprint.clone(),
|
||||||
|
tx_codec: self.tx_codec.lock().await.clone(),
|
||||||
|
rx_codec: self.rx_codec.lock().await.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ struct CallStatus {
|
|||||||
audio_level: u32,
|
audio_level: u32,
|
||||||
call_duration_secs: f64,
|
call_duration_secs: f64,
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
|
tx_codec: String,
|
||||||
|
rx_codec: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
@@ -204,6 +206,8 @@ async fn get_status(state: tauri::State<'_, Arc<AppState>>) -> Result<CallStatus
|
|||||||
audio_level: status.audio_level,
|
audio_level: status.audio_level,
|
||||||
call_duration_secs: status.call_duration_secs,
|
call_duration_secs: status.call_duration_secs,
|
||||||
fingerprint: status.fingerprint,
|
fingerprint: status.fingerprint,
|
||||||
|
tx_codec: status.tx_codec,
|
||||||
|
rx_codec: status.rx_codec,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(CallStatus {
|
Ok(CallStatus {
|
||||||
@@ -216,6 +220,8 @@ async fn get_status(state: tauri::State<'_, Arc<AppState>>) -> Result<CallStatus
|
|||||||
audio_level: 0,
|
audio_level: 0,
|
||||||
call_duration_secs: 0.0,
|
call_duration_secs: 0.0,
|
||||||
fingerprint: String::new(),
|
fingerprint: String::new(),
|
||||||
|
tx_codec: String::new(),
|
||||||
|
rx_codec: String::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -586,7 +586,10 @@ async function pollStatus() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
statsDiv.textContent = `TX: ${st.encode_fps} | RX: ${st.recv_fps}`;
|
// Stats line with codec badges
|
||||||
|
const txBadge = (st as any).tx_codec ? `<span class="codec-badge tx">${escapeHtml((st as any).tx_codec)}</span>` : "";
|
||||||
|
const rxBadge = (st as any).rx_codec ? `<span class="codec-badge rx">${escapeHtml((st as any).rx_codec)}</span>` : "";
|
||||||
|
statsDiv.innerHTML = `${txBadge} ${rxBadge} TX: ${st.encode_fps} | RX: ${st.recv_fps}`;
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -470,6 +470,27 @@ button.primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|||||||
.relay-dot-small.green { background: var(--green); }
|
.relay-dot-small.green { background: var(--green); }
|
||||||
.relay-dot-small.blue { background: #60a5fa; }
|
.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 ── */
|
||||||
.controls {
|
.controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user