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 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}