debuggage des stream

This commit is contained in:
2025-11-14 10:43:53 +01:00
parent de84cbafbb
commit 1c2d30cbe9
44 changed files with 2018 additions and 717 deletions

View File

@@ -0,0 +1,12 @@
host:
http_port: '8080'
cover_cache:
directory: cache_covers
size: 2000
audio_cache:
directory: cache_audio
size: 500
logger:
buffer_capacity: 200
enable_console: true
min_level: INFO

View File

@@ -84,10 +84,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Block Information:");
println!(" Event ID: {}", block.event);
println!(" Songs: {}", block.song_count());
println!(
" Duration: {:.1} minutes",
block.length as f64 / 60000.0
);
println!(" Duration: {:.1} minutes", block.length as f64 / 60000.0);
println!();
// Afficher la liste des pistes

View File

@@ -46,7 +46,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.add_directive("pmoaudio_ext=debug".parse()?)
.add_directive("pmoplaylist=debug".parse()?)
.add_directive("pmoparadise=debug".parse()?)
.add_directive("pmoaudiocache=debug".parse()?)
.add_directive("pmoaudiocache=debug".parse()?),
)
.init();
@@ -89,7 +89,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialiser les caches et le gestionnaire de playlist
// ═══════════════════════════════════════════════════════════════════════════
let base_dir = std::env::var("PMO_CONFIG_DIR").unwrap_or_else(|_| "/tmp/pmomusic_test".to_string());
let base_dir =
std::env::var("PMO_CONFIG_DIR").unwrap_or_else(|_| "/tmp/pmomusic_test".to_string());
std::fs::create_dir_all(&base_dir)?;
tracing::info!("Initializing caches in: {}", base_dir);
@@ -130,8 +131,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing::info!("Creating playlist: {}", playlist_id);
// Créer une playlist éphémère (non persistante) pour cet exemple
let writer = playlist_manager.get_write_handle(playlist_id.clone()).await?;
writer.set_title(format!("Radio Paradise - Channel {}", channel_id)).await?;
let writer = playlist_manager
.get_write_handle(playlist_id.clone())
.await?;
writer
.set_title(format!("Radio Paradise - Channel {}", channel_id))
.await?;
writer.flush().await?; // Vider la playlist si elle existait
tracing::debug!("Playlist created and flushed");
@@ -178,7 +183,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Créer la source Radio Paradise
let mut download_source = RadioParadiseStreamSource::new(client);
download_source.push_block_id(block.event);
tracing::debug!("RadioParadiseStreamSource created with block {}", block.event);
tracing::debug!(
"RadioParadiseStreamSource created with block {}",
block.event
);
// Créer le sink de cache FLAC
let mut cache_sink = FlacCacheSink::new(audio_cache.clone(), cover_cache.clone());

View File

