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