2025-10-11 15:36:06 +02:00
|
|
|
//! ChromecastSink - Diffuse le flux audio vers un périphérique Chromecast
|
|
|
|
|
//!
|
|
|
|
|
//! Ce module fournit un sink qui envoie le flux audio à un Chromecast.
|
|
|
|
|
//! Note: Cette implémentation est une version mock/skeleton. Une vraie implémentation
|
|
|
|
|
//! nécessiterait une bibliothèque comme `rust-cast` ou similaire.
|
|
|
|
|
|
|
|
|
|
use crate::{nodes::AudioError, AudioChunk};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tokio::sync::mpsc;
|
|
|
|
|
|
|
|
|
|
/// Configuration pour le ChromecastSink
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct ChromecastConfig {
|
|
|
|
|
/// Nom ou adresse IP du Chromecast
|
|
|
|
|
pub device_address: String,
|
|
|
|
|
|
|
|
|
|
/// Nom amical du device
|
|
|
|
|
pub device_name: String,
|
|
|
|
|
|
|
|
|
|
/// Port de communication (défaut: 8009)
|
|
|
|
|
pub port: u16,
|
|
|
|
|
|
|
|
|
|
/// Taille du buffer de streaming
|
|
|
|
|
pub buffer_size: usize,
|
|
|
|
|
|
|
|
|
|
/// Format d'encodage pour le streaming
|
|
|
|
|
pub encoding: StreamEncoding,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for ChromecastConfig {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
device_address: "192.168.1.100".to_string(),
|
|
|
|
|
device_name: "Living Room".to_string(),
|
|
|
|
|
port: 8009,
|
|
|
|
|
buffer_size: 50,
|
|
|
|
|
encoding: StreamEncoding::Mp3,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Formats d'encodage supportés pour le streaming
|
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
|
|
|
pub enum StreamEncoding {
|
|
|
|
|
/// MP3 (compatible avec la plupart des Chromecasts)
|
|
|
|
|
Mp3,
|
|
|
|
|
/// AAC
|
|
|
|
|
Aac,
|
|
|
|
|
/// Opus
|
|
|
|
|
Opus,
|
|
|
|
|
/// PCM non compressé (haute qualité, bande passante élevée)
|
|
|
|
|
Pcm,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// ChromecastSink - Diffuse vers un périphérique Chromecast
|
|
|
|
|
///
|
|
|
|
|
/// Ce sink encode le flux audio et le streame vers un Chromecast.
|
|
|
|
|
/// La connexion est établie lors de l'initialisation et maintenue pendant toute la durée.
|
|
|
|
|
///
|
|
|
|
|
/// # Implémentation actuelle
|
|
|
|
|
///
|
|
|
|
|
/// Cette version est un mock qui simule l'envoi au Chromecast.
|
|
|
|
|
/// Pour une vraie implémentation, il faudrait:
|
|
|
|
|
/// - Utiliser une bibliothèque comme `rust-cast`
|
|
|
|
|
/// - Établir une connexion TLS avec le device
|
|
|
|
|
/// - Lancer une application de récepteur sur le Chromecast
|
|
|
|
|
/// - Encoder l'audio dans le format approprié
|
|
|
|
|
/// - Streamer via HTTP ou WebSocket
|
|
|
|
|
///
|
|
|
|
|
/// # Exemples
|
|
|
|
|
///
|
|
|
|
|
/// ```no_run
|
|
|
|
|
/// use pmoaudio::{ChromecastSink, ChromecastConfig};
|
|
|
|
|
///
|
|
|
|
|
/// #[tokio::main]
|
|
|
|
|
/// async fn main() {
|
|
|
|
|
/// let config = ChromecastConfig {
|
|
|
|
|
/// device_address: "192.168.1.100".to_string(),
|
|
|
|
|
/// device_name: "Living Room".to_string(),
|
|
|
|
|
/// ..Default::default()
|
|
|
|
|
/// };
|
|
|
|
|
///
|
|
|
|
|
/// let (sink, sink_tx) = ChromecastSink::new("chromecast1".to_string(), config, 10);
|
|
|
|
|
///
|
|
|
|
|
/// tokio::spawn(async move {
|
|
|
|
|
/// sink.run().await.unwrap()
|
|
|
|
|
/// });
|
|
|
|
|
/// }
|
|
|
|
|
/// ```
|
|
|
|
|
pub struct ChromecastSink {
|
|
|
|
|
/// Identifiant du sink
|
|
|
|
|
node_id: String,
|
|
|
|
|
|
|
|
|
|
/// Channel pour recevoir les chunks audio
|
|
|
|
|
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
|
|
|
|
|
|
|
|
|
/// Configuration
|
|
|
|
|
config: ChromecastConfig,
|
|
|
|
|
|
|
|
|
|
/// État de la connexion (mock)
|
|
|
|
|
connected: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ChromecastSink {
|
|
|
|
|
/// Crée un nouveau ChromecastSink
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
///
|
|
|
|
|
/// * `node_id` - Identifiant unique du sink
|
|
|
|
|
/// * `config` - Configuration du Chromecast
|
|
|
|
|
/// * `channel_size` - Taille du buffer du channel
|
|
|
|
|
pub fn new(
|
|
|
|
|
node_id: String,
|
|
|
|
|
config: ChromecastConfig,
|
|
|
|
|
channel_size: usize,
|
|
|
|
|
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
|
|
|
|
let (tx, rx) = mpsc::channel(channel_size);
|
|
|
|
|
|
|
|
|
|
let sink = Self {
|
|
|
|
|
node_id,
|
|
|
|
|
rx,
|
|
|
|
|
config,
|
|
|
|
|
connected: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(sink, tx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Établit la connexion avec le Chromecast (mock)
|
|
|
|
|
async fn connect(&mut self) -> Result<(), AudioError> {
|
|
|
|
|
println!(
|
|
|
|
|
"[{}] Connecting to Chromecast '{}' at {}:{}...",
|
|
|
|
|
self.node_id, self.config.device_name, self.config.device_address, self.config.port
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Simuler une connexion
|
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
|
|
|
|
|
|
|
|
self.connected = true;
|
|
|
|
|
|
|
|
|
|
println!(
|
|
|
|
|
"[{}] Connected to Chromecast '{}' successfully",
|
|
|
|
|
self.node_id, self.config.device_name
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Envoie un chunk au Chromecast (mock)
|
|
|
|
|
async fn send_chunk(&self, _chunk: &AudioChunk) -> Result<(), AudioError> {
|
|
|
|
|
if !self.connected {
|
|
|
|
|
return Err(AudioError::ProcessingError(
|
|
|
|
|
"Not connected to Chromecast".to_string(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dans une vraie implémentation:
|
|
|
|
|
// 1. Appliquer le gain
|
|
|
|
|
// 2. Encoder dans le format approprié (MP3, AAC, etc.)
|
|
|
|
|
// 3. Envoyer via le protocole Chromecast
|
|
|
|
|
|
|
|
|
|
// Pour l'instant, simplement simuler un délai d'envoi
|
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_micros(50)).await;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Déconnecte proprement du Chromecast (mock)
|
|
|
|
|
async fn disconnect(&mut self) -> Result<(), AudioError> {
|
|
|
|
|
if self.connected {
|
|
|
|
|
println!(
|
|
|
|
|
"[{}] Disconnecting from Chromecast '{}'...",
|
|
|
|
|
self.node_id, self.config.device_name
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Simuler la déconnexion
|
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
|
|
|
|
|
|
|
|
|
self.connected = false;
|
|
|
|
|
|
|
|
|
|
println!("[{}] Disconnected successfully", self.node_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Démarre la boucle de traitement du ChromecastSink
|
|
|
|
|
pub async fn run(mut self) -> Result<ChromecastStats, AudioError> {
|
|
|
|
|
// Établir la connexion
|
|
|
|
|
self.connect().await?;
|
|
|
|
|
|
2025-10-19 13:42:29 +02:00
|
|
|
let mut stats = ChromecastStats::new(self.node_id.clone(), self.config.device_name.clone());
|
2025-10-11 15:36:06 +02:00
|
|
|
|
|
|
|
|
// Boucle principale
|
|
|
|
|
while let Some(chunk) = self.rx.recv().await {
|
|
|
|
|
// Appliquer le gain si nécessaire
|
2025-10-28 18:41:34 +01:00
|
|
|
let chunk_to_send = if chunk.gain_db().abs() > f64::EPSILON {
|
|
|
|
|
Arc::clone(&chunk).apply_gain()
|
2025-10-11 15:36:06 +02:00
|
|
|
} else {
|
2025-10-28 18:41:34 +01:00
|
|
|
Arc::clone(&chunk)
|
2025-10-11 15:36:06 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Envoyer au Chromecast
|
|
|
|
|
self.send_chunk(&chunk_to_send).await?;
|
|
|
|
|
|
|
|
|
|
stats.record_chunk(&chunk_to_send);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Déconnexion propre
|
|
|
|
|
self.disconnect().await?;
|
|
|
|
|
|
|
|
|
|
stats.finalize();
|
|
|
|
|
Ok(stats)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Statistiques du ChromecastSink
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct ChromecastStats {
|
|
|
|
|
pub node_id: String,
|
|
|
|
|
pub device_name: String,
|
|
|
|
|
pub chunks_sent: u64,
|
|
|
|
|
pub total_samples: u64,
|
|
|
|
|
pub total_duration_sec: f64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ChromecastStats {
|
|
|
|
|
pub fn new(node_id: String, device_name: String) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
node_id,
|
|
|
|
|
device_name,
|
|
|
|
|
chunks_sent: 0,
|
|
|
|
|
total_samples: 0,
|
|
|
|
|
total_duration_sec: 0.0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn record_chunk(&mut self, chunk: &AudioChunk) {
|
|
|
|
|
self.chunks_sent += 1;
|
|
|
|
|
self.total_samples += chunk.len() as u64;
|
2025-10-28 18:41:34 +01:00
|
|
|
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
|
2025-10-11 15:36:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn finalize(&mut self) {
|
|
|
|
|
// Calculs finaux si nécessaire
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn display(&self) {
|
|
|
|
|
println!("\n=== Chromecast Statistics: {} ===", self.node_id);
|
|
|
|
|
println!("Device: {}", self.device_name);
|
|
|
|
|
println!("Chunks sent: {}", self.chunks_sent);
|
|
|
|
|
println!("Total samples: {}", self.total_samples);
|
|
|
|
|
println!("Total duration: {:.3} sec", self.total_duration_sec);
|
|
|
|
|
println!("==================================\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2025-10-28 18:41:34 +01:00
|
|
|
use std::i32;
|
|
|
|
|
|
2025-10-11 15:36:06 +02:00
|
|
|
use super::*;
|
2025-10-28 18:41:34 +01:00
|
|
|
use crate::BitDepth;
|
2025-10-11 15:36:06 +02:00
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_chromecast_sink_basic() {
|
|
|
|
|
let config = ChromecastConfig {
|
|
|
|
|
device_address: "127.0.0.1".to_string(),
|
|
|
|
|
device_name: "Test Device".to_string(),
|
|
|
|
|
..Default::default()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let (sink, tx) = ChromecastSink::new("test".to_string(), config, 10);
|
|
|
|
|
|
|
|
|
|
let handle = tokio::spawn(async move { sink.run().await });
|
|
|
|
|
|
|
|
|
|
// Envoyer quelques chunks
|
|
|
|
|
for i in 0..5 {
|
2025-10-28 18:41:34 +01:00
|
|
|
let stereo = vec![[i32::MAX / 2; 2]; 1000];
|
|
|
|
|
let chunk = AudioChunk::new(i, stereo, 48000, BitDepth::B24);
|
|
|
|
|
tx.send(chunk).await.unwrap();
|
2025-10-11 15:36:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
drop(tx);
|
|
|
|
|
|
|
|
|
|
let stats = handle.await.unwrap().unwrap();
|
|
|
|
|
assert_eq!(stats.chunks_sent, 5);
|
|
|
|
|
assert_eq!(stats.device_name, "Test Device");
|
|
|
|
|
}
|
|
|
|
|
}
|