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