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