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