refactor: separate SignalManager from WzpEngine for direct calling
SignalManager (NEW): - Dedicated Rust struct with its own QUIC connection to _signal - Separate JNI handle (nativeSignalConnect/GetState/PlaceCall/etc) - Kotlin wrapper polls state every 500ms via getState() JSON - Lives independently of WzpEngine — survives across calls - connect() blocks briefly on 8MB thread, then recv loop runs on dedicated thread WzpEngine (CLEANED): - Back to pure media-only role (audio, codec, FEC, jitter) - Removed start_signaling/place_call/answer_call methods - Removed signal_transport/signal_fingerprint from EngineState CallViewModel: - Two separate managers: signalManager (persistent) + engine (per-call) - Two separate polling loops: signalPollJob + statsJob - Auto-connect to media room when signal polling detects "setup" state - hangupDirectCall() ends media but keeps signal alive Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,10 +97,6 @@ pub(crate) struct EngineState {
|
||||
/// QUIC transport handle — stored so stop_call() can close it immediately,
|
||||
/// triggering relay-side leave + RoomUpdate broadcast.
|
||||
pub quic_transport: Mutex<Option<Arc<wzp_transport::QuinnTransport>>>,
|
||||
/// Signal transport for direct calling — stored so place_call/answer_call can send.
|
||||
pub signal_transport: Mutex<Option<Arc<wzp_transport::QuinnTransport>>>,
|
||||
/// Our fingerprint (set during signaling registration).
|
||||
pub signal_fingerprint: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
pub struct WzpEngine {
|
||||
@@ -122,8 +118,6 @@ impl WzpEngine {
|
||||
playout_ring: AudioRing::new(),
|
||||
audio_level_rms: AtomicU32::new(0),
|
||||
quic_transport: Mutex::new(None),
|
||||
signal_transport: Mutex::new(None),
|
||||
signal_fingerprint: Mutex::new(None),
|
||||
});
|
||||
Self {
|
||||
state,
|
||||
@@ -250,203 +244,7 @@ impl WzpEngine {
|
||||
}
|
||||
|
||||
/// Start persistent signaling connection for direct calls.
|
||||
/// Spawns a background task that maintains the `_signal` connection.
|
||||
/// Start persistent signaling for direct calls.
|
||||
/// Blocks the calling thread (Kotlin provides a Thread with 8MB stack).
|
||||
/// Same pattern as start_call: tokio block_on on the caller's thread.
|
||||
pub fn start_signaling(
|
||||
&mut self,
|
||||
relay_addr: &str,
|
||||
seed_hex: &str,
|
||||
token: Option<&str>,
|
||||
alias: Option<&str>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
use wzp_proto::{MediaTransport, SignalMessage};
|
||||
|
||||
|
||||
let addr: SocketAddr = relay_addr.parse()?;
|
||||
let seed = if seed_hex.is_empty() {
|
||||
wzp_crypto::Seed::generate()
|
||||
} else {
|
||||
wzp_crypto::Seed::from_hex(seed_hex).map_err(|e| anyhow::anyhow!(e))?
|
||||
};
|
||||
let identity = seed.derive_identity();
|
||||
let pub_id = identity.public_identity();
|
||||
let identity_pub = *pub_id.signing.as_bytes();
|
||||
let fp = pub_id.fingerprint.to_string();
|
||||
let token = token.map(|s| s.to_string());
|
||||
let alias = alias.map(|s| s.to_string());
|
||||
let state = self.state.clone();
|
||||
|
||||
info!(fingerprint = %fp, relay = %addr, "starting signaling");
|
||||
|
||||
self.state.running.store(true, Ordering::Release);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
let signal_state = state.clone();
|
||||
rt.block_on(async move {
|
||||
let bind: SocketAddr = "0.0.0.0:0".parse().unwrap();
|
||||
let endpoint = match wzp_transport::create_endpoint(bind, None) {
|
||||
Ok(e) => e,
|
||||
Err(e) => { error!("signal endpoint: {e}"); return; }
|
||||
};
|
||||
let client_cfg = wzp_transport::client_config();
|
||||
let conn = match wzp_transport::connect(&endpoint, addr, "_signal", client_cfg).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => { error!("signal connect: {e}"); return; }
|
||||
};
|
||||
let transport = std::sync::Arc::new(wzp_transport::QuinnTransport::new(conn));
|
||||
|
||||
// Auth if token provided
|
||||
if let Some(ref tok) = token {
|
||||
let _ = transport.send_signal(&SignalMessage::AuthToken { token: tok.clone() }).await;
|
||||
}
|
||||
|
||||
// Register presence
|
||||
let _ = transport.send_signal(&SignalMessage::RegisterPresence {
|
||||
identity_pub,
|
||||
signature: vec![],
|
||||
alias: alias.clone(),
|
||||
}).await;
|
||||
|
||||
// Wait for ack
|
||||
match transport.recv_signal().await {
|
||||
Ok(Some(SignalMessage::RegisterPresenceAck { success: true, .. })) => {
|
||||
info!(fingerprint = %fp, "signal: registered");
|
||||
let mut stats = signal_state.stats.lock().unwrap();
|
||||
stats.state = crate::stats::CallState::Registered;
|
||||
drop(stats);
|
||||
// Store transport + fingerprint so place_call/answer_call can use them
|
||||
*signal_state.signal_transport.lock().unwrap() = Some(transport.clone());
|
||||
*signal_state.signal_fingerprint.lock().unwrap() = Some(fp.clone());
|
||||
}
|
||||
other => {
|
||||
error!("signal registration failed: {other:?}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Signal recv loop
|
||||
loop {
|
||||
if !signal_state.running.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
match transport.recv_signal().await {
|
||||
Ok(Some(SignalMessage::CallRinging { call_id })) => {
|
||||
info!(call_id = %call_id, "signal: ringing");
|
||||
let mut stats = signal_state.stats.lock().unwrap();
|
||||
stats.state = crate::stats::CallState::Ringing;
|
||||
}
|
||||
Ok(Some(SignalMessage::DirectCallOffer { caller_fingerprint, caller_alias, call_id, .. })) => {
|
||||
info!(from = %caller_fingerprint, call_id = %call_id, "signal: incoming call");
|
||||
let mut stats = signal_state.stats.lock().unwrap();
|
||||
stats.state = crate::stats::CallState::IncomingCall;
|
||||
stats.incoming_call_id = Some(call_id);
|
||||
stats.incoming_caller_fp = Some(caller_fingerprint);
|
||||
stats.incoming_caller_alias = caller_alias;
|
||||
}
|
||||
Ok(Some(SignalMessage::DirectCallAnswer { call_id, accept_mode, .. })) => {
|
||||
info!(call_id = %call_id, mode = ?accept_mode, "signal: call answered");
|
||||
}
|
||||
Ok(Some(SignalMessage::CallSetup { call_id, room, relay_addr })) => {
|
||||
info!(call_id = %call_id, room = %room, relay = %relay_addr, "signal: call setup");
|
||||
// Connect to media room via the existing start_call mechanism
|
||||
// Store the room info so Kotlin can call startCall with it
|
||||
let mut stats = signal_state.stats.lock().unwrap();
|
||||
stats.state = crate::stats::CallState::Connecting;
|
||||
// Store call setup info for Kotlin to pick up
|
||||
stats.incoming_call_id = Some(format!("{relay_addr}|{room}"));
|
||||
}
|
||||
Ok(Some(SignalMessage::Hangup { reason })) => {
|
||||
info!(reason = ?reason, "signal: call ended by remote");
|
||||
let mut stats = signal_state.stats.lock().unwrap();
|
||||
stats.state = crate::stats::CallState::Closed;
|
||||
stats.incoming_call_id = None;
|
||||
stats.incoming_caller_fp = None;
|
||||
stats.incoming_caller_alias = None;
|
||||
}
|
||||
Ok(Some(_)) => {}
|
||||
Ok(None) => {
|
||||
info!("signal: connection closed");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("signal recv error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut stats = signal_state.stats.lock().unwrap();
|
||||
stats.state = crate::stats::CallState::Closed;
|
||||
}); // block_on
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Place a direct call to a target fingerprint via the signal transport.
|
||||
pub fn place_call(&self, target_fingerprint: &str) -> Result<(), anyhow::Error> {
|
||||
use wzp_proto::SignalMessage;
|
||||
|
||||
let transport = self.state.signal_transport.lock().unwrap().clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("not registered"))?;
|
||||
let caller_fp = self.state.signal_fingerprint.lock().unwrap().clone()
|
||||
.unwrap_or_default();
|
||||
let target = target_fingerprint.to_string();
|
||||
let call_id = format!("{:016x}", std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
|
||||
|
||||
// Send on a separate thread since we can't block the UI thread
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime");
|
||||
rt.block_on(async {
|
||||
let _ = transport.send_signal(&SignalMessage::DirectCallOffer {
|
||||
caller_fingerprint: caller_fp,
|
||||
caller_alias: None,
|
||||
target_fingerprint: target,
|
||||
call_id,
|
||||
identity_pub: [0u8; 32],
|
||||
ephemeral_pub: [0u8; 32],
|
||||
signature: vec![],
|
||||
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
||||
}).await;
|
||||
});
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Answer an incoming direct call via the signal transport.
|
||||
pub fn answer_call(&self, call_id: &str, mode: wzp_proto::CallAcceptMode) -> Result<(), anyhow::Error> {
|
||||
use wzp_proto::SignalMessage;
|
||||
|
||||
let transport = self.state.signal_transport.lock().unwrap().clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("not registered"))?;
|
||||
let call_id = call_id.to_string();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime");
|
||||
rt.block_on(async {
|
||||
let _ = transport.send_signal(&SignalMessage::DirectCallAnswer {
|
||||
call_id,
|
||||
accept_mode: mode,
|
||||
identity_pub: None,
|
||||
ephemeral_pub: None,
|
||||
signature: None,
|
||||
chosen_profile: Some(wzp_proto::QualityProfile::GOOD),
|
||||
}).await;
|
||||
});
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
// Signal methods (start_signaling, place_call, answer_call) moved to signal_mgr.rs
|
||||
|
||||
pub fn set_mute(&self, muted: bool) {
|
||||
self.state.muted.store(muted, Ordering::Relaxed);
|
||||
|
||||
@@ -390,87 +390,132 @@ pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeGetFingerprint
|
||||
|
||||
// ── Direct calling JNI functions ──
|
||||
|
||||
/// Start persistent signaling connection to relay for direct calls.
|
||||
/// Returns 0 immediately — the actual work happens on a dedicated thread.
|
||||
/// The JNI function MUST be minimal because Android's DefaultDispatch thread
|
||||
/// has a tiny stack that overflows with any Rust crypto/network code.
|
||||
// ── SignalManager JNI functions ──
|
||||
|
||||
/// Opaque handle for SignalManager (separate from EngineHandle).
|
||||
struct SignalHandle {
|
||||
mgr: crate::signal_mgr::SignalManager,
|
||||
}
|
||||
|
||||
unsafe fn signal_ref(handle: jlong) -> &'static SignalHandle {
|
||||
unsafe { &*(handle as *const SignalHandle) }
|
||||
}
|
||||
|
||||
/// Connect to relay for signaling. Returns handle (jlong) or 0 on error.
|
||||
/// MUST be called from a thread with sufficient stack (8MB).
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeStartSignaling<'a>(
|
||||
pub unsafe extern "system" fn Java_com_wzp_engine_SignalManager_nativeSignalConnect<'a>(
|
||||
mut env: JNIEnv<'a>,
|
||||
_class: JClass,
|
||||
relay_j: JString,
|
||||
seed_j: JString,
|
||||
) -> jlong {
|
||||
let relay: String = env.get_string(&relay_j).map(|s| s.into()).unwrap_or_default();
|
||||
let seed: String = env.get_string(&seed_j).map(|s| s.into()).unwrap_or_default();
|
||||
|
||||
match crate::signal_mgr::SignalManager::connect(&relay, &seed) {
|
||||
Ok(mgr) => {
|
||||
// Spawn recv loop on a dedicated thread
|
||||
let mgr_ref = &mgr as *const crate::signal_mgr::SignalManager;
|
||||
let handle = Box::new(SignalHandle { mgr });
|
||||
let raw = Box::into_raw(handle);
|
||||
|
||||
// Get a reference for the recv thread
|
||||
let recv_ref = unsafe { &(*raw).mgr };
|
||||
std::thread::Builder::new()
|
||||
.name("wzp-signal-recv".into())
|
||||
.stack_size(4 * 1024 * 1024)
|
||||
.spawn(move || {
|
||||
recv_ref.run_recv_loop();
|
||||
})
|
||||
.ok();
|
||||
|
||||
raw as jlong
|
||||
}
|
||||
Err(e) => {
|
||||
error!("signal connect failed: {e}");
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get signal state as JSON string.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "system" fn Java_com_wzp_engine_SignalManager_nativeSignalGetState<'a>(
|
||||
mut env: JNIEnv<'a>,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
relay_addr_j: JString,
|
||||
seed_hex_j: JString,
|
||||
token_j: JString,
|
||||
alias_j: JString,
|
||||
) -> jstring {
|
||||
if handle == 0 { return JObject::null().into_raw(); }
|
||||
let h = signal_ref(handle);
|
||||
let json = h.mgr.get_state_json();
|
||||
env.new_string(&json)
|
||||
.map(|s| s.into_raw())
|
||||
.unwrap_or(JObject::null().into_raw())
|
||||
}
|
||||
|
||||
/// Place a direct call.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "system" fn Java_com_wzp_engine_SignalManager_nativeSignalPlaceCall<'a>(
|
||||
mut env: JNIEnv<'a>,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
target_j: JString,
|
||||
) -> jint {
|
||||
// Extract JNI strings — this is all we do on the caller's thread
|
||||
let relay_addr: String = env.get_string(&relay_addr_j).map(|s| s.into()).unwrap_or_default();
|
||||
let seed_hex: String = env.get_string(&seed_hex_j).map(|s| s.into()).unwrap_or_default();
|
||||
let token: String = env.get_string(&token_j).map(|s| s.into()).unwrap_or_default();
|
||||
let alias: String = env.get_string(&alias_j).map(|s| s.into()).unwrap_or_default();
|
||||
|
||||
// Use the existing start_call pattern — create engine, call start_signaling
|
||||
// which spawns a thread internally. This is the same pattern that works for room calls.
|
||||
let h = unsafe { handle_ref(handle) };
|
||||
|
||||
match h.engine.start_signaling(
|
||||
&relay_addr,
|
||||
&seed_hex,
|
||||
if token.is_empty() { None } else { Some(&token) },
|
||||
if alias.is_empty() { None } else { Some(&alias) },
|
||||
) {
|
||||
if handle == 0 { return -1; }
|
||||
let h = signal_ref(handle);
|
||||
let target: String = env.get_string(&target_j).map(|s| s.into()).unwrap_or_default();
|
||||
match h.mgr.place_call(&target) {
|
||||
Ok(()) => 0,
|
||||
Err(e) => { error!("start_signaling failed: {e}"); -1 }
|
||||
Err(e) => { error!("place_call: {e}"); -1 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Place a direct call to a target fingerprint.
|
||||
/// Returns 0 on success, -1 on error.
|
||||
/// Answer an incoming call.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativePlaceCall<'a>(
|
||||
mut env: JNIEnv<'a>,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
target_fp_j: JString,
|
||||
) -> jint {
|
||||
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
|
||||
let h = unsafe { handle_ref(handle) };
|
||||
let target: String = env.get_string(&target_fp_j).map(|s| s.into()).unwrap_or_default();
|
||||
h.engine.place_call(&target)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => 0,
|
||||
Ok(Err(e)) => { error!("place_call failed: {e}"); -1 }
|
||||
Err(_) => { error!("place_call panicked"); -1 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Answer an incoming direct call.
|
||||
/// mode: 0=Reject, 1=AcceptTrusted, 2=AcceptGeneric
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "system" fn Java_com_wzp_engine_WzpEngine_nativeAnswerCall<'a>(
|
||||
pub unsafe extern "system" fn Java_com_wzp_engine_SignalManager_nativeSignalAnswerCall<'a>(
|
||||
mut env: JNIEnv<'a>,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
call_id_j: JString,
|
||||
mode: jint,
|
||||
) -> jint {
|
||||
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
|
||||
let h = unsafe { handle_ref(handle) };
|
||||
let call_id: String = env.get_string(&call_id_j).map(|s| s.into()).unwrap_or_default();
|
||||
let accept_mode = match mode {
|
||||
0 => wzp_proto::CallAcceptMode::Reject,
|
||||
1 => wzp_proto::CallAcceptMode::AcceptTrusted,
|
||||
_ => wzp_proto::CallAcceptMode::AcceptGeneric,
|
||||
};
|
||||
h.engine.answer_call(&call_id, accept_mode)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => 0,
|
||||
Ok(Err(e)) => { error!("answer_call failed: {e}"); -1 }
|
||||
Err(_) => { error!("answer_call panicked"); -1 }
|
||||
if handle == 0 { return -1; }
|
||||
let h = signal_ref(handle);
|
||||
let call_id: String = env.get_string(&call_id_j).map(|s| s.into()).unwrap_or_default();
|
||||
let accept_mode = match mode {
|
||||
0 => wzp_proto::CallAcceptMode::Reject,
|
||||
1 => wzp_proto::CallAcceptMode::AcceptTrusted,
|
||||
_ => wzp_proto::CallAcceptMode::AcceptGeneric,
|
||||
};
|
||||
match h.mgr.answer_call(&call_id, accept_mode) {
|
||||
Ok(()) => 0,
|
||||
Err(e) => { error!("answer_call: {e}"); -1 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Send hangup signal.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "system" fn Java_com_wzp_engine_SignalManager_nativeSignalHangup(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
) {
|
||||
if handle == 0 { return; }
|
||||
let h = signal_ref(handle);
|
||||
h.mgr.hangup();
|
||||
}
|
||||
|
||||
/// Destroy the signal manager and free resources.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "system" fn Java_com_wzp_engine_SignalManager_nativeSignalDestroy(
|
||||
_env: JNIEnv,
|
||||
_class: JClass,
|
||||
handle: jlong,
|
||||
) {
|
||||
if handle == 0 { return; }
|
||||
let h = signal_ref(handle);
|
||||
h.mgr.stop();
|
||||
// Reclaim the Box
|
||||
let _ = unsafe { Box::from_raw(handle as *mut SignalHandle) };
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@ pub mod audio_ring;
|
||||
pub mod commands;
|
||||
pub mod engine;
|
||||
pub mod pipeline;
|
||||
pub mod signal_mgr;
|
||||
pub mod stats;
|
||||
pub mod jni_bridge;
|
||||
|
||||
265
crates/wzp-android/src/signal_mgr.rs
Normal file
265
crates/wzp-android/src/signal_mgr.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
//! Persistent signal connection manager for direct 1:1 calls.
|
||||
//!
|
||||
//! Separate from the media engine — survives across calls.
|
||||
//! Connects to relay via `_signal` SNI, registers presence,
|
||||
//! and handles call signaling (offer/answer/setup/hangup).
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use tracing::{error, info, warn};
|
||||
use wzp_proto::{MediaTransport, SignalMessage};
|
||||
|
||||
/// Signal connection status.
|
||||
#[derive(Clone, Debug, Default, serde::Serialize)]
|
||||
pub struct SignalState {
|
||||
pub status: String, // "idle", "registered", "ringing", "incoming", "setup"
|
||||
pub fingerprint: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub incoming_call_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub incoming_caller_fp: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub incoming_caller_alias: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub call_setup_relay: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub call_setup_room: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub call_setup_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Manages a persistent `_signal` QUIC connection to a relay.
|
||||
pub struct SignalManager {
|
||||
transport: Arc<wzp_transport::QuinnTransport>,
|
||||
state: Arc<Mutex<SignalState>>,
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl SignalManager {
|
||||
/// Connect to relay, register presence, return immediately.
|
||||
/// Call `run_recv_loop()` on a separate thread after this.
|
||||
pub fn connect(relay_addr: &str, seed_hex: &str) -> Result<Self, anyhow::Error> {
|
||||
let addr: SocketAddr = relay_addr.parse()?;
|
||||
let seed = if seed_hex.is_empty() {
|
||||
wzp_crypto::Seed::generate()
|
||||
} else {
|
||||
wzp_crypto::Seed::from_hex(seed_hex).map_err(|e| anyhow::anyhow!(e))?
|
||||
};
|
||||
let identity = seed.derive_identity();
|
||||
let pub_id = identity.public_identity();
|
||||
let identity_pub = *pub_id.signing.as_bytes();
|
||||
let fp = pub_id.fingerprint.to_string();
|
||||
|
||||
info!(fingerprint = %fp, relay = %addr, "signal: connecting");
|
||||
|
||||
// Synchronous QUIC connect + register (runs on caller's thread)
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
let transport = rt.block_on(async {
|
||||
let bind: SocketAddr = "0.0.0.0:0".parse().unwrap();
|
||||
let endpoint = wzp_transport::create_endpoint(bind, None)?;
|
||||
let client_cfg = wzp_transport::client_config();
|
||||
let conn = wzp_transport::connect(&endpoint, addr, "_signal", client_cfg).await?;
|
||||
let transport = Arc::new(wzp_transport::QuinnTransport::new(conn));
|
||||
|
||||
// Register presence
|
||||
transport.send_signal(&SignalMessage::RegisterPresence {
|
||||
identity_pub,
|
||||
signature: vec![],
|
||||
alias: None,
|
||||
}).await?;
|
||||
|
||||
match transport.recv_signal().await? {
|
||||
Some(SignalMessage::RegisterPresenceAck { success: true, .. }) => {
|
||||
info!(fingerprint = %fp, "signal: registered");
|
||||
}
|
||||
other => {
|
||||
return Err(anyhow::anyhow!("registration failed: {other:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<_, anyhow::Error>(transport)
|
||||
})?;
|
||||
|
||||
// Don't drop the runtime — we need it for the recv loop
|
||||
// Store it... actually, we'll create a new one in run_recv_loop
|
||||
drop(rt);
|
||||
|
||||
let state = Arc::new(Mutex::new(SignalState {
|
||||
status: "registered".into(),
|
||||
fingerprint: fp,
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
Ok(Self {
|
||||
transport,
|
||||
state,
|
||||
running: Arc::new(AtomicBool::new(true)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Blocking signal recv loop. Run on a dedicated thread.
|
||||
pub fn run_recv_loop(&self) {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio runtime for signal recv");
|
||||
|
||||
let transport = self.transport.clone();
|
||||
let state = self.state.clone();
|
||||
let running = self.running.clone();
|
||||
|
||||
rt.block_on(async move {
|
||||
loop {
|
||||
if !running.load(Ordering::Relaxed) { break; }
|
||||
|
||||
match transport.recv_signal().await {
|
||||
Ok(Some(SignalMessage::CallRinging { call_id })) => {
|
||||
info!(call_id = %call_id, "signal: ringing");
|
||||
let mut s = state.lock().unwrap();
|
||||
s.status = "ringing".into();
|
||||
}
|
||||
Ok(Some(SignalMessage::DirectCallOffer { caller_fingerprint, caller_alias, call_id, .. })) => {
|
||||
info!(from = %caller_fingerprint, call_id = %call_id, "signal: incoming call");
|
||||
let mut s = state.lock().unwrap();
|
||||
s.status = "incoming".into();
|
||||
s.incoming_call_id = Some(call_id);
|
||||
s.incoming_caller_fp = Some(caller_fingerprint);
|
||||
s.incoming_caller_alias = caller_alias;
|
||||
}
|
||||
Ok(Some(SignalMessage::DirectCallAnswer { call_id, accept_mode, .. })) => {
|
||||
info!(call_id = %call_id, mode = ?accept_mode, "signal: call answered");
|
||||
}
|
||||
Ok(Some(SignalMessage::CallSetup { call_id, room, relay_addr })) => {
|
||||
info!(call_id = %call_id, room = %room, relay = %relay_addr, "signal: call setup");
|
||||
let mut s = state.lock().unwrap();
|
||||
s.status = "setup".into();
|
||||
s.call_setup_relay = Some(relay_addr);
|
||||
s.call_setup_room = Some(room);
|
||||
s.call_setup_id = Some(call_id);
|
||||
}
|
||||
Ok(Some(SignalMessage::Hangup { reason })) => {
|
||||
info!(reason = ?reason, "signal: hangup");
|
||||
let mut s = state.lock().unwrap();
|
||||
s.status = "registered".into();
|
||||
s.incoming_call_id = None;
|
||||
s.incoming_caller_fp = None;
|
||||
s.incoming_caller_alias = None;
|
||||
s.call_setup_relay = None;
|
||||
s.call_setup_room = None;
|
||||
s.call_setup_id = None;
|
||||
}
|
||||
Ok(Some(_)) => {}
|
||||
Ok(None) => {
|
||||
info!("signal: connection closed");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("signal recv error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut s = state.lock().unwrap();
|
||||
s.status = "idle".into();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get current state (non-blocking).
|
||||
pub fn get_state(&self) -> SignalState {
|
||||
self.state.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Get state as JSON string.
|
||||
pub fn get_state_json(&self) -> String {
|
||||
serde_json::to_string(&self.get_state()).unwrap_or_else(|_| "{}".into())
|
||||
}
|
||||
|
||||
/// Place a direct call.
|
||||
pub fn place_call(&self, target_fp: &str) -> Result<(), anyhow::Error> {
|
||||
let fp = self.state.lock().unwrap().fingerprint.clone();
|
||||
let target = target_fp.to_string();
|
||||
let call_id = format!("{:016x}", std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
|
||||
let transport = self.transport.clone();
|
||||
|
||||
// Send on a small thread (async send needs a runtime)
|
||||
std::thread::Builder::new()
|
||||
.name("wzp-call-send".into())
|
||||
.spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all().build().expect("rt");
|
||||
rt.block_on(async {
|
||||
let _ = transport.send_signal(&SignalMessage::DirectCallOffer {
|
||||
caller_fingerprint: fp,
|
||||
caller_alias: None,
|
||||
target_fingerprint: target,
|
||||
call_id,
|
||||
identity_pub: [0u8; 32],
|
||||
ephemeral_pub: [0u8; 32],
|
||||
signature: vec![],
|
||||
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
||||
}).await;
|
||||
});
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Answer an incoming call.
|
||||
pub fn answer_call(&self, call_id: &str, mode: wzp_proto::CallAcceptMode) -> Result<(), anyhow::Error> {
|
||||
let call_id = call_id.to_string();
|
||||
let transport = self.transport.clone();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("wzp-answer-send".into())
|
||||
.spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all().build().expect("rt");
|
||||
rt.block_on(async {
|
||||
let _ = transport.send_signal(&SignalMessage::DirectCallAnswer {
|
||||
call_id,
|
||||
accept_mode: mode,
|
||||
identity_pub: None,
|
||||
ephemeral_pub: None,
|
||||
signature: None,
|
||||
chosen_profile: Some(wzp_proto::QualityProfile::GOOD),
|
||||
}).await;
|
||||
});
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send hangup.
|
||||
pub fn hangup(&self) {
|
||||
let transport = self.transport.clone();
|
||||
let state = self.state.clone();
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all().build().expect("rt");
|
||||
rt.block_on(async {
|
||||
let _ = transport.send_signal(&SignalMessage::Hangup {
|
||||
reason: wzp_proto::HangupReason::Normal,
|
||||
}).await;
|
||||
});
|
||||
let mut s = state.lock().unwrap();
|
||||
s.status = "registered".into();
|
||||
s.incoming_call_id = None;
|
||||
s.incoming_caller_fp = None;
|
||||
s.incoming_caller_alias = None;
|
||||
s.call_setup_relay = None;
|
||||
s.call_setup_room = None;
|
||||
s.call_setup_id = None;
|
||||
});
|
||||
}
|
||||
|
||||
/// Stop the signal connection.
|
||||
pub fn stop(&self) {
|
||||
self.running.store(false, Ordering::Release);
|
||||
self.transport.connection().close(0u32.into(), b"shutdown");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user