debuggage des stream
This commit is contained in:
12
pmoparadise/.pmomusic/config.yaml
Normal file
12
pmoparadise/.pmomusic/config.yaml
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user