ringo_core/baresip/mod.rs
1//! Baresip backend — direct FFI bindings to libbaresip + libre.
2//!
3//! This module provides raw FFI declarations for the C API functions
4//! ringo needs, plus a `BaresipBackend` that implements `Backend` by
5//! calling libbaresip directly (no process spawning, no ctrl_tcp wire protocol).
6//!
7//! Architecture:
8//! - `libre_init()` + `ua_init()` + `conf_configure_buf()` on init
9//! - `re_main()` runs on a dedicated thread (the RE thread)
10//! - Commands run on the RE thread via `re_thread_enter/leave`
11//! - Events arrive via `bevent_register()` callback, translated to `AppEvent`
12//! - All baresip modules (codecs, audio drivers, …) are statically linked
13//! into libbaresip.a and resolved via `lookup_static_module()` — no dlopen
14//! - Inbound INVITE headers are extracted in the `BEVENT_SIPSESS_CONN` handler
15//! (all headers from `msg->hdrl`) and surfaced via the `header_poll` closure
16
17mod ausrc;
18mod bindings;
19mod config;
20mod events;
21mod phone;
22mod re_thread;
23mod siptrace;
24mod sounds;
25mod stats;
26
27use std::collections::HashMap;
28use std::ffi::CString;
29use std::sync::{Mutex, OnceLock};
30
31use anyhow::{Result, bail};
32
33use crate::account::{Account, BackendOptions};
34use crate::backend::{Backend, Session};
35use crate::event::AppEvent;
36
37use self::bindings::*;
38use self::config::{build_config_string, configure_account};
39use self::events::bevent_handler;
40use self::phone::{BaresipPhone, BaresipSessionHandle, make_header_poll};
41use self::re_thread::{EVENT_TX, enter_re_thread, on_re_thread, redirect_logging, start_re_thread};
42
43pub use self::re_thread::stop_re_thread;
44
45/// Trace every SIP request/response to `path` (its own file, separate from the
46/// log). Call before the first session is spawned — the handler is installed
47/// during baresip init.
48pub fn sip_trace_file(path: impl AsRef<std::path::Path>) {
49 siptrace::init_file(path);
50}
51
52/// Trace every SIP request/response to stderr (separate from the log).
53pub fn sip_trace_stderr() {
54 siptrace::init_stderr();
55}
56
57/// Returns true if any UA is still registered. Checks `uag_list()` on the
58/// RE thread without holding the lock. Used by ringo-flow to wait for
59/// `ua_unregister` to complete between scenarios.
60pub fn is_registered() -> bool {
61 use self::bindings::{Ua, ua_isregistered, uag_list};
62 use std::sync::mpsc;
63 let (tx, rx) = mpsc::channel();
64 on_re_thread(move || {
65 let result = unsafe {
66 let list = uag_list();
67 if list.is_null() {
68 false
69 } else {
70 let mut le = (*list).head;
71 let mut found = false;
72 while !le.is_null() {
73 let ua = (*le).data as *const Ua;
74 if !ua.is_null() && ua_isregistered(ua) {
75 found = true;
76 break;
77 }
78 le = (*le).next;
79 }
80 found
81 }
82 };
83 let _ = tx.send(result);
84 });
85 rx.recv().unwrap_or(false)
86}
87
88/// Recently received (decoded) mono audio for the UA with audio key `key`
89/// (the account username), plus its sample rate. Captured in-process by ringo's
90/// own audio player, so ringo-flow can verify a received tone without reading
91/// baresip's sndfile recordings. `None` if no audio has been received yet.
92pub fn received_audio(key: &str) -> Option<(Vec<i16>, u32)> {
93 ausrc::received_window(key)
94}
95
96/// Recently sent (rendered) mono audio for the UA with audio key `key`, plus its
97/// sample rate. Only populated when full capture is enabled (`--save-audio`);
98/// otherwise empty. Used by ringo-flow to save the sent recording.
99pub fn sent_audio(key: &str) -> Option<(Vec<i16>, u32)> {
100 ausrc::sent_window(key)
101}
102
103/// A mono PCM audio frame plus its sample rate — the unit streamed into a call
104/// via [`push_audio`] / out of one via [`subscribe_received_audio`].
105pub use self::ausrc::AudioFrame;
106
107/// Switch the agent's audio source to live-streamed mono s16 PCM at `rate` Hz
108/// (`key` = the audio key / account username). Samples fed via [`push_audio`] are
109/// played in real time; before any arrive (or on underrun) the source is silent.
110/// This is the inbound half for live audio (e.g. TTS into the call).
111pub fn start_audio_stream(key: &str, rate: u32) {
112 ausrc::start_audio_stream(key, rate);
113}
114
115/// Append mono s16 `samples` to the agent's stream-in queue (see
116/// [`start_audio_stream`]). No-op until a stream has been started for `key`.
117pub fn push_audio(key: &str, samples: &[i16]) {
118 ausrc::push_audio(key, samples);
119}
120
121/// Subscribe to the agent's received (decoded) audio as live mono [`AudioFrame`]s
122/// — the outbound half for live audio (e.g. feeding STT). Replaces any previous
123/// subscription for `key`.
124pub fn subscribe_received_audio(key: &str) -> std::sync::mpsc::Receiver<AudioFrame> {
125 ausrc::subscribe_received_audio(key)
126}
127
128/// Returns the total number of active calls across all UAs. Used by
129/// ringo-flow to wait for BYE flush before dropping sessions.
130pub fn call_count() -> u32 {
131 use self::bindings::uag_call_count;
132 use std::sync::mpsc;
133 let (tx, rx) = mpsc::channel();
134 on_re_thread(move || {
135 let count = unsafe { uag_call_count() };
136 let _ = tx.send(count);
137 });
138 rx.recv().unwrap_or(0)
139}
140
141/// The audio codecs this baresip build has registered (name + real srate/ch).
142/// Empty until a session has started the RE thread.
143pub fn available_audio_codecs() -> Vec<crate::event::CodecInfo> {
144 stats::available_audio_codecs()
145}
146
147/// Backend that uses libbaresip directly via FFI (no process spawning,
148/// no ctrl_tcp wire protocol). libre and libbaresip are built from the
149/// vendored submodules and statically linked into the binary.
150pub struct BaresipBackend;
151
152impl Backend for BaresipBackend {
153 fn spawn_session(
154 &self,
155 _rt: &tokio::runtime::Handle,
156 _name: &str,
157 account: &Account,
158 options: &BackendOptions,
159 ) -> Result<Session> {
160 // Ensure EVENT_TX exists
161 let _ = EVENT_TX.set(Mutex::new(HashMap::new()));
162
163 // Redirect libre/baresip debug output to stderr
164 redirect_logging();
165
166 // Start libre + re_main on a dedicated RE thread (once, idempotent)
167 if let Err(e) = start_re_thread() {
168 bail!("{e}");
169 }
170
171 // All baresip API calls must run on the RE thread.
172 let mut ua_ptr: *mut Ua = std::ptr::null_mut();
173 let msg_rx;
174 unsafe {
175 // RAII guard: re_thread_leave() runs on drop, including on panic or
176 // any early `bail!` below — so a failure can't deadlock the RE thread.
177 let _guard = enter_re_thread();
178
179 // Set conf_path to a per-process temp dir so baresip does NOT read
180 // ~/.baresip/accounts or ~/.baresip/config (our config is in-memory
181 // via conf_configure_buf). baresip still requires a valid conf_path,
182 // but the dir stays empty in practice: the only writer would be the
183 // `uuid` module (writes <conf_path>/uuid), and we don't load it.
184 // 0700 keeps it private on a shared host. Removed on shutdown
185 // (stop_re_thread), so a clean exit leaves nothing in /tmp.
186 let dir = format!("/tmp/ringo-baresip-{}", std::process::id());
187 {
188 use std::os::unix::fs::DirBuilderExt;
189 let _ = std::fs::DirBuilder::new()
190 .recursive(true)
191 .mode(0o700)
192 .create(&dir);
193 }
194 let tmp = CString::new(dir).unwrap();
195 let _ = conf_path_set(tmp.as_ptr());
196
197 let config_str = build_config_string(account, options);
198 crate::rlog!(Info, "baresip config:\n{}", config_str);
199 let config_c = match CString::new(config_str) {
200 Ok(s) => s,
201 Err(_) => bail!("generated baresip config contains an interior NUL byte"),
202 };
203 let rc = conf_configure_buf(config_c.as_ptr() as *const u8, config_c.to_bytes().len());
204 if rc != 0 {
205 bail!("conf_configure_buf() failed (rc={rc})");
206 }
207
208 // baresip_init + ua_init: only once (not per-session)
209 static BARESIP_INIT_DONE: OnceLock<bool> = OnceLock::new();
210 if !BARESIP_INIT_DONE.get().copied().unwrap_or(false) {
211 let cfg = conf_config();
212 if cfg.is_null() {
213 bail!("conf_config() returned null");
214 }
215 let rc = baresip_init(cfg);
216 if rc != 0 {
217 bail!("baresip_init() failed (rc={rc})");
218 }
219
220 // SIP User-Agent string — the binary passes its own name+version
221 // (e.g. `ringo-phone/0.11.0`); falls back to this crate's own
222 // `ringo-core/<version>`.
223 const DEFAULT_UA: &str = concat!("ringo-core/", env!("CARGO_PKG_VERSION"));
224 let sw_str = options.user_agent.as_deref().unwrap_or(DEFAULT_UA);
225 let sw = CString::new(sw_str).unwrap_or_else(|_| CString::new(DEFAULT_UA).unwrap());
226 let rc = ua_init(sw.as_ptr(), true, true, true);
227 if rc != 0 {
228 bail!("ua_init() failed (rc={rc})");
229 }
230
231 bevent_register(Some(bevent_handler), std::ptr::null_mut());
232
233 // Register ringo's own audio source + player module (persistent
234 // per-UA source that survives re-INVITEs — see ausrc.rs).
235 if let Err(e) = ausrc::register_module() {
236 bail!("{e}");
237 }
238
239 // Install the SIP trace handler if `--sip-trace` was requested.
240 siptrace::install_if_requested();
241
242 let _ = BARESIP_INIT_DONE.set(true);
243 }
244
245 // Load statically compiled modules (from config "module" lines).
246 // All modules — including the audio driver (pulse/coreaudio) — are
247 // linked into libbaresip.a, so module_load() resolves them via
248 // lookup_static_module() without ever hitting dlopen. The
249 // audio_driver is already set by build_config_string from
250 // RINGO_DEFAULT_AUDIO, so no runtime override is needed.
251 let rc = conf_modules();
252 if rc != 0 {
253 crate::rlog!(Warn, "conf_modules() returned {rc}");
254 }
255
256 // Alloc UA with the account AOR.
257 // In headless mode, route BOTH the source and player through ringo's
258 // own audio module (see ausrc.rs), with a per-UA device key. The
259 // SOURCE is persistent per-UA, survives re-INVITEs, and is race-free
260 // across the parallel UAs in this single process. The PLAYER is
261 // self-clocked so the RX decode/record pipeline always advances —
262 // aubridge's player only clocks when paired with an aubridge source,
263 // which no longer exists. Only when audio_driver is "aubridge"
264 // (headless) — not None (ringo-phone uses real audio like pipewire).
265 let audio_params = if options.audio_driver.as_deref() == Some("aubridge") {
266 // Full per-call capture (sent + received) only when recordings
267 // are wanted (--save-audio); otherwise just the rolling verify
268 // window is retained.
269 ausrc::set_full_capture(options.record_audio);
270 ausrc::init_generator(&account.username);
271 format!(
272 ";audio_source=ringo,{};audio_player=ringo,{}",
273 account.username, account.username
274 )
275 } else {
276 String::new()
277 };
278 // catchall: route incoming INVITEs that match no contact-user to this
279 // UA instead of replying 404 (needed when the provider addresses calls
280 // to identities other than the registration username). It's an account
281 // address parameter (`;catchall`).
282 let catchall_param = if account.catchall {
283 ";catchall=yes"
284 } else {
285 ""
286 };
287 // Restrict/order the offered audio codecs (baresip account param).
288 let codecs_param = if account.audio_codecs.is_empty() {
289 String::new()
290 } else {
291 format!(";audio_codecs={}", account.audio_codecs.join(","))
292 };
293 let aor = match CString::new(format!(
294 "{}<sip:{}@{}>{}{}{}{}",
295 account
296 .display_name
297 .as_deref()
298 .filter(|s| !s.is_empty())
299 .map(|s| format!("{s} "))
300 .unwrap_or_default(),
301 account.username,
302 account.domain,
303 account
304 .transport
305 .as_deref()
306 .filter(|s| !s.is_empty())
307 .map(|s| format!(";transport={s}"))
308 .unwrap_or_default(),
309 audio_params,
310 codecs_param,
311 catchall_param,
312 )) {
313 Ok(s) => s,
314 Err(_) => bail!("account fields contain an interior NUL byte"),
315 };
316
317 let rc = ua_alloc(&mut ua_ptr, aor.as_ptr());
318 if rc != 0 {
319 bail!("ua_alloc() failed (rc={rc})");
320 }
321
322 // Configure the account: auth, outbound, STUN, mediaenc, etc.
323 let acc = ua_account(ua_ptr);
324 if !acc.is_null() {
325 if let Err(e) = configure_account(acc, account) {
326 bail!("configure_account() failed: {e}");
327 }
328 }
329
330 // Register event sender BEFORE ua_register — ua_register fires
331 // RegisterOk synchronously, so the handler needs the sender
332 // in EVENT_TX to route it.
333 let ua_usize = ua_ptr as usize;
334 let (msg_tx, rx) = std::sync::mpsc::channel::<AppEvent>();
335 msg_rx = rx;
336 if let Some(mtx) = EVENT_TX.get() {
337 mtx.lock()
338 .unwrap_or_else(|e| e.into_inner())
339 .insert(ua_usize, msg_tx);
340 }
341
342 // Register
343 let rc = ua_register(ua_ptr);
344 if rc != 0 {
345 crate::rlog!(Warn, "ua_register() failed (rc={rc})");
346 }
347 // _guard drops here → re_thread_leave()
348 }
349
350 let ua_usize = ua_ptr as usize;
351
352 // The registry key for ringo's audio source module (only in aubridge
353 // mode; with real audio the source isn't ours and set_audio_source falls
354 // back to baresip's transient audio_set_source).
355 let audio_key = if options.audio_driver.as_deref() == Some("aubridge") {
356 Some(account.username.clone())
357 } else {
358 None
359 };
360 let phone = Box::new(BaresipPhone::new(ua_usize, audio_key.clone()));
361 let handle = Box::new(BaresipSessionHandle::new(ua_usize, audio_key));
362 let header_poll = Some(make_header_poll(ua_usize));
363
364 Ok(Session::new(msg_rx, phone, header_poll, handle))
365 }
366}