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