use serde::Deserialize; /// LiveKit WebRTC 服务配置。`url` 下发给浏览器,`api_url` 供服务端签发 /// token / 调用 RoomService,两者在同一部署里通常指向同一进程。 #[derive(Clone, Debug, Deserialize)] #[serde(default)] pub struct LiveKitConfig { pub enabled: bool, /// 浏览器侧 WebSocket 地址(ws:// 或 wss://)。 pub url: String, /// 服务端侧 HTTP 地址(http:// 或 https://)。 pub api_url: String, pub api_key: String, pub api_secret: String, pub token_ttl_secs: u64, pub empty_timeout_secs: u32, pub departure_timeout_secs: u32, pub default_max_participants: u32, /// Redis 中参与者在场记录的存活时间;每次 webhook 事件续期。 pub presence_ttl_secs: u64, pub hand_ttl_secs: u64, /// 录制/推流能力开关;为 false 时相关接口直接拒绝。 pub recording_enabled: bool, } impl Default for LiveKitConfig { fn default() -> Self { Self { enabled: false, url: String::new(), api_url: String::new(), api_key: String::new(), api_secret: String::new(), token_ttl_secs: 7200, empty_timeout_secs: 300, departure_timeout_secs: 20, default_max_participants: 50, presence_ttl_secs: 43_200, hand_ttl_secs: 3_600, recording_enabled: false, } } } fn scheme_ok(value: &str, allowed: &[&str]) -> bool { match value.split_once("://") { Some((scheme, rest)) => { !rest.is_empty() && allowed.contains(&scheme.to_ascii_lowercase().as_str()) } None => false, } } impl LiveKitConfig { pub fn is_valid(&self) -> Result<(), String> { if !self.enabled { return Ok(()); } if self.url.trim().is_empty() { return Err("livekit.url must not be empty when livekit is enabled".to_string()); } if self.api_url.trim().is_empty() { return Err("livekit.api_url must not be empty when livekit is enabled".to_string()); } if !scheme_ok(self.url.trim(), &["ws", "wss"]) { return Err("livekit.url must be a ws:// or wss:// URL".to_string()); } if !scheme_ok(self.api_url.trim(), &["http", "https"]) { return Err("livekit.api_url must be an http:// or https:// URL".to_string()); } if self.api_key.trim().is_empty() { return Err("livekit.api_key must not be empty when livekit is enabled".to_string()); } if self.api_secret.trim().is_empty() { return Err("livekit.api_secret must not be empty when livekit is enabled".to_string()); } if self.token_ttl_secs == 0 || self.empty_timeout_secs == 0 || self.departure_timeout_secs == 0 || self.default_max_participants == 0 || self.presence_ttl_secs == 0 || self.hand_ttl_secs == 0 { return Err("livekit numeric settings must be greater than zero".to_string()); } Ok(()) } /// 前端可安全暴露的字段(绝不包含 api_secret)。 pub fn browser_url(&self) -> &str { self.url.trim() } }