Skip to main content

active_call/
lib.rs

1use anyhow::Result;
2use rsipstack::dialog::{authenticate::Credential, invitation::InviteOption};
3use serde::{Deserialize, Serialize};
4use serde_with::skip_serializing_none;
5use std::collections::HashMap;
6
7use crate::{
8    media::{
9        agc::AGCOption,
10        ambiance::AmbianceOption,
11        recorder::RecorderOption,
12        track::media_pass::MediaPassOption,
13        vad::VADOption,
14    },
15    synthesis::SynthesisOption,
16    transcription::TranscriptionOption,
17};
18
19pub mod app;
20pub mod call;
21pub mod callrecord;
22pub mod config;
23pub mod event;
24pub mod handler;
25pub mod locator;
26pub mod media;
27pub mod net_tool;
28pub mod main_builder;
29
30#[cfg(feature = "offline")]
31pub mod offline;
32
33pub mod playbook;
34pub mod synthesis;
35pub mod transcription;
36pub mod useragent;
37
38#[derive(Debug, Deserialize, Serialize, Default, Clone)]
39#[serde(default)]
40pub struct SipOption {
41    pub username: Option<String>,
42    pub password: Option<String>,
43    pub realm: Option<String>,
44    pub contact: Option<String>,
45    pub headers: Option<HashMap<String, String>>,
46    pub hangup_headers: Option<HashMap<String, String>>,
47    pub extract_headers: Option<Vec<String>>,
48    pub enable_srtp: Option<bool>,
49}
50
51#[skip_serializing_none]
52#[derive(Debug, Deserialize, Serialize, Clone)]
53#[serde(rename_all = "camelCase")]
54pub struct CallOption {
55    pub denoise: Option<bool>,
56    pub agc: Option<AGCOption>,
57    pub offer: Option<String>,
58    pub callee: Option<String>,
59    pub caller: Option<String>,
60    pub recorder: Option<RecorderOption>,
61    pub vad: Option<VADOption>,
62    pub asr: Option<TranscriptionOption>,
63    pub tts: Option<SynthesisOption>,
64    pub media_pass: Option<MediaPassOption>,
65    // handshake timeout in seconds
66    pub handshake_timeout: Option<u64>,
67    pub enable_ipv6: Option<bool>,
68    pub inactivity_timeout: Option<u64>, // inactivity timeout in seconds
69    pub sip: Option<SipOption>,
70    pub extra: Option<HashMap<String, String>>,
71    pub codec: Option<String>, // pcmu, pcma, g722, pcm, only for websocket call
72    pub ambiance: Option<AmbianceOption>,
73    pub eou: Option<EouOption>,
74    pub realtime: Option<RealtimeOption>,
75    pub subscribe: Option<bool>,
76    pub enable_ice_lite: Option<bool>,
77    pub ringback_detection: Option<RingbackDetectionOption>,
78}
79
80impl Default for CallOption {
81    fn default() -> Self {
82        Self {
83            denoise: None,
84            agc: None,
85            offer: None,
86            callee: None,
87            caller: None,
88            recorder: None,
89            asr: None,
90            vad: None,
91            tts: None,
92            media_pass: None,
93            handshake_timeout: None,
94            inactivity_timeout: Some(50), // default 50 seconds
95            enable_ipv6: None,
96            sip: None,
97            extra: None,
98            codec: None,
99            ambiance: None,
100            eou: None,
101            realtime: None,
102            subscribe: None,
103            enable_ice_lite: None,
104            ringback_detection: None,
105        }
106    }
107}
108
109impl CallOption {
110    pub fn check_default(&mut self) {
111        if let Some(tts) = &mut self.tts {
112            tts.check_default();
113        }
114        if let Some(asr) = &mut self.asr {
115            asr.check_default();
116        }
117        if let Some(realtime) = &mut self.realtime {
118            realtime.check_default();
119        }
120    }
121
122    pub fn build_invite_option(&self) -> Result<InviteOption> {
123        let mut invite_option = InviteOption::default();
124        if let Some(offer) = &self.offer {
125            invite_option.offer = Some(offer.clone().into());
126        }
127        if let Some(callee) = &self.callee {
128            invite_option.callee = callee.clone().try_into()?;
129        }
130        let caller_uri = if let Some(caller) = &self.caller {
131            // Ensure caller URI has proper sip: scheme
132            if caller.starts_with("sip:") || caller.starts_with("sips:") {
133                caller.clone()
134            } else {
135                format!("sip:{}", caller)
136            }
137        } else if let Some(username) = self.sip.as_ref().and_then(|sip| sip.username.as_ref()) {
138            // If caller is not specified but we have SIP credentials, use username as caller
139            // If realm is available, use it, otherwise use local IP
140            let domain = self
141                .sip
142                .as_ref()
143                .and_then(|sip| sip.realm.as_ref())
144                .map(|s| s.as_str())
145                .unwrap_or("127.0.0.1");
146            format!("sip:{}@{}", username, domain)
147        } else {
148            // Default to a valid SIP URI if nothing is specified
149            "sip:active-call@127.0.0.1".to_string()
150        };
151        invite_option.caller = caller_uri.try_into()?;
152
153        if let Some(sip) = &self.sip {
154            invite_option.credential = Some(Credential {
155                username: sip.username.clone().unwrap_or_default(),
156                password: sip.password.clone().unwrap_or_default(),
157                realm: sip.realm.clone(),
158            });
159            invite_option.headers = sip.headers.as_ref().map(|h| {
160                h.iter()
161                    .map(|(k, v)| rsipstack::rsip::Header::Other(k.clone(), v.clone()))
162                    .collect::<Vec<_>>()
163            });
164            sip.contact.as_ref().map(|c| match c.clone().try_into() {
165                Ok(u) => {
166                    invite_option.contact = u;
167                }
168                Err(_) => {}
169            });
170        }
171        Ok(invite_option)
172    }
173}
174
175#[skip_serializing_none]
176#[derive(Debug, Deserialize, Serialize, Clone)]
177#[serde(rename_all = "camelCase")]
178pub struct ReferOption {
179    pub denoise: Option<bool>,
180    pub agc: Option<AGCOption>,
181    pub timeout: Option<u32>,
182    pub moh: Option<String>,
183    pub vad: Option<VADOption>,
184    pub asr: Option<TranscriptionOption>,
185    /// hangup after the call is ended
186    pub auto_hangup: Option<bool>,
187    pub sip: Option<SipOption>,
188    pub call_id: Option<String>,
189    /// Pause parent call's ASR during refer call, will resume after refer ends (if auto_hangup is false)
190    pub pause_parent_asr: Option<bool>,
191    /// If false, DTMF RTP packets are not forwarded between the main call and the refer call
192    pub forward_dtmf: Option<bool>,
193}
194
195#[skip_serializing_none]
196#[derive(Clone, Debug, Deserialize, Serialize, Default)]
197#[serde(rename_all = "camelCase")]
198pub struct EouOption {
199    pub r#type: Option<String>,
200    pub endpoint: Option<String>,
201    #[serde(alias = "apiKey")]
202    pub secret_key: Option<String>,
203    pub secret_id: Option<String>,
204    /// max timeout in milliseconds
205    pub timeout: Option<u32>,
206    pub extra: Option<HashMap<String, String>>,
207}
208
209#[skip_serializing_none]
210#[derive(Clone, Debug, Deserialize, Serialize, Default)]
211#[serde(rename_all = "camelCase")]
212pub struct RingbackDetectionOption {
213    pub enabled: Option<bool>,
214    /// Path to telcoclassifier_weights.bin (default: "./telcoclassifier_weights.bin")
215    pub model_weights_path: Option<String>,
216    /// Minimum audio accumulation (seconds) before first inference
217    pub min_buffer_secs: Option<f32>,
218    /// Seconds between consecutive inferences
219    pub detection_interval_secs: Option<f32>,
220    /// Confidence threshold for reporting a state
221    pub confidence_threshold: Option<f32>,
222    /// Only emit events on state change (ringing→human_voice etc.)
223    pub on_state_change_only: Option<bool>,
224    /// Sliding window size for result accumulation (default: 6, i.e. 4+2)
225    pub sliding_window_size: Option<usize>,
226    /// Confidence threshold for immediate finalization (default: 0.9)
227    pub final_confidence_threshold: Option<f32>,
228}
229
230#[derive(Debug, Clone, Serialize, Hash, Eq, PartialEq)]
231pub enum RealtimeType {
232    #[serde(rename = "openai")]
233    OpenAI,
234    #[serde(rename = "azure")]
235    Azure,
236    Other(String),
237}
238
239impl<'de> Deserialize<'de> for RealtimeType {
240    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
241    where
242        D: serde::Deserializer<'de>,
243    {
244        let value = String::deserialize(deserializer)?;
245        match value.as_str() {
246            "openai" => Ok(RealtimeType::OpenAI),
247            "azure" => Ok(RealtimeType::Azure),
248            _ => Ok(RealtimeType::Other(value)),
249        }
250    }
251}
252
253#[skip_serializing_none]
254#[derive(Clone, Debug, Deserialize, Serialize, Default)]
255#[serde(rename_all = "camelCase")]
256pub struct RealtimeOption {
257    pub provider: Option<RealtimeType>,
258    pub model: Option<String>,
259    #[serde(alias = "apiKey")]
260    pub secret_key: Option<String>,
261    pub secret_id: Option<String>,
262    pub endpoint: Option<String>,
263    pub turn_detection: Option<serde_json::Value>,
264    pub tools: Option<Vec<serde_json::Value>>,
265    pub extra: Option<HashMap<String, String>>,
266}
267
268impl RealtimeOption {
269    pub fn check_default(&mut self) {
270        if self.secret_key.is_none() {
271            self.secret_key = std::env::var("OPENAI_API_KEY").ok();
272        }
273    }
274}
275
276pub type Spawner = fn(
277    std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
278) -> tokio::task::JoinHandle<()>;
279static EXTERNAL_SPAWNER: std::sync::OnceLock<Spawner> = std::sync::OnceLock::new();
280
281pub fn set_spawner(spawner: Spawner) -> Result<(), Spawner> {
282    EXTERNAL_SPAWNER.set(spawner)
283}
284
285pub fn spawn<F>(future: F) -> tokio::task::JoinHandle<()>
286where
287    F: std::future::Future<Output = ()> + Send + 'static,
288{
289    if let Some(spawner) = EXTERNAL_SPAWNER.get() {
290        spawner(Box::pin(future))
291    } else {
292        tokio::spawn(future)
293    }
294}