fix(android): playout Usage::Media + relay CallSetup advertises real IP
Some checks failed
Mirror to GitHub / mirror (push) Failing after 40s
Build Release Binaries / build-amd64 (push) Failing after 3m43s

Three real bugs, one smoke-test session's worth of progress.

1. RELAY: wrong advertised addr in CallSetup
   The direct-call CallSetup computed `relay_addr = addr.ip()` where
   `addr = connection.remote_address()` — i.e. the CLIENT'S IP, not the
   relay's. So the relay was telling both parties "the call room is at
   the answerer's IP:4433", which meant each client dialed either the
   other client (no server listening) or themselves. Both endpoint.connect
   calls hung forever and the call never happened.
   Fix: compute the relay's own advertised IP once at startup. If the
   listen addr is 0.0.0.0, probe the primary outbound interface via the
   classic UDP-bind-and-connect(8.8.8.8:80) trick to discover the LAN
   IP the OS would use to reach external hosts. Thread the resulting
   advertised_addr_str into the CallSetup sender for both parties.

2. RELAY: accept loop serialized QUIC handshakes
   Previously the main accept loop called `wzp_transport::accept` which
   did both `endpoint.accept().await` AND `incoming.await` (the server-
   side QUIC handshake). A single slow handshake therefore blocked every
   subsequent client from being accepted. Unroll the helper here and
   move `incoming.await` into the per-connection spawned task, so every
   handshake runs in parallel. Also log "accept queue: new Incoming",
   "QUIC handshake complete", and "QUIC handshake failed" so we can tell
   immediately whether a client's packets are reaching the relay at all.

3. ANDROID: playout was routed to the silent in-call stream
   The Oboe playout stream was configured with Usage::VoiceCommunication,
   which routes to the Android in-call earpiece stream. That stream is
   silent unless the Activity has called AudioManager.setMode(
   IN_COMMUNICATION) and, even then, only the earpiece/BT headset get
   audio (not the loud speaker). Result: android→mac calls worked
   because mac had a normal media output, but mac→android calls were
   silent even though packets flowed through the relay just fine.
   Switch to Usage::Media + ContentType::Speech so Oboe routes to the
   loud speaker and uses the media volume slider. A later polish step
   will wire setMode + setSpeakerphoneOn from MainActivity.kt so we can
   go back to VoiceCommunication for AEC and proximity-sensor routing.

Plus: heartbeat tracing every 2s in the send/recv tasks — frames_sent,
last_rms, last_pkt_bytes, short_reads on the send side; decoded_frames,
last_decode_n, last_written, decode_errs on the recv side. Will make the
next "no sound" regression trivial to localize.
This commit is contained in:
Siavash Sameni
2026-04-09 20:55:10 +04:00
parent 49f101d785
commit 8c4d640f89
3 changed files with 129 additions and 25 deletions

View File

@@ -224,6 +224,11 @@ impl CallEngine {
encoder.set_aec_enabled(false);
let mut buf = vec![0i16; frame_samples];
let mut heartbeat = std::time::Instant::now();
let mut last_rms: u32 = 0;
let mut last_pkt_bytes: usize = 0;
let mut short_reads: u64 = 0;
loop {
if !send_r.load(Ordering::Relaxed) {
break;
@@ -234,6 +239,7 @@ impl CallEngine {
// so in steady state this spins once per frame.
let read = crate::wzp_native::audio_read_capture(&mut buf);
if read < frame_samples {
short_reads += 1;
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
continue;
}
@@ -242,6 +248,7 @@ impl CallEngine {
let sum_sq: f64 = buf.iter().map(|&s| (s as f64) * (s as f64)).sum();
let rms = (sum_sq / buf.len() as f64).sqrt() as u32;
send_level.store(rms, Ordering::Relaxed);
last_rms = rms;
if send_mic.load(Ordering::Relaxed) {
buf.fill(0);
@@ -249,6 +256,7 @@ impl CallEngine {
match encoder.encode_frame(&buf) {
Ok(pkts) => {
for pkt in &pkts {
last_pkt_bytes = pkt.payload.len();
if let Err(e) = send_t.send_media(pkt).await {
send_drops.fetch_add(1, Ordering::Relaxed);
if send_drops.load(Ordering::Relaxed) <= 3 {
@@ -260,6 +268,21 @@ impl CallEngine {
}
Err(e) => error!("encode: {e}"),
}
// Heartbeat every 2s with capture+encode+send state
if heartbeat.elapsed() >= std::time::Duration::from_secs(2) {
let fs = send_fs.load(Ordering::Relaxed);
let drops = send_drops.load(Ordering::Relaxed);
info!(
frames_sent = fs,
last_rms,
last_pkt_bytes,
short_reads,
send_drops = drops,
"send heartbeat (android)"
);
heartbeat = std::time::Instant::now();
}
}
});
@@ -275,6 +298,15 @@ impl CallEngine {
let mut current_codec = initial_profile.codec;
let mut agc = wzp_codec::AutoGainControl::new();
let mut pcm = vec![0i16; FRAME_SAMPLES_40MS];
info!(codec = ?current_codec, "recv task starting (android/oboe)");
let mut heartbeat = std::time::Instant::now();
let mut decoded_frames: u64 = 0;
let mut written_samples: u64 = 0;
let mut last_decode_n: usize = 0;
let mut last_written: usize = 0;
let mut decode_errs: u64 = 0;
let mut first_packet_logged = false;
loop {
if !recv_r.load(Ordering::Relaxed) {
@@ -287,6 +319,10 @@ impl CallEngine {
.await
{
Ok(Ok(Some(pkt))) => {
if !first_packet_logged {
info!(codec_id = ?pkt.header.codec_id, payload_bytes = pkt.payload.len(), is_repair = pkt.header.is_repair, "recv: first media packet received");
first_packet_logged = true;
}
if !pkt.header.is_repair && pkt.header.codec_id != CodecId::ComfortNoise {
{
let mut rx = recv_rx_codec.lock().await;
@@ -311,10 +347,22 @@ impl CallEngine {
let _ = decoder.set_profile(new_profile);
current_codec = pkt.header.codec_id;
}
if let Ok(n) = decoder.decode(&pkt.payload, &mut pcm) {
agc.process_frame(&mut pcm[..n]);
if !recv_spk.load(Ordering::Relaxed) {
crate::wzp_native::audio_write_playout(&pcm[..n]);
match decoder.decode(&pkt.payload, &mut pcm) {
Ok(n) => {
last_decode_n = n;
decoded_frames += 1;
agc.process_frame(&mut pcm[..n]);
if !recv_spk.load(Ordering::Relaxed) {
let w = crate::wzp_native::audio_write_playout(&pcm[..n]);
last_written = w;
written_samples = written_samples.saturating_add(w as u64);
}
}
Err(e) => {
decode_errs += 1;
if decode_errs <= 3 {
tracing::warn!("decode error: {e}");
}
}
}
}