veza/veza-stream-server/src/streaming/webrtc/config.rs
2025-12-03 20:36:56 +01:00

286 lines
9.9 KiB
Rust

//! Configuration WebRTC pour le stream server
//!
//! Ce module fournit la configuration WebRTC avec :
//! - Configuration des serveurs ICE (STUN/TURN)
//! - Configuration du signaling
//! - Configuration des codecs audio
//! - Gestion depuis les variables d'environnement
use serde::{Deserialize, Serialize};
use std::time::Duration;
// Note: Use tracing::info! macro directly instead of importing
use super::{AudioCodec, IceServer};
/// Configuration WebRTC pour streaming audio
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebRTCConfig {
pub ice_servers: Vec<IceServer>,
pub signaling_url: String,
pub max_peers: usize,
pub connection_timeout: Duration,
pub heartbeat_interval: Duration,
pub codec_preferences: Vec<AudioCodec>,
pub bitrate_adaptation: bool,
pub jitter_buffer_ms: u32,
}
impl Default for WebRTCConfig {
fn default() -> Self {
Self {
ice_servers: vec![IceServer {
urls: vec!["stun:stun.l.google.com:19302".to_string()],
username: None,
credential: None,
}],
signaling_url: "ws://localhost:3002/ws/webrtc".to_string(),
max_peers: 1000,
connection_timeout: Duration::from_secs(30),
heartbeat_interval: Duration::from_secs(10),
codec_preferences: vec![
AudioCodec::Opus { bitrate: 320 },
AudioCodec::Aac { bitrate: 256 },
AudioCodec::Mp3 { bitrate: 192 },
],
bitrate_adaptation: true,
jitter_buffer_ms: 100,
}
}
}
impl WebRTCConfig {
/// Crée une configuration WebRTC depuis les variables d'environnement
pub fn from_env() -> Self {
let mut config = Self::default();
// Configuration des serveurs ICE
if let Ok(ice_servers) = std::env::var("WEBRTC_ICE_SERVERS") {
config.ice_servers = Self::parse_ice_servers(&ice_servers);
tracing::info!("🔧 WebRTC ICE servers configurés depuis WEBRTC_ICE_SERVERS");
} else {
// Configuration par défaut avec STUN/TURN si disponibles
let mut ice_servers = vec![IceServer {
urls: vec!["stun:stun.l.google.com:19302".to_string()],
username: None,
credential: None,
}];
// Ajouter serveur STUN personnalisé si configuré
if let Ok(stun_url) = std::env::var("WEBRTC_STUN_URL") {
ice_servers.push(IceServer {
urls: vec![stun_url],
username: None,
credential: None,
});
}
// Ajouter serveur TURN si configuré
if let (Ok(turn_url), Ok(turn_username), Ok(turn_credential)) = (
std::env::var("WEBRTC_TURN_URL"),
std::env::var("WEBRTC_TURN_USERNAME"),
std::env::var("WEBRTC_TURN_CREDENTIAL"),
) {
ice_servers.push(IceServer {
urls: vec![turn_url],
username: Some(turn_username),
credential: Some(turn_credential),
});
tracing::info!("✅ Serveur TURN configuré");
} else {
tracing::warn!(
"⚠️ Serveur TURN non configuré - certaines connexions peuvent échouer"
);
}
config.ice_servers = ice_servers;
}
// Configuration du signaling URL
if let Ok(signaling_url) = std::env::var("WEBRTC_SIGNALING_URL") {
config.signaling_url = signaling_url;
tracing::info!("🔧 WebRTC signaling URL: {}", config.signaling_url);
} else {
// Utiliser le port du serveur si disponible
let port = std::env::var("STREAM_PORT")
.or_else(|_| std::env::var("PORT"))
.unwrap_or_else(|_| "3002".to_string());
config.signaling_url = format!("ws://localhost:{}/ws/webrtc", port);
tracing::info!(
"🔧 WebRTC signaling URL par défaut: {}",
config.signaling_url
);
}
// Configuration du nombre maximum de peers
if let Ok(max_peers) = std::env::var("WEBRTC_MAX_PEERS") {
if let Ok(max) = max_peers.parse::<usize>() {
config.max_peers = max;
}
}
// Configuration du timeout de connexion
if let Ok(timeout) = std::env::var("WEBRTC_CONNECTION_TIMEOUT") {
if let Ok(secs) = timeout.parse::<u64>() {
config.connection_timeout = Duration::from_secs(secs);
}
}
// Configuration de l'intervalle de heartbeat
if let Ok(interval) = std::env::var("WEBRTC_HEARTBEAT_INTERVAL") {
if let Ok(secs) = interval.parse::<u64>() {
config.heartbeat_interval = Duration::from_secs(secs);
}
}
// Configuration de l'adaptation de bitrate
if let Ok(adaptation) = std::env::var("WEBRTC_BITRATE_ADAPTATION") {
config.bitrate_adaptation = adaptation.parse().unwrap_or(true);
}
// Configuration du jitter buffer
if let Ok(jitter) = std::env::var("WEBRTC_JITTER_BUFFER_MS") {
if let Ok(ms) = jitter.parse::<u32>() {
config.jitter_buffer_ms = ms;
}
}
tracing::info!(
"✅ Configuration WebRTC initialisée: {} ICE servers, max_peers={}, signaling={}",
config.ice_servers.len(),
config.max_peers,
config.signaling_url
);
config
}
/// Parse une chaîne de serveurs ICE au format JSON ou CSV
///
/// Format JSON: `[{"urls":["stun:stun.example.com"],"username":null,"credential":null}]`
/// Format CSV: `stun:stun.example.com,turn:turn.example.com:user:pass`
fn parse_ice_servers(servers_str: &str) -> Vec<IceServer> {
// Essayer de parser comme JSON d'abord
if let Ok(servers) = serde_json::from_str::<Vec<IceServer>>(servers_str) {
return servers;
}
// Sinon, parser comme CSV
let mut servers = Vec::new();
for server_str in servers_str.split(',') {
let server_str = server_str.trim();
if server_str.is_empty() {
continue;
}
// Format: "turn:turn.example.com:user:pass" ou "stun:stun.example.com"
if server_str.contains(':') {
let parts: Vec<&str> = server_str.split(':').collect();
if parts.len() >= 3 {
let protocol = parts[0];
let url = format!("{}:{}", protocol, parts[1]);
let username = if parts.len() > 3 {
Some(parts[2].to_string())
} else {
None
};
let credential = if parts.len() > 4 {
Some(parts[3].to_string())
} else {
None
};
servers.push(IceServer {
urls: vec![url],
username,
credential,
});
} else {
servers.push(IceServer {
urls: vec![server_str.to_string()],
username: None,
credential: None,
});
}
} else {
servers.push(IceServer {
urls: vec![server_str.to_string()],
username: None,
credential: None,
});
}
}
servers
}
/// Valide la configuration WebRTC
pub fn validate(&self) -> Result<(), String> {
if self.ice_servers.is_empty() {
return Err("Au moins un serveur ICE est requis".to_string());
}
if self.signaling_url.is_empty() {
return Err("URL de signaling est requise".to_string());
}
if !self.signaling_url.starts_with("ws://") && !self.signaling_url.starts_with("wss://") {
return Err("URL de signaling doit être un WebSocket (ws:// ou wss://)".to_string());
}
if self.max_peers == 0 {
return Err("max_peers doit être supérieur à 0".to_string());
}
if self.connection_timeout.as_secs() == 0 {
return Err("connection_timeout doit être supérieur à 0".to_string());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = WebRTCConfig::default();
assert!(!config.ice_servers.is_empty());
assert!(!config.signaling_url.is_empty());
assert!(config.max_peers > 0);
assert!(config.validate().is_ok());
}
#[test]
fn test_parse_ice_servers_json() {
let json = r#"[{"urls":["stun:stun.example.com"],"username":null,"credential":null}]"#;
let servers = WebRTCConfig::parse_ice_servers(json);
assert_eq!(servers.len(), 1);
assert_eq!(servers[0].urls[0], "stun:stun.example.com");
}
#[test]
fn test_parse_ice_servers_csv() {
let csv = "stun:stun.example.com:19302,turn:turn.example.com:user:pass";
let servers = WebRTCConfig::parse_ice_servers(csv);
assert!(servers.len() >= 1);
}
#[test]
fn test_validate_config() {
let mut config = WebRTCConfig::default();
assert!(config.validate().is_ok());
config.ice_servers.clear();
assert!(config.validate().is_err());
config = WebRTCConfig::default();
config.signaling_url = "".to_string();
assert!(config.validate().is_err());
config = WebRTCConfig::default();
config.signaling_url = "http://example.com".to_string();
assert!(config.validate().is_err());
}
}