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

@@ -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(),
))
}