veza/veza-stream-server/src/audio/processing.rs

381 lines
13 KiB
Rust
Raw Normal View History

2025-12-03 19:36:56 +00:00
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
time::{Duration, SystemTime},
};
use tokio::sync::RwLock;
// Imports simplifiés pour éviter les erreurs de compilation
use serde::{Deserialize, Serialize};
// Note: Use tracing::info! macro directly instead of importing
use crate::config::Config;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioMetadata {
pub duration_seconds: f64,
pub sample_rate: u32,
pub channels: u32,
pub bitrate_kbps: Option<u32>,
pub codec: String,
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub year: Option<u32>,
pub genre: Option<String>,
pub artwork_available: bool,
pub file_size: u64,
pub last_modified: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WaveformData {
pub peaks: Vec<f32>, // Amplitude peaks pour la visualisation
pub rms: Vec<f32>, // RMS values pour un rendu plus smooth
pub sample_rate: u32, // Taux d'échantillonnage des données de waveform
pub duration_ms: u32, // Durée en millisecondes
pub generated_at: SystemTime,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudioQuality {
pub bitrate_kbps: u32,
pub sample_rate: u32,
pub channels: u32,
pub codec: String,
}
impl AudioQuality {
pub fn high_quality() -> Self {
Self {
bitrate_kbps: 320,
sample_rate: 44100,
channels: 2,
codec: "mp3".to_string(),
}
}
pub fn medium_quality() -> Self {
Self {
bitrate_kbps: 192,
sample_rate: 44100,
channels: 2,
codec: "mp3".to_string(),
}
}
pub fn low_quality() -> Self {
Self {
bitrate_kbps: 128,
sample_rate: 22050,
channels: 1,
codec: "mp3".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct AudioProcessor {
_config: Arc<Config>,
metadata_cache: Arc<RwLock<HashMap<PathBuf, AudioMetadata>>>,
waveform_cache: Arc<RwLock<HashMap<PathBuf, WaveformData>>>,
}
impl AudioProcessor {
pub fn new(config: Arc<Config>) -> Self {
Self {
_config: config,
metadata_cache: Arc::new(RwLock::new(HashMap::new())),
waveform_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Extrait les métadonnées d'un fichier audio
pub async fn extract_metadata(
&self,
file_path: &Path,
) -> Result<AudioMetadata, Box<dyn std::error::Error + Send + Sync>> {
// Vérifier le cache d'abord
if let Some(metadata) = self.metadata_cache.read().await.get(file_path) {
return Ok(metadata.clone());
}
// Extraire les métadonnées réelles
let metadata = self.extract_metadata_from_file(file_path).await?;
// Mettre en cache
self.metadata_cache
.write()
.await
.insert(file_path.to_path_buf(), metadata.clone());
Ok(metadata)
}
async fn extract_metadata_from_file(
&self,
file_path: &Path,
) -> Result<AudioMetadata, Box<dyn std::error::Error + Send + Sync>> {
// Implémentation basique - dans un vrai projet, on utiliserait symphonia
let file_metadata = std::fs::metadata(file_path)?;
Ok(AudioMetadata {
duration_seconds: 180.0, // Valeur par défaut
sample_rate: 44100,
channels: 2,
bitrate_kbps: Some(320),
codec: "MP3".to_string(),
title: file_path
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string()),
artist: None,
album: None,
year: None,
genre: None,
artwork_available: false,
file_size: file_metadata.len(),
last_modified: file_metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
})
}
/// Génère les données de waveform pour la visualisation
pub async fn generate_waveform(
&self,
file_path: &Path,
resolution: usize,
) -> Result<WaveformData, Box<dyn std::error::Error + Send + Sync>> {
// Vérifier le cache d'abord
if let Some(waveform) = self.waveform_cache.read().await.get(file_path) {
return Ok(waveform.clone());
}
// Générer la waveform
let waveform = self.generate_waveform_data(file_path, resolution).await?;
// Mettre en cache
self.waveform_cache
.write()
.await
.insert(file_path.to_path_buf(), waveform.clone());
Ok(waveform)
}
async fn generate_waveform_data(
&self,
_file_path: &Path,
resolution: usize,
) -> Result<WaveformData, Box<dyn std::error::Error + Send + Sync>> {
// Implémentation basique - génère des données de test
let peaks: Vec<f32> = (0..resolution)
.map(|i| ((i as f32 * 0.1).sin() * 0.8).abs())
.collect();
let rms: Vec<f32> = peaks.iter().map(|&p| p * 0.7).collect();
Ok(WaveformData {
peaks,
rms,
sample_rate: 44100,
duration_ms: 180000, // 3 minutes
generated_at: SystemTime::now(),
})
}
/// Analyse spectrale pour obtenir les fréquences dominantes
pub async fn analyze_spectrum(
&self,
_file_path: &Path,
fft_size: usize,
) -> Result<Vec<f32>, Box<dyn std::error::Error + Send + Sync>> {
// Implémentation basique - génère des données de test
let spectrum: Vec<f32> = (0..fft_size / 2)
.map(|i| {
let freq = i as f32 / (fft_size as f32 / 2.0);
(freq * 10.0).sin().abs() * (1.0 - freq).max(0.0)
})
.collect();
Ok(spectrum)
}
/// Transcode un fichier audio vers une qualité différente
pub async fn transcode_quality(
&self,
input_path: &Path,
output_path: &Path,
quality: AudioQuality,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
tracing::info!(
"Transcodage de {:?} vers {:?} avec qualité {:?}",
input_path,
output_path,
quality
);
// Pour l'instant, implémentation simplifiée
// En production, on utiliserait FFmpeg ou un encodeur dédié
let metadata = self.extract_metadata(input_path).await?;
tracing::debug!(
"Transcodage requis: {} Hz -> {} Hz, {} -> {} canaux",
metadata.sample_rate,
quality.sample_rate,
metadata.channels,
quality.channels
);
// Cette fonction serait complétée avec un vrai transcodeur
tracing::warn!("Transcodage non encore implémenté - nécessite un encodeur MP3/AAC");
Ok(())
}
/// Nettoie les caches expirés
pub async fn cleanup_caches(&self, max_age_hours: u64) {
let cutoff = SystemTime::now() - std::time::Duration::from_secs(max_age_hours * 3600);
{
let mut metadata_cache = self.metadata_cache.write().await;
let before_count = metadata_cache.len();
metadata_cache.retain(|_, _| true); // Pour l'instant, pas de timestamp sur les métadonnées
tracing::debug!("Nettoyage cache métadonnées: {} entrées", before_count);
}
{
let mut waveform_cache = self.waveform_cache.write().await;
let before_count = waveform_cache.len();
waveform_cache.retain(|_, waveform| waveform.generated_at > cutoff);
let after_count = waveform_cache.len();
if before_count > after_count {
tracing::info!(
"Nettoyage cache waveform: {} -> {} entrées",
before_count,
after_count
);
}
}
}
/// Obtient les statistiques des caches
pub async fn get_cache_stats(&self) -> serde_json::Value {
let metadata_cache = self.metadata_cache.read().await;
let waveform_cache = self.waveform_cache.read().await;
serde_json::json!({
"metadata_cache_entries": metadata_cache.len(),
"waveform_cache_entries": waveform_cache.len(),
"total_cache_entries": metadata_cache.len() + waveform_cache.len()
})
}
pub async fn clear_cache(&self) {
self.metadata_cache.write().await.clear();
self.waveform_cache.write().await.clear();
}
}
impl Default for AudioProcessor {
fn default() -> Self {
// Utiliser une configuration par défaut simple pour les tests
let config = Config::from_env().unwrap_or_else(|_| {
// Valeurs par défaut minimales si from_env échoue
Config {
secret_key: "default_secret_key_for_testing_only".to_string(),
port: 18082,
backend_url: "http://backend-api:18080".to_string(),
2025-12-03 19:36:56 +00:00
audio_dir: "./audio".to_string(),
allowed_origins: vec!["*".to_string()],
max_file_size: 100 * 1024 * 1024,
max_range_size: 10 * 1024 * 1024,
signature_tolerance: 60,
database: crate::config::DatabaseConfig {
url: std::env::var("DATABASE_URL").unwrap_or_else(|_| {
"postgres://veza:veza_password@postgres:5432/veza_db?sslmode=disable"
.to_string()
}),
max_connections: 10,
min_connections: 1,
connection_timeout: Duration::from_secs(30),
idle_timeout: Duration::from_secs(600),
max_lifetime: Duration::from_secs(1800),
enable_logging: false,
migrate_on_start: true,
},
cache: crate::config::CacheConfig {
max_size_mb: 256,
ttl_seconds: 3600,
cleanup_interval: Duration::from_secs(300),
compression_enabled: false,
redis_url: None,
redis_pool_size: None,
},
security: crate::config::SecurityConfig {
jwt_secret: None,
2026-03-05 18:22:31 +00:00
jwt_public_key_path: None,
2025-12-03 19:36:56 +00:00
jwt_expiration: Duration::from_secs(3600),
bcrypt_cost: 10,
rate_limit_requests_per_minute: 60,
rate_limit_burst: 10,
cors_max_age: Duration::from_secs(3600),
csrf_protection: false,
secure_headers: true,
tls_cert_path: None,
tls_key_path: None,
},
performance: crate::config::PerformanceConfig {
worker_threads: None,
max_blocking_threads: None,
thread_stack_size: None,
tcp_nodelay: true,
tcp_keepalive: None,
buffer_size: 65536,
max_concurrent_streams: 100,
stream_timeout: Duration::from_secs(30),
compression_level: 6,
},
monitoring: crate::config::MonitoringConfig {
metrics_enabled: false,
metrics_port: 9090,
health_check_interval: Duration::from_secs(30),
log_level: "info".to_string(),
log_format: crate::config::LogFormat::Pretty,
jaeger_endpoint: None,
prometheus_namespace: "stream_server".to_string(),
alert_webhooks: vec![],
},
notifications: crate::config::NotificationConfig {
enabled: false,
max_queue_size: 1000,
delivery_workers: 2,
retry_attempts: 3,
retry_delay: Duration::from_secs(60),
batch_size: 10,
email_provider: None,
sms_provider: None,
push_provider: None,
},
compression: crate::config::CompressionConfig {
enabled: false,
output_dir: "./compressed".to_string(),
temp_dir: "./temp".to_string(),
max_concurrent_jobs: 2,
cleanup_after_days: 30,
ffmpeg_path: None,
quality_profiles: vec![],
},
rabbit_mq: crate::config::RabbitMQConfig {
// Added rabbit_mq config
url: "amqp://guest:guest@localhost:5672/".to_string(),
max_retries: 3,
retry_interval_secs: 2,
enable: true,
},
environment: crate::config::Environment::Development,
}
});
Self::new(Arc::new(config))
}
}