debuggage des stream
This commit is contained in:
@@ -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(),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user