Skip to main content

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}