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