2025-11-05 15:49:23 +00:00
|
|
|
//! Télécharge un bloc Radio Paradise, le cache, et le joue en même temps
|
|
|
|
|
//!
|
|
|
|
|
//! Ce programme démontre l'utilisation complète de la chaîne :
|
|
|
|
|
//! 1. RadioParadiseStreamSource - Télécharge et décode un bloc FLAC
|
|
|
|
|
//! 2. FlacCacheSink - Cache chaque piste en FLAC et alimente une playlist
|
|
|
|
|
//! 3. PlaylistSource - Lit la playlist pendant le téléchargement
|
2025-11-07 13:44:25 +00:00
|
|
|
//! 4. TimerNode - Régule le débit pour éviter EOF prématurés (progressive cache)
|
|
|
|
|
//! 5. AudioSink - Joue l'audio sur la sortie standard
|
2025-11-05 15:49:23 +00:00
|
|
|
//!
|
|
|
|
|
//! Architecture :
|
|
|
|
|
//! ```text
|
|
|
|
|
//! Pipeline 1 (Download & Cache):
|
|
|
|
|
//! RadioParadiseStreamSource → FlacCacheSink (avec playlist abonnée)
|
|
|
|
|
//!
|
|
|
|
|
//! Pipeline 2 (Playback):
|
2025-11-07 13:44:25 +00:00
|
|
|
//! PlaylistSource → TimerNode (rate limiting) → AudioSink
|
|
|
|
|
//! ↓
|
|
|
|
|
//! Prévention EOF
|
|
|
|
|
//! (3s max lead)
|
2025-11-05 15:49:23 +00:00
|
|
|
//! ```
|
|
|
|
|
//!
|
|
|
|
|
//! Usage:
|
2025-11-05 15:55:57 +00:00
|
|
|
//! cargo run --example play_and_cache --features full -- <channel_id>
|
2025-11-05 15:49:23 +00:00
|
|
|
//!
|
|
|
|
|
//! Exemple:
|
2025-11-05 15:55:57 +00:00
|
|
|
//! cargo run --example play_and_cache --features full -- 0 # Main Mix
|
|
|
|
|
//! cargo run --example play_and_cache --features full -- 2 # Rock Mix
|
2025-11-05 15:49:23 +00:00
|
|
|
|
2025-11-07 13:44:25 +00:00
|
|
|
use pmoaudio::{AudioPipelineNode, AudioSink, TimerNode};
|
2025-11-05 15:49:23 +00:00
|
|
|
use pmoaudio_ext::{FlacCacheSink, PlaylistSource};
|
2025-11-05 15:55:57 +00:00
|
|
|
use pmoaudiocache::Cache as AudioCache;
|
|
|
|
|
use pmocovers::Cache as CoverCache;
|
2025-11-05 15:49:23 +00:00
|
|
|
use pmoparadise::{RadioParadiseClient, RadioParadiseStreamSource};
|
|
|
|
|
use std::env;
|
2025-11-05 15:55:57 +00:00
|
|
|
use std::sync::Arc;
|
2025-11-05 15:49:23 +00:00
|
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
// Initialiser tracing avec beaucoup de logs
|
|
|
|
|
tracing_subscriber::fmt()
|
|
|
|
|
.with_env_filter(
|
|
|
|
|
tracing_subscriber::EnvFilter::from_default_env()
|
|
|
|
|
.add_directive(tracing::Level::DEBUG.into())
|
|
|
|
|
.add_directive("pmoaudio=debug".parse()?)
|
|
|
|
|
.add_directive("pmoaudio_ext=debug".parse()?)
|
|
|
|
|
.add_directive("pmoplaylist=debug".parse()?)
|
|
|
|
|
.add_directive("pmoparadise=debug".parse()?)
|
2025-11-14 10:43:53 +01:00
|
|
|
.add_directive("pmoaudiocache=debug".parse()?),
|
2025-11-05 15:49:23 +00:00
|
|
|
)
|
|
|
|
|
.init();
|
|
|
|
|
|
|
|
|
|
tracing::info!("=== Radio Paradise Play & Cache ===");
|
|
|
|
|
|
|
|
|
|
// Récupérer les arguments
|
|
|
|
|
let args: Vec<String> = env::args().collect();
|
2025-11-07 07:24:19 +00:00
|
|
|
if args.len() < 2 {
|
|
|
|
|
eprintln!("Usage: {} <channel_id> [--null-audio]", args[0]);
|
2025-11-05 15:49:23 +00:00
|
|
|
eprintln!();
|
|
|
|
|
eprintln!("Downloads a Radio Paradise block, caches it, and plays it simultaneously.");
|
|
|
|
|
eprintln!();
|
|
|
|
|
eprintln!("Channel IDs:");
|
|
|
|
|
eprintln!(" 0 - Main Mix (eclectic, diverse mix)");
|
|
|
|
|
eprintln!(" 1 - Mellow Mix (smooth, chilled music)");
|
|
|
|
|
eprintln!(" 2 - Rock Mix (classic & modern rock)");
|
|
|
|
|
eprintln!(" 3 - World/Etc Mix (global sounds)");
|
2025-11-07 07:24:19 +00:00
|
|
|
eprintln!();
|
|
|
|
|
eprintln!("Options:");
|
|
|
|
|
eprintln!(" --null-audio Don't play audio (for testing without audio device)");
|
2025-11-05 15:49:23 +00:00
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let channel_id: u8 = match args[1].parse() {
|
|
|
|
|
Ok(id) if id <= 3 => id,
|
|
|
|
|
_ => {
|
|
|
|
|
eprintln!("Error: channel_id must be a number between 0 and 3");
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-07 07:24:19 +00:00
|
|
|
let use_null_audio = args.len() > 2 && args[2] == "--null-audio";
|
|
|
|
|
|
2025-11-05 15:49:23 +00:00
|
|
|
tracing::info!("Channel ID: {}", channel_id);
|
2025-11-07 07:24:19 +00:00
|
|
|
if use_null_audio {
|
|
|
|
|
tracing::info!("Using null audio output (no playback)");
|
|
|
|
|
}
|
2025-11-05 15:49:23 +00:00
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Initialiser les caches et le gestionnaire de playlist
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2025-11-14 10:43:53 +01:00
|
|
|
let base_dir =
|
|
|
|
|
std::env::var("PMO_CONFIG_DIR").unwrap_or_else(|_| "/tmp/pmomusic_test".to_string());
|
2025-11-05 15:55:57 +00:00
|
|
|
std::fs::create_dir_all(&base_dir)?;
|
2025-11-05 15:49:23 +00:00
|
|
|
|
2025-11-05 15:55:57 +00:00
|
|
|
tracing::info!("Initializing caches in: {}", base_dir);
|
2025-11-05 15:49:23 +00:00
|
|
|
|
2025-11-05 15:55:57 +00:00
|
|
|
// Créer le cache audio
|
|
|
|
|
let audio_cache_dir = format!("{}/audio_cache", base_dir);
|
|
|
|
|
std::fs::create_dir_all(&audio_cache_dir)?;
|
|
|
|
|
let audio_cache = Arc::new(AudioCache::new(
|
|
|
|
|
&audio_cache_dir,
|
|
|
|
|
1000, // 1000 MB limit
|
|
|
|
|
)?);
|
|
|
|
|
tracing::debug!("Audio cache initialized at: {}", audio_cache_dir);
|
2025-11-05 15:49:23 +00:00
|
|
|
|
2025-11-05 15:55:57 +00:00
|
|
|
// Créer le cache de covers
|
|
|
|
|
let cover_cache_dir = format!("{}/cover_cache", base_dir);
|
|
|
|
|
std::fs::create_dir_all(&cover_cache_dir)?;
|
|
|
|
|
let cover_cache = Arc::new(CoverCache::new(
|
|
|
|
|
&cover_cache_dir,
|
|
|
|
|
100, // 100 MB limit
|
|
|
|
|
)?);
|
|
|
|
|
tracing::debug!("Cover cache initialized at: {}", cover_cache_dir);
|
|
|
|
|
|
2025-11-05 20:02:01 +00:00
|
|
|
// Enregistrer le cache audio dans pmoplaylist
|
2025-11-05 19:49:59 +00:00
|
|
|
// (requis par pmoplaylist pour valider les pks)
|
2025-11-05 20:02:01 +00:00
|
|
|
pmoplaylist::register_audio_cache(audio_cache.clone());
|
|
|
|
|
tracing::debug!("Audio cache registered in pmoplaylist");
|
2025-11-05 19:49:59 +00:00
|
|
|
|
2025-11-05 15:55:57 +00:00
|
|
|
// Utiliser le gestionnaire de playlist singleton
|
|
|
|
|
tracing::info!("Getting playlist manager...");
|
|
|
|
|
let playlist_manager = pmoplaylist::PlaylistManager();
|
|
|
|
|
tracing::debug!("Playlist manager obtained");
|
2025-11-05 15:49:23 +00:00
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Créer la playlist pour ce channel
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
let playlist_id = format!("radio-paradise-ch{}", channel_id);
|
|
|
|
|
tracing::info!("Creating playlist: {}", playlist_id);
|
|
|
|
|
|
2025-11-09 09:32:44 +00:00
|
|
|
// Créer une playlist éphémère (non persistante) pour cet exemple
|
2025-11-14 10:43:53 +01:00
|
|
|
let writer = playlist_manager
|
|
|
|
|
.get_write_handle(playlist_id.clone())
|
|
|
|
|
.await?;
|
|
|
|
|
writer
|
|
|
|
|
.set_title(format!("Radio Paradise - Channel {}", channel_id))
|
|
|
|
|
.await?;
|
2025-11-05 15:49:23 +00:00
|
|
|
writer.flush().await?; // Vider la playlist si elle existait
|
|
|
|
|
tracing::debug!("Playlist created and flushed");
|
|
|
|
|
|
|
|
|
|
// Créer le reader pour la lecture
|
|
|
|
|
let reader = playlist_manager.get_read_handle(&playlist_id).await?;
|
|
|
|
|
tracing::debug!("Read handle created");
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Récupérer les infos du bloc à télécharger
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
tracing::info!("Fetching current block metadata...");
|
|
|
|
|
let client = RadioParadiseClient::builder()
|
|
|
|
|
.channel(channel_id)
|
|
|
|
|
.build()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let block = client.get_block(None).await?;
|
|
|
|
|
|
|
|
|
|
tracing::info!("Block Information:");
|
|
|
|
|
tracing::info!(" Event ID: {}", block.event);
|
|
|
|
|
tracing::info!(" Songs: {}", block.song_count());
|
|
|
|
|
tracing::info!(" Duration: {:.1} minutes", block.length as f64 / 60000.0);
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
|
|
|
|
|
tracing::info!("Tracklist:");
|
|
|
|
|
for (index, song) in block.songs_ordered() {
|
|
|
|
|
tracing::info!(
|
|
|
|
|
" {:2}. {} - {} ({})",
|
|
|
|
|
index + 1,
|
|
|
|
|
song.artist,
|
|
|
|
|
song.title,
|
|
|
|
|
song.album.as_deref().unwrap_or("Unknown Album")
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Pipeline 1: Téléchargement et cache
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
tracing::info!("Creating download pipeline...");
|
|
|
|
|
|
|
|
|
|
// Créer la source Radio Paradise
|
|
|
|
|
let mut download_source = RadioParadiseStreamSource::new(client);
|
|
|
|
|
download_source.push_block_id(block.event);
|
2025-11-14 10:43:53 +01:00
|
|
|
tracing::debug!(
|
|
|
|
|
"RadioParadiseStreamSource created with block {}",
|
|
|
|
|
block.event
|
|
|
|
|
);
|
2025-11-05 15:49:23 +00:00
|
|
|
|
|
|
|
|
// Créer le sink de cache FLAC
|
|
|
|
|
let mut cache_sink = FlacCacheSink::new(audio_cache.clone(), cover_cache.clone());
|
|
|
|
|
cache_sink.register_playlist(writer);
|
|
|
|
|
tracing::debug!("FlacCacheSink created and registered with playlist");
|
|
|
|
|
|
|
|
|
|
// Connecter source → sink
|
|
|
|
|
download_source.register(Box::new(cache_sink));
|
|
|
|
|
tracing::info!("Download pipeline connected: RadioParadiseStreamSource → FlacCacheSink");
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Pipeline 2: Lecture depuis la playlist
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
tracing::info!("Creating playback pipeline...");
|
|
|
|
|
|
|
|
|
|
// Créer la source playlist
|
|
|
|
|
let mut playlist_source = PlaylistSource::new(reader, audio_cache.clone());
|
|
|
|
|
tracing::debug!("PlaylistSource created");
|
|
|
|
|
|
2025-11-07 13:44:25 +00:00
|
|
|
// Créer le timer node pour réguler le débit (empêche EOF prématurés)
|
|
|
|
|
// Tolère 3 secondes d'avance max pour permettre le buffering
|
|
|
|
|
let mut timer = TimerNode::new(3.0);
|
|
|
|
|
tracing::debug!("TimerNode created (max_lead_time=3.0s)");
|
|
|
|
|
|
2025-11-05 19:39:48 +00:00
|
|
|
// Créer le sink audio
|
2025-11-07 07:24:19 +00:00
|
|
|
let audio_sink = if use_null_audio {
|
|
|
|
|
AudioSink::with_null_output()
|
|
|
|
|
} else {
|
|
|
|
|
AudioSink::new()
|
|
|
|
|
};
|
2025-11-05 19:39:48 +00:00
|
|
|
tracing::debug!("AudioSink created");
|
2025-11-05 15:49:23 +00:00
|
|
|
|
2025-11-07 13:44:25 +00:00
|
|
|
// Connecter timer → audio (AVANT de mettre timer dans une Box)
|
|
|
|
|
timer.register(Box::new(audio_sink));
|
|
|
|
|
|
|
|
|
|
// Connecter playlist → timer
|
|
|
|
|
playlist_source.register(Box::new(timer));
|
|
|
|
|
tracing::info!("Playback pipeline connected: PlaylistSource → TimerNode → AudioSink");
|
2025-11-05 15:49:23 +00:00
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Lancer les deux pipelines en parallèle
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("Starting both pipelines...");
|
|
|
|
|
tracing::info!("Pipeline 1: Downloading and caching");
|
|
|
|
|
tracing::info!("Pipeline 2: Playing from playlist");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
|
|
|
|
|
let stop_token = CancellationToken::new();
|
|
|
|
|
let stop_token_download = stop_token.clone();
|
|
|
|
|
let stop_token_playback = stop_token.clone();
|
|
|
|
|
|
|
|
|
|
// Gérer Ctrl+C
|
|
|
|
|
let stop_token_ctrl_c = stop_token.clone();
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
tokio::signal::ctrl_c().await.ok();
|
|
|
|
|
tracing::warn!("Received Ctrl+C, stopping...");
|
|
|
|
|
stop_token_ctrl_c.cancel();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let start = std::time::Instant::now();
|
|
|
|
|
|
|
|
|
|
// Lancer les deux pipelines en parallèle
|
|
|
|
|
let download_handle = tokio::spawn(async move {
|
|
|
|
|
tracing::info!("[DOWNLOAD] Pipeline starting...");
|
|
|
|
|
let result = Box::new(download_source).run(stop_token_download).await;
|
|
|
|
|
match &result {
|
|
|
|
|
Ok(()) => tracing::info!("[DOWNLOAD] Pipeline completed successfully"),
|
|
|
|
|
Err(e) => tracing::error!("[DOWNLOAD] Pipeline error: {}", e),
|
|
|
|
|
}
|
|
|
|
|
result
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let playback_handle = tokio::spawn(async move {
|
2025-11-06 21:38:29 +00:00
|
|
|
// Pas de sleep - le cache progressif permet de démarrer immédiatement
|
|
|
|
|
// dès que le prebuffer (512 KB) est atteint
|
|
|
|
|
tracing::info!("[PLAYBACK] Pipeline starting (will wait for prebuffer)...");
|
2025-11-05 15:49:23 +00:00
|
|
|
let result = Box::new(playlist_source).run(stop_token_playback).await;
|
|
|
|
|
match &result {
|
|
|
|
|
Ok(()) => tracing::info!("[PLAYBACK] Pipeline completed successfully"),
|
|
|
|
|
Err(e) => tracing::error!("[PLAYBACK] Pipeline error: {}", e),
|
|
|
|
|
}
|
|
|
|
|
result
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Attendre les deux pipelines
|
|
|
|
|
let (download_result, playback_result) = tokio::join!(download_handle, playback_handle);
|
|
|
|
|
|
|
|
|
|
let elapsed = start.elapsed();
|
|
|
|
|
|
|
|
|
|
// Vérifier les résultats
|
|
|
|
|
match (download_result, playback_result) {
|
|
|
|
|
(Ok(Ok(())), Ok(Ok(()))) => {
|
|
|
|
|
tracing::info!("");
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
tracing::info!("✓ Both pipelines completed successfully");
|
|
|
|
|
tracing::info!(" Total time: {:.2}s", elapsed.as_secs_f64());
|
|
|
|
|
tracing::info!("========================================");
|
|
|
|
|
}
|
|
|
|
|
(download_res, playback_res) => {
|
|
|
|
|
tracing::error!("");
|
|
|
|
|
tracing::error!("========================================");
|
|
|
|
|
if let Err(e) = download_res {
|
|
|
|
|
tracing::error!("✗ Download pipeline error: {:?}", e);
|
|
|
|
|
} else if let Ok(Err(e)) = download_res {
|
|
|
|
|
tracing::error!("✗ Download pipeline error: {}", e);
|
|
|
|
|
}
|
|
|
|
|
if let Err(e) = playback_res {
|
|
|
|
|
tracing::error!("✗ Playback pipeline error: {:?}", e);
|
|
|
|
|
} else if let Ok(Err(e)) = playback_res {
|
|
|
|
|
tracing::error!("✗ Playback pipeline error: {}", e);
|
|
|
|
|
}
|
|
|
|
|
tracing::error!("========================================");
|
|
|
|
|
return Err("Pipeline error".into());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|