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