@@ -9,13 +9,13 @@
//!
//! Architecture:
//! ```text
//! RadioParadiseStreamSource → TimerNode → StreamingFlacSink
//! ↓
//! StreamHandle
//! ↓
//! pmoserver (Axum)
//! ↓
//! VLC / Media Player Client
//! RadioParadiseStreamSource → TimerBufferNode → StreamingFlacSink
//!
//! StreamHandle
//!
//! pmoserver (Axum)
//!
//! VLC / Media Player Client
//! ```
//!
//! Usage:
@@ -38,11 +38,11 @@ use axum::{
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use pmoaudio::{AudioPipelineNode, TimerNode};
use pmoaudio::{AudioPipelineNode, TimerBufferNode};
use pmoaudio_ext::{StreamingFlacSink, StreamingOggFlacSink};
use pmoflac::EncoderOptions;
use pmoparadise::{RadioParadiseClient, RadioParadiseStreamSource, END_OF_BLOCKS_SIGNAL};
use pmoserver::{ServerBuilder, init_logging};
use pmoserver::{init_logging, ServerBuilder};
use std::env;
use std::sync::Arc;
use tokio_util::io::ReaderStream;
@@ -210,25 +210,40 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut source_flac = RadioParadiseStreamSource::new(client.clone());
source_flac.push_block_id(block.event);
source_flac.push_block_id(END_OF_BLOCKS_SIGNAL); // Signal: no more blocks after this one
tracing::debug!("RadioParadiseStreamSource (FLAC) created with block {} + END signal", block.event);
tracing::debug!(
"RadioParadiseStreamSource (FLAC) created with block {} + END signal",
block.event
);
// Use SMALL channel size to make backpressure more reactive
// Instead of trying to buffer 3s of audio (60 chunks), use a much smaller buffer
// This forces tighter backpressure control
let max_lead_time = 3.0;
let channel_size = 8; // Small buffer for reactive backpressure
tracing::debug!("Using channel size: {} chunks ({:.1}s buffer at 50ms/chunk)", channel_size, channel_size as f64 * 0.05);
let buffer_sec = 10.0;
let max_lead_time = buffer_sec;
let channel_size = 512;
tracing::debug!(
"Using channel size: {} chunks ({:.1}s buffer à 50ms/chunk)",
channel_size,
channel_size as f64 * 0.05
);
let mut timer_flac = TimerNode::with_channel_size(max_lead_time, channel_size);
tracing::debug!("TimerNode (FLAC) created with {:.1}s max lead time, {} chunk buffer", max_lead_time, channel_size);
let mut timer_flac = TimerBufferNode::with_channel_size(buffer_sec, channel_size);
tracing::debug!(
"TimerBufferNode (FLAC) created with {:.1}s buffer, {} chunk queue",
buffer_sec,
channel_size
);
// StreamingFlacSink doesn't take channel_size - it uses bits_per_sample (16, 24, or 32)
let (streaming_sink, stream_handle) = StreamingFlacSink::new(encoder_options.clone(), 16);
let (streaming_sink, stream_handle) =
StreamingFlacSink::with_max_broadcast_lead(encoder_options.clone(), 16, max_lead_time);
tracing::debug!("StreamingFlacSink created");
timer_flac.register(Box::new(streaming_sink));
source_flac.register(Box::new(timer_flac));
tracing::info!("Pipeline 1 connected: RadioParadiseStreamSource → TimerNode → StreamingFlacSink");
tracing::info!(
"Pipeline 1 connected: RadioParadiseStreamSource → TimerBufferNode → StreamingFlacSink"
);
// ─────────────────────────────────────────────────────────────────────────
// Pipeline 2: OGG-FLAC streaming
@@ -237,18 +252,29 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut source_ogg = RadioParadiseStreamSource::new(client);
source_ogg.push_block_id(block.event);
source_ogg.push_block_id(END_OF_BLOCKS_SIGNAL); // Signal: no more blocks after this one
tracing::debug!("RadioParadiseStreamSource (OGG) created with block {} + END signal", block.event);
tracing::debug!(
"RadioParadiseStreamSource (OGG) created with block {} + END signal",
block.event
);
let mut timer_ogg = TimerNode::with_channel_size(max_lead_time, channel_size);
tracing::debug!("TimerNode (OGG) created with {:.1}s max lead time, {} chunk buffer", max_lead_time, channel_size);
let mut timer_ogg = TimerBufferNode::with_channel_size(buffer_sec, channel_size);
tracing::debug!(
"TimerBufferNode (OGG) created with {:.1}s buffer, {} chunk queue",
buffer_sec,
channel_size
);
// StreamingOggFlacSink doesn't take channel_size - it uses bits_per_sample (16, 24, or 32)
let (ogg_sink, ogg_handle) = StreamingOggFlacSink::new(encoder_options, 16);
let (ogg_sink, ogg_handle) =
StreamingOggFlacSink::with_max_broadcast_lead(encoder_options, 16, max_lead_time);
tracing::debug!("StreamingOggFlacSink created");
timer_ogg.register(Box::new(ogg_sink));
source_ogg.register(Box::new(timer_ogg));
tracing::info!("Pipeline 2 connected: RadioParadiseStreamSource → TimerNode → StreamingOggFlacSink");
tracing::info!(
"Pipeline 2 connected: RadioParadiseStreamSource → TimerBufferNode → StreamingOggFlacSink"
);
// ═══════════════════════════════════════════════════════════════════════════
// Setup pmoserver with streaming routes
@@ -256,8 +282,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing::info!("Setting up pmoserver...");
let mut server = ServerBuilder::new("RadioParadiseStreamTest", "http://localhost", 8080)
.build();
let mut server =
ServerBuilder::new("RadioParadiseStreamTest", "http://localhost", 8080).build();
let app_state = Arc::new(AppState {
stream_handle,
@@ -265,12 +291,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
});
// Add streaming routes
server.add_handler_with_state("/test/stream", stream_handler, app_state.clone()).await;
server.add_handler_with_state("/test/stream-icy", stream_icy_handler, app_state.clone()).await;
server.add_handler_with_state("/test/stream-ogg", stream_ogg_handler, app_state.clone()).await;
server
.add_handler_with_state("/test/stream", stream_handler, app_state.clone())
.await;
server
.add_handler_with_state("/test/stream-icy", stream_icy_handler, app_state.clone())
.await;
server
.add_handler_with_state("/test/stream-ogg", stream_ogg_handler, app_state.clone())
.await;
// Add metadata route
server.add_handler_with_state("/test/metadata", metadata_handler, app_state.clone()).await;
server
.add_handler_with_state("/test/metadata", metadata_handler, app_state.clone())
.await;
// Add health check
server.add_handler("/test/health", health_handler).await;

View File

@@ -140,7 +140,8 @@ impl RadioParadiseClient {
url.query_pairs_mut()
.append_pair("bitrate", "4") // FLAC lossless
.append_pair("info", "true")
.append_pair("channel", &self.channel.to_string());
// RP API expects `chan` rather than `channel` for channel selection.
.append_pair("chan", &self.channel.to_string());
if let Some(event_id) = event {
url.query_pairs_mut()

View File

@@ -91,13 +91,15 @@ impl NodeStats {
/// Enregistre l'envoi d'un segment
pub fn record_segment_sent(&self, bytes: usize) {
self.segments_sent.fetch_add(1, Ordering::Relaxed);
self.bytes_processed.fetch_add(bytes as u64, Ordering::Relaxed);
self.bytes_processed
.fetch_add(bytes as u64, Ordering::Relaxed);
}
/// Enregistre un événement de backpressure
pub fn record_backpressure(&self, duration_ms: u64) {
self.backpressure_blocks.fetch_add(1, Ordering::Relaxed);
self.backpressure_time_ms.fetch_add(duration_ms, Ordering::Relaxed);
self.backpressure_time_ms
.fetch_add(duration_ms, Ordering::Relaxed);
}
/// Retourne un rapport formaté des statistiques
@@ -112,7 +114,11 @@ impl NodeStats {
let first_ts = self.first_segment_timestamp.load(Ordering::Relaxed);
let last_ts = self.last_segment_timestamp.load(Ordering::Relaxed);
let first_ts_sec = if first_ts == u64::MAX { 0.0 } else { first_ts as f64 / 1000.0 };
let first_ts_sec = if first_ts == u64::MAX {
0.0
} else {
first_ts as f64 / 1000.0
};
let last_ts_sec = last_ts as f64 / 1000.0;
let audio_duration = last_ts_sec - first_ts_sec;
@@ -126,12 +132,27 @@ impl NodeStats {
Audio: {:.1}s (first: {:.1}s, last: {:.1}s) | Real-time ratio: {:.1}%\n\
Backpressure: {} blocks, {:.2}s total ({:.1}% of time)",
self.name,
elapsed, received, sent, received.saturating_sub(sent),
mb, throughput_mbps,
audio_duration, first_ts_sec, last_ts_sec,
if audio_duration > 0.0 { (elapsed / audio_duration) * 100.0 } else { 0.0 },
bp_blocks, bp_time_ms as f64 / 1000.0,
if elapsed > 0.0 { (bp_time_ms as f64 / 1000.0 / elapsed) * 100.0 } else { 0.0 }
elapsed,
received,
sent,
received.saturating_sub(sent),
mb,
throughput_mbps,
audio_duration,
first_ts_sec,
last_ts_sec,
if audio_duration > 0.0 {
(elapsed / audio_duration) * 100.0
} else {
0.0
},
bp_blocks,
bp_time_ms as f64 / 1000.0,
if elapsed > 0.0 {
(bp_time_ms as f64 / 1000.0 / elapsed) * 100.0
} else {
0.0
}
)
}
}

View File

@@ -97,7 +97,9 @@ impl RadioParadiseStreamSourceLogic {
block.length as f64 / 60000.0,
block.url
);
let response = self.client.client
let response = self
.client
.client
.get(&block.url)
.timeout(self.client.block_timeout)
.send()
@@ -125,9 +127,9 @@ impl RadioParadiseStreamSourceLogic {
// Créer un stream reader
tracing::debug!("Creating byte stream reader");
let byte_stream = response.bytes_stream().map(|result| {
result.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
});
let byte_stream = response
.bytes_stream()
.map(|result| result.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
let stream_reader = StreamReader::new(byte_stream);
tracing::debug!("Stream reader created");
@@ -140,7 +142,11 @@ impl RadioParadiseStreamSourceLogic {
let stream_info = decoder.info().clone();
let sample_rate = stream_info.sample_rate;
let bits_per_sample = stream_info.bits_per_sample;
tracing::debug!("FLAC decoder initialized: {}Hz, {} bits/sample", sample_rate, bits_per_sample);
tracing::debug!(
"FLAC decoder initialized: {}Hz, {} bits/sample",
sample_rate,
bits_per_sample
);
// Préparer les songs ordonnées pour tracking
let songs = block.songs_ordered();
@@ -165,13 +171,16 @@ impl RadioParadiseStreamSourceLogic {
// Envoyer TrackBoundary pour la première song AVANT le premier chunk audio
// Même si son elapsed > 0, cela garantit que FlacCacheSink a des métadonnées
// dès le début (sinon il attendrait indéfiniment un TrackBoundary)
let mut next_song: Option<(usize, &Song)> = if let Some((idx, song)) = songs.get(0).copied() {
tracing::debug!("Sending TrackBoundary for first song (idx={}, elapsed={}ms) at timestamp 0",
idx, song.elapsed);
let mut next_song: Option<(usize, &Song)> = if let Some((idx, song)) = songs.get(0).copied()
{
tracing::debug!(
"Sending TrackBoundary for first song (idx={}, elapsed={}ms) at timestamp 0",
idx,
song.elapsed
);
let metadata = song_to_metadata(song, block).await;
let track_boundary = AudioSegment::new_track_boundary(
*order,
0.0, // timestamp = 0 au début du stream
*order, 0.0, // timestamp = 0 au début du stream
metadata,
);
self.send_to_children(output, track_boundary).await?;
@@ -183,7 +192,6 @@ impl RadioParadiseStreamSourceLogic {
};
tracing::debug!("Starting audio chunk loop");
// Buffer pour lecture
let bytes_per_sample = (bits_per_sample / 8) as usize;
let frame_bytes = bytes_per_sample * 2; // stereo
@@ -196,6 +204,7 @@ impl RadioParadiseStreamSourceLogic {
let mut chunk_count = 0;
let mut total_bytes_decoded = 0u64;
let expected_duration_sec = block.length as f64 / 1000.0;
let mut stats_last_log = Instant::now();
loop {
// Vérifier stop_token
@@ -213,7 +222,9 @@ impl RadioParadiseStreamSourceLogic {
// Remplir le buffer
if pending.len() < chunk_byte_len {
let read = decoder.read(&mut read_buf).await
let read = decoder
.read(&mut read_buf)
.await
.map_err(|e| AudioError::ProcessingError(format!("Read error: {}", e)))?;
if read == 0 {
@@ -263,22 +274,35 @@ impl RadioParadiseStreamSourceLogic {
);
let metadata = song_to_metadata(song, block).await;
let timestamp_sec = total_samples as f64 / sample_rate as f64;
let track_boundary = AudioSegment::new_track_boundary(
*order,
timestamp_sec,
metadata,
);
let track_boundary =
AudioSegment::new_track_boundary(*order, timestamp_sec, metadata);
self.send_to_children(output, track_boundary).await?;
// Passer à la song suivante
song_index += 1;
next_song = songs.get(song_index).copied();
tracing::debug!("Moved to next song, song_index={}, next_song present={}", song_index, next_song.is_some());
tracing::debug!(
"Moved to next song, song_index={}, next_song present={}",
song_index,
next_song.is_some()
);
}
}
// Envoyer le chunk audio
let timestamp_sec = total_samples as f64 / sample_rate as f64;
if stats_last_log.elapsed() >= Duration::from_secs(1) {
let real_elapsed = start_instant.elapsed().as_secs_f64();
tracing::debug!(
"RP timing: chunk={} ts={:.3}s real_elapsed={:.3}s delta={:.3}s chunk_len={} frames",
chunk_count,
timestamp_sec,
real_elapsed,
timestamp_sec - real_elapsed,
chunk_len
);
stats_last_log = Instant::now();
}
let audio_segment = pcm_to_audio_segment(
&pcm_data,
*order,
@@ -295,7 +319,11 @@ impl RadioParadiseStreamSourceLogic {
// Retourner le timestamp du dernier chunk (durée totale du bloc) et l'instant de début
let final_timestamp = total_samples as f64 / sample_rate as f64;
tracing::debug!("Block decode complete: {} samples, {:.2}s duration", total_samples, final_timestamp);
tracing::debug!(
"Block decode complete: {} samples, {:.2}s duration",
total_samples,
final_timestamp
);
Ok((final_timestamp, start_instant))
}
@@ -312,7 +340,9 @@ impl RadioParadiseStreamSourceLogic {
let capacity_before = tx.capacity();
tracing::trace!(
"send_to_children: Sending to child {} (channel capacity={}, timestamp={:.3}s)",
i, capacity_before, segment.timestamp_sec
i,
capacity_before,
segment.timestamp_sec
);
let send_start = std::time::Instant::now();
@@ -325,8 +355,11 @@ impl RadioParadiseStreamSourceLogic {
let duration_ms = send_duration.as_millis() as u64;
self.stats.record_backpressure(duration_ms);
tracing::debug!(
"send_to_children: Send to child {} BLOCKED for {:.3}s (backpressure triggered, timestamp={:.3}s)",
i, send_duration.as_secs_f64(), segment.timestamp_sec
"send_to_children: Send to child {} BLOCKED for {:.3}s (channel capacity before send={}, timestamp={:.3}s)",
i,
send_duration.as_secs_f64(),
capacity_before,
segment.timestamp_sec
);
}
@@ -519,7 +552,10 @@ impl NodeLogic for RadioParadiseStreamSourceLogic {
output: Vec<mpsc::Sender<Arc<AudioSegment>>>,
stop_token: CancellationToken,
) -> Result<(), AudioError> {
tracing::debug!("RadioParadiseStreamSource::process() started, block_queue has {} items", self.block_queue.len());
tracing::debug!(
"RadioParadiseStreamSource::process() started, block_queue has {} items",
self.block_queue.len()
);
for (i, event_id) in self.block_queue.iter().enumerate() {
tracing::debug!(" block_queue[{}] = {}", i, event_id);
}
@@ -544,7 +580,9 @@ impl NodeLogic for RadioParadiseStreamSourceLogic {
// Vérifier si c'est le signal de fin
if id == END_OF_BLOCKS_SIGNAL {
tracing::info!("Received END_OF_BLOCKS_SIGNAL, finishing after current block");
tracing::info!(
"Received END_OF_BLOCKS_SIGNAL, finishing after current block"
);
break None;
}
@@ -573,10 +611,10 @@ impl NodeLogic for RadioParadiseStreamSourceLogic {
// Récupérer les métadonnées du bloc
tracing::debug!("Fetching block metadata for event_id {}...", event_id);
let block = self.client
.get_block(Some(event_id))
.await
.map_err(|e| AudioError::ProcessingError(format!("Failed to get block: {}", e)))?;
let block =
self.client.get_block(Some(event_id)).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to get block: {}", e))
})?;
tracing::debug!("Block metadata received: url={}", block.url);
// Marquer comme téléchargé
@@ -584,15 +622,24 @@ impl NodeLogic for RadioParadiseStreamSourceLogic {
// Télécharger et décoder le bloc
tracing::info!("Starting download and decode for block {}...", event_id);
let (block_duration, start_instant) = self.download_and_decode_block(&block, &output, &stop_token, &mut order)
let (block_duration, start_instant) = self
.download_and_decode_block(&block, &output, &stop_token, &mut order)
.await?;
last_timestamp = block_duration;
last_start_instant = Some(start_instant);
tracing::info!("Finished download and decode for block {} (duration: {:.2}s)", event_id, block_duration);
tracing::info!(
"Finished download and decode for block {} (duration: {:.2}s)",
event_id,
block_duration
);
}
// Envoyer EndOfStream avec le timestamp du dernier chunk
tracing::info!("Sending EndOfStream with timestamp {:.2}s to {} outputs", last_timestamp, output.len());
tracing::info!(
"Sending EndOfStream with timestamp {:.2}s to {} outputs",
last_timestamp,
output.len()
);
let eos = AudioSegment::new_end_of_stream(order, last_timestamp);
for tx in &output {
tx.send(eos.clone())
@@ -692,7 +739,8 @@ mod tests {
#[test]
fn test_cache_fifo_basic() {
let client = create_test_client();
let mut logic = RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
let mut logic =
RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
// Ajouter 5 blocs
for i in 1..=5 {
@@ -709,7 +757,8 @@ mod tests {
#[test]
fn test_cache_fifo_exactly_10_elements() {
let client = create_test_client();
let mut logic = RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
let mut logic =
RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
// Ajouter exactement 10 blocs
for i in 1..=10 {
@@ -717,7 +766,11 @@ mod tests {
}
// Vérifier qu'on a exactement 10 éléments
assert_eq!(logic.recent_blocks.len(), 10, "Cache should have exactly 10 elements");
assert_eq!(
logic.recent_blocks.len(),
10,
"Cache should have exactly 10 elements"
);
// Tous devraient être dans le cache
for i in 1..=10 {
@@ -728,7 +781,8 @@ mod tests {
#[test]
fn test_cache_fifo_eviction_oldest() {
let client = create_test_client();
let mut logic = RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
let mut logic =
RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
// Remplir le cache avec 10 éléments (1..=10)
for i in 1..=10 {
@@ -739,10 +793,17 @@ mod tests {
logic.mark_block_downloaded(11);
// Le cache doit toujours avoir 10 éléments
assert_eq!(logic.recent_blocks.len(), 10, "Cache should still have 10 elements");
assert_eq!(
logic.recent_blocks.len(),
10,
"Cache should still have 10 elements"
);
// Le premier (plus ancien) doit avoir été évincé
assert!(!logic.is_recent_block(1), "Oldest block (1) should be evicted");
assert!(
!logic.is_recent_block(1),
"Oldest block (1) should be evicted"
);
// Les éléments 2..=11 doivent être présents
for i in 2..=11 {
@@ -753,7 +814,8 @@ mod tests {
#[test]
fn test_cache_fifo_multiple_evictions() {
let client = create_test_client();
let mut logic = RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
let mut logic =
RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
// Remplir avec 10 éléments
for i in 1..=10 {
@@ -766,7 +828,11 @@ mod tests {
}
// Toujours 10 éléments
assert_eq!(logic.recent_blocks.len(), 10, "Cache should have 10 elements");
assert_eq!(
logic.recent_blocks.len(),
10,
"Cache should have 10 elements"
);
// Les 5 premiers doivent avoir été évincés
for i in 1..=5 {
@@ -782,7 +848,8 @@ mod tests {
#[test]
fn test_cache_never_exceeds_capacity() {
let client = create_test_client();
let mut logic = RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
let mut logic =
RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
// Vérifier la capacité pré-allouée
assert_eq!(logic.recent_blocks.capacity(), RECENT_BLOCKS_CACHE_SIZE);
@@ -812,7 +879,8 @@ mod tests {
#[test]
fn test_cache_fifo_order_preserved() {
let client = create_test_client();
let mut logic = RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
let mut logic =
RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
// Ajouter 10 éléments
for i in 1..=10 {
@@ -830,7 +898,8 @@ mod tests {
#[test]
fn test_block_queue_push() {
let client = create_test_client();
let mut logic = RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
let mut logic =
RadioParadiseStreamSourceLogic::new(client, DEFAULT_CHUNK_DURATION_MS as u32);
// Tester push_block_id
logic.push_block_id(100);

View File

@@ -58,8 +58,7 @@ impl RadioParadiseSource {
#[cfg(feature = "server")]
pub fn from_registry(_client: RadioParadiseClient) -> Result<Self> {
Err(MusicSourceError::SourceUnavailable(
"RadioParadiseSource is deprecated. Use RadioParadiseStreamSource instead."
.to_string(),
"RadioParadiseSource is deprecated. Use RadioParadiseStreamSource instead.".to_string(),
))
}
@@ -139,8 +138,7 @@ impl MusicSource for RadioParadiseSource {
async fn resolve_uri(&self, _object_id: &str) -> Result<String> {
Err(MusicSourceError::SourceUnavailable(
"RadioParadiseSource is deprecated. Use RadioParadiseStreamSource instead."
.to_string(),
"RadioParadiseSource is deprecated. Use RadioParadiseStreamSource instead.".to_string(),
))
}
@@ -150,8 +148,7 @@ impl MusicSource for RadioParadiseSource {
async fn append_track(&self, _track: Item) -> Result<()> {
Err(MusicSourceError::SourceUnavailable(
"RadioParadiseSource is deprecated and does not support FIFO operations."
.to_string(),
"RadioParadiseSource is deprecated and does not support FIFO operations.".to_string(),
))
}

View File

@@ -48,6 +48,7 @@ async fn test_get_current_block() {
.and(path("/api/get_block"))
.and(query_param("bitrate", "4"))
.and(query_param("info", "true"))
.and(query_param("chan", "0"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_block_json(1234, 5678)))
.mount(&mock_server)
.await;
@@ -82,6 +83,7 @@ async fn test_get_specific_block() {
.and(path("/api/get_block"))
.and(query_param("bitrate", "4"))
.and(query_param("info", "true"))
.and(query_param("chan", "0"))
.and(query_param("event", "5678"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_block_json(5678, 9012)))
.mount(&mock_server)
@@ -99,12 +101,38 @@ async fn test_get_specific_block() {
assert_eq!(block.end_event, 9012);
}
#[tokio::test]
async fn test_get_block_respects_channel() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/get_block"))
.and(query_param("bitrate", "4"))
.and(query_param("info", "true"))
.and(query_param("chan", "2"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_block_json(2222, 3333)))
.mount(&mock_server)
.await;
let client = RadioParadiseClient::builder()
.api_base(format!("{}/api", mock_server.uri()))
.channel(2)
.build()
.await
.unwrap();
let block = client.get_block(None).await.unwrap();
assert_eq!(block.event, 2222);
assert_eq!(block.end_event, 3333);
}
#[tokio::test]
async fn test_now_playing() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/get_block"))
.and(query_param("chan", "0"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_block_json(1234, 5678)))
.mount(&mock_server)
.await;
@@ -133,6 +161,8 @@ async fn test_prefetch_next() {
// First block
Mock::given(method("GET"))
.and(path("/api/get_block"))
.and(query_param("chan", "0"))
.and(query_param("event", "1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_block_json(1234, 5678)))
.mount(&mock_server)
@@ -140,6 +170,8 @@ async fn test_prefetch_next() {
// Next block
Mock::given(method("GET"))
.and(path("/api/get_block"))
.and(query_param("chan", "0"))
.and(query_param("event", "5678"))
.respond_with(ResponseTemplate::new(200).set_body_json(mock_block_json(5678, 9012)))
.mount(&mock_server)