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 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 agc: Option<AGCOption>,
54 pub offer: Option<String>,
55 pub callee: Option<String>,
56 pub caller: Option<String>,
57 pub recorder: Option<RecorderOption>,
58 pub vad: Option<VADOption>,
59 pub asr: Option<TranscriptionOption>,
60 pub tts: Option<SynthesisOption>,
61 pub media_pass: Option<MediaPassOption>,
62 pub handshake_timeout: Option<u64>,
64 pub enable_ipv6: Option<bool>,
65 pub inactivity_timeout: Option<u64>, pub sip: Option<SipOption>,
67 pub extra: Option<HashMap<String, String>>,
68 pub codec: Option<String>, pub ambiance: Option<AmbianceOption>,
70 pub eou: Option<EouOption>,
71 pub realtime: Option<RealtimeOption>,
72 pub subscribe: Option<bool>,
73 pub enable_ice_lite: Option<bool>,
74 pub ringback_detection: Option<RingbackDetectionOption>,
75}
76
77impl Default for CallOption {
78 fn default() -> Self {
79 Self {
80 denoise: None,
81 agc: None,
82 offer: None,
83 callee: None,
84 caller: None,
85 recorder: None,
86 asr: None,
87 vad: None,
88 tts: None,
89 media_pass: None,
90 handshake_timeout: None,
91 inactivity_timeout: Some(50), enable_ipv6: None,
93 sip: None,
94 extra: None,
95 codec: None,
96 ambiance: None,
97 eou: None,
98 realtime: None,
99 subscribe: None,
100 enable_ice_lite: None,
101 ringback_detection: None,
102 }
103 }
104}
105
106impl CallOption {
107 pub fn check_default(&mut self) {
108 if let Some(tts) = &mut self.tts {
109 tts.check_default();
110 }
111 if let Some(asr) = &mut self.asr {
112 asr.check_default();
113 }
114 if let Some(realtime) = &mut self.realtime {
115 realtime.check_default();
116 }
117 }
118
119 pub fn build_invite_option(&self) -> Result<InviteOption> {
120 let mut invite_option = InviteOption::default();
121 if let Some(offer) = &self.offer {
122 invite_option.offer = Some(offer.clone().into());
123 }
124 if let Some(callee) = &self.callee {
125 invite_option.callee = callee.clone().try_into()?;
126 }
127 let caller_uri = if let Some(caller) = &self.caller {
128 if caller.starts_with("sip:") || caller.starts_with("sips:") {
130 caller.clone()
131 } else {
132 format!("sip:{}", caller)
133 }
134 } else if let Some(username) = self.sip.as_ref().and_then(|sip| sip.username.as_ref()) {
135 let domain = self
138 .sip
139 .as_ref()
140 .and_then(|sip| sip.realm.as_ref())
141 .map(|s| s.as_str())
142 .unwrap_or("127.0.0.1");
143 format!("sip:{}@{}", username, domain)
144 } else {
145 "sip:active-call@127.0.0.1".to_string()
147 };
148 invite_option.caller = caller_uri.try_into()?;
149
150 if let Some(sip) = &self.sip {
151 invite_option.credential = Some(Credential {
152 username: sip.username.clone().unwrap_or_default(),
153 password: sip.password.clone().unwrap_or_default(),
154 realm: sip.realm.clone(),
155 });
156 invite_option.headers = sip.headers.as_ref().map(|h| {
157 h.iter()
158 .map(|(k, v)| rsipstack::rsip::Header::Other(k.clone(), v.clone()))
159 .collect::<Vec<_>>()
160 });
161 sip.contact.as_ref().map(|c| match c.clone().try_into() {
162 Ok(u) => {
163 invite_option.contact = u;
164 }
165 Err(_) => {}
166 });
167 }
168 Ok(invite_option)
169 }
170}
171
172#[skip_serializing_none]
173#[derive(Debug, Deserialize, Serialize, Clone)]
174#[serde(rename_all = "camelCase")]
175pub struct ReferOption {
176 pub denoise: Option<bool>,
177 pub agc: Option<AGCOption>,
178 pub timeout: Option<u32>,
179 pub moh: Option<String>,
180 pub vad: Option<VADOption>,
181 pub asr: Option<TranscriptionOption>,
182 pub auto_hangup: Option<bool>,
184 pub sip: Option<SipOption>,
185 pub call_id: Option<String>,
186 pub pause_parent_asr: Option<bool>,
188 pub forward_dtmf: Option<bool>,
190}
191
192#[skip_serializing_none]
193#[derive(Clone, Debug, Deserialize, Serialize, Default)]
194#[serde(rename_all = "camelCase")]
195pub struct EouOption {
196 pub r#type: Option<String>,
197 pub endpoint: Option<String>,
198 #[serde(alias = "apiKey")]
199 pub secret_key: Option<String>,
200 pub secret_id: Option<String>,
201 pub timeout: Option<u32>,
203 pub extra: Option<HashMap<String, String>>,
204}
205
206#[skip_serializing_none]
207#[derive(Clone, Debug, Deserialize, Serialize, Default)]
208#[serde(rename_all = "camelCase")]
209pub struct RingbackDetectionOption {
210 pub enabled: Option<bool>,
211 pub model_weights_path: Option<String>,
213 pub min_buffer_secs: Option<f32>,
215 pub detection_interval_secs: Option<f32>,
217 pub confidence_threshold: Option<f32>,
219 pub on_state_change_only: Option<bool>,
221 pub sliding_window_size: Option<usize>,
223 pub final_confidence_threshold: Option<f32>,
225}
226
227#[derive(Debug, Clone, Serialize, Hash, Eq, PartialEq)]
228pub enum RealtimeType {
229 #[serde(rename = "openai")]
230 OpenAI,
231 #[serde(rename = "azure")]
232 Azure,
233 Other(String),
234}
235
236impl<'de> Deserialize<'de> for RealtimeType {
237 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
238 where
239 D: serde::Deserializer<'de>,
240 {
241 let value = String::deserialize(deserializer)?;
242 match value.as_str() {
243 "openai" => Ok(RealtimeType::OpenAI),
244 "azure" => Ok(RealtimeType::Azure),
245 _ => Ok(RealtimeType::Other(value)),
246 }
247 }
248}
249
250#[skip_serializing_none]
251#[derive(Clone, Debug, Deserialize, Serialize, Default)]
252#[serde(rename_all = "camelCase")]
253pub struct RealtimeOption {
254 pub provider: Option<RealtimeType>,
255 pub model: Option<String>,
256 #[serde(alias = "apiKey")]
257 pub secret_key: Option<String>,
258 pub secret_id: Option<String>,
259 pub endpoint: Option<String>,
260 pub turn_detection: Option<serde_json::Value>,
261 pub tools: Option<Vec<serde_json::Value>>,
262 pub extra: Option<HashMap<String, String>>,
263}
264
265impl RealtimeOption {
266 pub fn check_default(&mut self) {
267 if self.secret_key.is_none() {
268 self.secret_key = std::env::var("OPENAI_API_KEY").ok();
269 }
270 }
271}
272
273pub type Spawner = fn(
274 std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
275) -> tokio::task::JoinHandle<()>;
276static EXTERNAL_SPAWNER: std::sync::OnceLock<Spawner> = std::sync::OnceLock::new();
277
278pub fn set_spawner(spawner: Spawner) -> Result<(), Spawner> {
279 EXTERNAL_SPAWNER.set(spawner)
280}
281
282pub fn spawn<F>(future: F) -> tokio::task::JoinHandle<()>
283where
284 F: std::future::Future<Output = ()> + Send + 'static,
285{
286 if let Some(spawner) = EXTERNAL_SPAWNER.get() {
287 spawner(Box::pin(future))
288 } else {
289 tokio::spawn(future)
290 }
291}