active_call/
config.rs

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