active_call/
config.rs

1use crate::useragent::RegisterOption;
2use anyhow::{Error, Result};
3use clap::Parser;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use voice_engine::{IceServer, media::recorder::RecorderFormat};
7
8#[derive(Parser, Debug)]
9#[command(version)]
10pub struct Cli {
11    #[clap(long)]
12    pub conf: Option<String>,
13
14    #[clap(long)]
15    pub http: Option<String>,
16
17    #[clap(long)]
18    pub sip: Option<String>,
19}
20
21pub(crate) fn default_config_recorder_path() -> String {
22    #[cfg(target_os = "windows")]
23    return "./config/recorders".to_string();
24    #[cfg(not(target_os = "windows"))]
25    return "./config/recorders".to_string();
26}
27
28fn default_config_media_cache_path() -> String {
29    #[cfg(target_os = "windows")]
30    return "./config/mediacache".to_string();
31    #[cfg(not(target_os = "windows"))]
32    return "./config/mediacache".to_string();
33}
34
35fn default_config_http_addr() -> String {
36    "0.0.0.0:8080".to_string()
37}
38
39fn default_sip_addr() -> String {
40    "0.0.0.0".to_string()
41}
42
43fn default_sip_port() -> u16 {
44    25060
45}
46
47fn default_config_rtp_start_port() -> Option<u16> {
48    Some(12000)
49}
50
51fn default_config_rtp_end_port() -> Option<u16> {
52    Some(42000)
53}
54
55#[derive(Debug, Clone, Deserialize, Serialize, Default)]
56#[serde(rename_all = "snake_case")]
57pub struct RecordingPolicy {
58    #[serde(default)]
59    pub enabled: bool,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub auto_start: Option<bool>,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub filename_pattern: Option<String>,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub samplerate: Option<u32>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub ptime: Option<u32>,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub path: Option<String>,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub format: Option<RecorderFormat>,
72}
73
74impl RecordingPolicy {
75    pub fn recorder_path(&self) -> String {
76        self.path
77            .as_ref()
78            .map(|p| p.trim())
79            .filter(|p| !p.is_empty())
80            .map(|p| p.to_string())
81            .unwrap_or_else(default_config_recorder_path)
82    }
83
84    pub fn recorder_format(&self) -> RecorderFormat {
85        self.format.unwrap_or_default()
86    }
87
88    pub fn ensure_defaults(&mut self) -> bool {
89        if self
90            .path
91            .as_ref()
92            .map(|p| p.trim().is_empty())
93            .unwrap_or(true)
94        {
95            self.path = Some(default_config_recorder_path());
96        }
97
98        let original = self.format.unwrap_or_default();
99        let effective = original.effective();
100        self.format = Some(effective);
101        original != effective
102    }
103}
104
105#[derive(Debug, Deserialize, Serialize)]
106pub struct Config {
107    #[serde(default = "default_config_http_addr")]
108    pub http_addr: String,
109    pub log_level: Option<String>,
110    pub log_file: Option<String>,
111    #[serde(default, skip_serializing_if = "Vec::is_empty")]
112    pub http_access_skip_paths: Vec<String>,
113
114    #[serde(default = "default_sip_addr")]
115    pub sip_addr: String,
116    #[serde(default = "default_sip_port")]
117    pub sip_port: u16,
118    pub useragent: Option<String>,
119    pub register_users: Option<Vec<RegisterOption>>,
120    pub graceful_shutdown: Option<bool>,
121    pub sip_handler: Option<InviteHandlerConfig>,
122    pub sip_accept_timeout: Option<String>,
123
124    pub external_ip: Option<String>,
125    #[serde(default = "default_config_rtp_start_port")]
126    pub rtp_start_port: Option<u16>,
127    #[serde(default = "default_config_rtp_end_port")]
128    pub rtp_end_port: Option<u16>,
129
130    pub callrecord: Option<CallRecordConfig>,
131    #[serde(default = "default_config_media_cache_path")]
132    pub media_cache_path: String,
133    pub ice_servers: Option<Vec<IceServer>>,
134    #[serde(default)]
135    pub recording: Option<RecordingPolicy>,
136}
137
138#[derive(Debug, Deserialize, Clone, Serialize)]
139#[serde(rename_all = "snake_case")]
140#[serde(tag = "type")]
141pub enum InviteHandlerConfig {
142    Webhook {
143        url: Option<String>,
144        urls: Option<Vec<String>>,
145        method: Option<String>,
146        headers: Option<Vec<(String, String)>>,
147    },
148}
149
150#[derive(Debug, Deserialize, Clone, Serialize)]
151#[serde(rename_all = "snake_case")]
152pub enum S3Vendor {
153    Aliyun,
154    Tencent,
155    Minio,
156    AWS,
157    GCP,
158    Azure,
159    DigitalOcean,
160}
161
162#[derive(Debug, Deserialize, Clone, Serialize)]
163#[serde(tag = "type")]
164#[serde(rename_all = "snake_case")]
165pub enum CallRecordConfig {
166    Local {
167        root: String,
168    },
169    S3 {
170        vendor: S3Vendor,
171        bucket: String,
172        region: String,
173        access_key: String,
174        secret_key: String,
175        endpoint: String,
176        root: String,
177        with_media: Option<bool>,
178        keep_media_copy: Option<bool>,
179    },
180    Http {
181        url: String,
182        headers: Option<HashMap<String, String>>,
183        with_media: Option<bool>,
184        keep_media_copy: Option<bool>,
185    },
186}
187
188impl Default for CallRecordConfig {
189    fn default() -> Self {
190        Self::Local {
191            #[cfg(target_os = "windows")]
192            root: "./config/cdr".to_string(),
193            #[cfg(not(target_os = "windows"))]
194            root: "./config/cdr".to_string(),
195        }
196    }
197}
198
199impl Default for Config {
200    fn default() -> Self {
201        Self {
202            http_addr: default_config_http_addr(),
203            log_level: None,
204            log_file: None,
205            http_access_skip_paths: Vec::new(),
206            sip_addr: default_sip_addr(),
207            sip_port: default_sip_port(),
208            useragent: None,
209            register_users: None,
210            graceful_shutdown: Some(true),
211            sip_handler: None,
212            sip_accept_timeout: Some("50s".to_string()),
213            media_cache_path: default_config_media_cache_path(),
214            callrecord: None,
215            ice_servers: None,
216            external_ip: None,
217            rtp_start_port: default_config_rtp_start_port(),
218            rtp_end_port: default_config_rtp_end_port(),
219            recording: None,
220        }
221    }
222}
223
224impl Clone for Config {
225    fn clone(&self) -> Self {
226        // This is a bit expensive but Config is not cloned often in hot paths
227        // and implementing Clone manually for all nested structs is tedious
228        let s = toml::to_string(self).unwrap();
229        toml::from_str(&s).unwrap()
230    }
231}
232
233impl Config {
234    pub fn load(path: &str) -> Result<Self, Error> {
235        let mut config: Self = toml::from_str(
236            &std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("{}: {}", e, path))?,
237        )?;
238        if config.ensure_recording_defaults() {
239            tracing::warn!(
240                "recorder_format=ogg requires compiling with the 'opus' feature; falling back to wav"
241            );
242        }
243        Ok(config)
244    }
245
246    pub fn recorder_path(&self) -> String {
247        self.recording
248            .as_ref()
249            .map(|policy| policy.recorder_path())
250            .unwrap_or_else(default_config_recorder_path)
251    }
252
253    pub fn recorder_format(&self) -> RecorderFormat {
254        self.recording
255            .as_ref()
256            .map(|policy| policy.recorder_format())
257            .unwrap_or_default()
258    }
259
260    pub fn ensure_recording_defaults(&mut self) -> bool {
261        let mut fallback = false;
262
263        if let Some(policy) = self.recording.as_mut() {
264            fallback |= policy.ensure_defaults();
265        }
266
267        fallback
268    }
269}