Tentative de gestion d'un historique

This commit is contained in:
2025-11-15 15:43:42 +01:00
parent 58c4383023
commit 97a383c079
25 changed files with 2195 additions and 912 deletions

View File

@@ -35,6 +35,7 @@ impl TrackBoundaryCoverNode {
}
}
#[async_trait::async_trait]
impl AudioPipelineNode for TrackBoundaryCoverNode {
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
self.inner.get_tx()

View File

@@ -6,7 +6,7 @@
//! - Paces broadcast to match audio playback rate
use std::time::Instant;
use tracing::{debug, info, warn};
use tracing::{trace, warn};
/// Error returned when a frame should be skipped (too late)
#[derive(Debug)]
@@ -56,7 +56,7 @@ impl BroadcastPacer {
let elapsed_since_start = self.start_time.elapsed().as_secs_f64();
if audio_timestamp < 0.1 && elapsed_since_start > 1.0 {
self.start_time = Instant::now();
info!(
trace!(
"{} broadcaster: TopZeroSync detected, resetting timer",
self.label
);
@@ -94,7 +94,7 @@ impl BroadcastPacer {
// Log pour info si on est très en avance, mais on ne dort PAS
if self.max_lead_time > 0.0 && lead_time > self.max_lead_time {
debug!(
trace!(
"{} broadcaster: lead_time={:.3}s > max={:.3}s (audio_ts={:.3}s, elapsed={:.3}s) - relying on natural backpressure",
self.label, lead_time, self.max_lead_time, audio_timestamp, elapsed
);

View File

@@ -200,6 +200,7 @@ impl NodeLogic for FlacCacheSinkLogic {
// Phase 1: Dispatcher jusqu'à ce que le prebuffer soit terminé
let mut end_of_stream_received = false;
let mut early_track_boundary_received = false;
let mut track_tx_opt = Some(track_tx);
let pk = loop {
tokio::select! {
@@ -221,9 +222,9 @@ impl NodeLogic for FlacCacheSinkLogic {
result = rx.recv() => {
match result {
Some(segment) => {
// Si EndOfStream a été reçu, ignorer tous les segments suivants
// Si EndOfStream ou TrackBoundary a été reçu, ignorer tous les segments suivants
// et continuer à attendre cache_future
if end_of_stream_received {
if end_of_stream_received || early_track_boundary_received {
continue;
}
@@ -239,10 +240,18 @@ impl NodeLogic for FlacCacheSinkLogic {
}
}
_AudioSegment::Sync(marker) => match &**marker {
SyncMarker::TrackBoundary { .. } => {
// TrackBoundary avant fin du prebuffer - track trop courte
tracing::error!("FlacCacheSink: TrackBoundary received before prebuffer complete - track too short");
return Err(AudioError::ProcessingError("Track too short for prebuffer".to_string()));
SyncMarker::TrackBoundary { metadata } => {
// TrackBoundary pendant le prebuffer - track courte (< 512KB)
tracing::warn!(
"FlacCacheSink: TrackBoundary received before prebuffer complete - track shorter than 512KB, closing pump and waiting for ingestion"
);
// Stocker les métadonnées pour la prochaine track
next_track_metadata = Some(metadata.clone());
// Fermer le track_tx pour que le pump se termine proprement
track_tx_opt = None;
// Marquer qu'on a reçu un TrackBoundary précoce
early_track_boundary_received = true;
// Continuer à attendre cache_future
}
SyncMarker::EndOfStream => {
tracing::debug!("FlacCacheSink: EndOfStream during prebuffer - closing pump and waiting for ingestion to complete");
@@ -262,7 +271,7 @@ impl NodeLogic for FlacCacheSinkLogic {
}
None => {
// EOF sur rx pendant le prebuffer - attendre que cache_future se termine
if !end_of_stream_received {
if !end_of_stream_received && !early_track_boundary_received {
tracing::debug!("FlacCacheSink: EOF on rx during prebuffer, waiting for ingestion to complete");
track_tx_opt = None;
end_of_stream_received = true;
@@ -296,26 +305,62 @@ impl NodeLogic for FlacCacheSinkLogic {
))
})?;
let url = match dest_metadata.read().await.get_cover_url().await {
Ok(url) => {
tracing::debug!("FlacCacheSink: Got cover URL for pk {}: {:?}", pk, url);
url
let cover_pk_present = match dest_metadata.read().await.get_cover_pk().await {
Ok(Some(existing_pk)) => {
tracing::debug!(
"FlacCacheSink: cover_pk already set for audio asset {} ({})",
pk,
existing_pk
);
true
}
Ok(None) => false,
Err(e) if e.is_transient() => {
tracing::debug!(
"FlacCacheSink: Transient error getting cover URL for pk {}: {}",
"FlacCacheSink: Transient error getting cover_pk for pk {}: {}",
pk,
e
);
None
false
}
Err(e) => {
tracing::warn!(
"FlacCacheSink: Cannot obtain cover URL for audio asset {}: {}",
"FlacCacheSink: Cannot obtain cover_pk for audio asset {}: {}",
pk,
e
);
None
false
}
};
let url = if cover_pk_present {
None
} else {
match dest_metadata.read().await.get_cover_url().await {
Ok(url) => {
tracing::debug!(
"FlacCacheSink: Got cover URL for pk {}: {:?}",
pk,
url
);
url
}
Err(e) if e.is_transient() => {
tracing::debug!(
"FlacCacheSink: Transient error getting cover URL for pk {}: {}",
pk,
e
);
None
}
Err(e) => {
tracing::warn!(
"FlacCacheSink: Cannot obtain cover URL for audio asset {}: {}",
pk,
e
);
None
}
}
};
@@ -378,6 +423,16 @@ impl NodeLogic for FlacCacheSinkLogic {
continue; // Passer à la track suivante (qui n'arrivera pas car EndOfStream)
}
// Si TrackBoundary précoce a été reçu pendant le prebuffer, passer à la track suivante
if early_track_boundary_received {
tracing::debug!(
"FlacCacheSink: TrackBoundary was received during prebuffer, track complete, moving to next track"
);
drop(pump_handle);
track_number += 1;
continue; // Passer à la track suivante (métadonnées déjà stockées dans next_track_metadata)
}
// Phase 3: Continuer à dispatcher jusqu'au TrackBoundary
tracing::debug!(
"FlacCacheSink: Continuing dispatch until TrackBoundary (pump runs in background)"

View File

@@ -79,7 +79,7 @@ use pmometadata::TrackMetadata;
use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
use tokio::sync::{mpsc, RwLock};
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, trace, warn};
use tracing::{debug, error, trace, warn};
/// Default ICY metadata interval (bytes of audio between metadata blocks).
/// Standard value used by most streaming servers.
@@ -291,7 +291,7 @@ impl AsyncRead for FlacClientStream {
if let Some(header) = header_opt {
self.buffer.extend(header.iter());
info!(
debug!(
"Sending cached FLAC header to new client ({} bytes)",
header.len()
);
@@ -356,10 +356,10 @@ impl Drop for FlacClientStream {
if count == 1 {
if self.handle.auto_stop.load(Ordering::SeqCst) {
info!("Last client disconnected, signaling pipeline stop");
debug!("Last client disconnected, signaling pipeline stop");
self.handle.stop_token.cancel();
} else {
info!("Last client disconnected, keeping pipeline alive");
debug!("Last client disconnected, keeping pipeline alive");
}
}
}
@@ -465,7 +465,7 @@ impl AsyncRead for IcyClientStream {
if let Some(header) = header_opt {
self.buffer.extend(header.iter());
info!(
debug!(
"Sending cached FLAC header to new ICY client ({} bytes)",
header.len()
);
@@ -568,10 +568,10 @@ impl Drop for IcyClientStream {
if count == 1 {
if self.handle.auto_stop.load(Ordering::SeqCst) {
info!("Last client disconnected, signaling pipeline stop");
debug!("Last client disconnected, signaling pipeline stop");
self.handle.stop_token.cancel();
} else {
info!("Last client disconnected, keeping pipeline alive");
debug!("Last client disconnected, keeping pipeline alive");
}
}
}
@@ -603,7 +603,7 @@ impl StreamingFlacSinkLogic {
return Ok(()); // Already initialized
}
info!(
debug!(
"Initializing FLAC encoder with sample rate: {} Hz",
sample_rate
);
@@ -634,7 +634,7 @@ impl StreamingFlacSinkLogic {
AudioError::ProcessingError(format!("Failed to start FLAC encoder: {}", e))
})?;
info!("FLAC encoder initialized successfully");
debug!("FLAC encoder initialized successfully");
// Spawn broadcaster task with timestamp for pacing
let flac_broadcast = self.flac_broadcast.clone();
@@ -656,7 +656,7 @@ impl StreamingFlacSinkLogic {
self.encoder_state = Some(EncoderState { broadcaster_task });
info!("Broadcaster task spawned");
debug!("Broadcaster task spawned");
Ok(())
}
@@ -716,7 +716,7 @@ impl NodeLogic for StreamingFlacSinkLogic {
AudioError::ProcessingError("StreamingFlacSink requires an input".into())
})?;
info!("StreamingFlacSink started");
debug!("StreamingFlacSink started");
// We'll initialize the encoder lazily when we get the first chunk
// For now, just process segments
@@ -724,7 +724,7 @@ impl NodeLogic for StreamingFlacSinkLogic {
loop {
tokio::select! {
_ = stop_token.cancelled() => {
info!("StreamingFlacSink stopped by cancellation");
debug!("StreamingFlacSink stopped by cancellation");
break;
}
@@ -737,7 +737,7 @@ impl NodeLogic for StreamingFlacSinkLogic {
if self.sample_rate.is_none() {
let sample_rate = chunk.sample_rate();
self.sample_rate = Some(sample_rate);
info!("Detected sample rate: {} Hz", sample_rate);
debug!("Detected sample rate: {} Hz", sample_rate);
// Initialize the FLAC encoder now
self.initialize_encoder(sample_rate).await?;
@@ -765,11 +765,11 @@ impl NodeLogic for StreamingFlacSinkLogic {
}
let send_duration = send_start.elapsed();
if send_duration.as_millis() >= 50 {
debug!(
"StreamingFlacSink: pcm_tx send blocked for {:.3}s (ts={:.3}s)",
send_duration.as_secs_f64(),
seg.timestamp_sec
);
trace!(
"StreamingFlacSink: pcm_tx send blocked for {:.3}s (ts={:.3}s)",
send_duration.as_secs_f64(),
seg.timestamp_sec
);
}
}
@@ -782,7 +782,7 @@ impl NodeLogic for StreamingFlacSinkLogic {
}
SyncMarker::EndOfStream => {
info!("End of stream marker received");
debug!("End of stream marker received");
break;
}
@@ -800,7 +800,7 @@ impl NodeLogic for StreamingFlacSinkLogic {
}
None => {
info!("Input channel closed");
debug!("Input channel closed");
break;
}
}
@@ -808,12 +808,12 @@ impl NodeLogic for StreamingFlacSinkLogic {
}
}
info!("StreamingFlacSink processing complete");
debug!("StreamingFlacSink processing complete");
Ok(())
}
async fn cleanup(&mut self, reason: StopReason) -> Result<(), AudioError> {
info!("StreamingFlacSink cleanup: {:?}", reason);
debug!("StreamingFlacSink cleanup: {:?}", reason);
Ok(())
}
}
@@ -828,7 +828,7 @@ async fn broadcast_flac_stream(
current_timestamp: Arc<RwLock<f64>>,
broadcast_max_lead_time: f64,
) -> Result<(), AudioError> {
info!(
trace!(
"Broadcaster task started with FLAC frame boundary detection (max_lead={:.3}s)",
broadcast_max_lead_time
);
@@ -861,7 +861,7 @@ async fn broadcast_flac_stream(
break;
}
}
info!("FLAC encoder stream ended, total bytes: {}", total_bytes);
trace!("FLAC encoder stream ended, total bytes: {}", total_bytes);
break;
}
Ok(n) => {
@@ -870,7 +870,7 @@ async fn broadcast_flac_stream(
total_read_time += read_duration;
if read_duration > 0.01 {
debug!(
trace!(
"FLAC: flac_stream.read() took {:.3}s for {} bytes (avg: {:.3}s over {} reads)",
read_duration,
n,
@@ -924,7 +924,7 @@ async fn broadcast_flac_stream(
let audio_timestamp = *current_timestamp.read().await;
if stats_last_log.elapsed() >= Duration::from_secs(1) {
debug!(
trace!(
"Broadcaster pacing snapshot: audio_ts={:.3}s buffer_bytes={}",
audio_timestamp,
accumulator.len()
@@ -951,7 +951,7 @@ async fn broadcast_flac_stream(
// Log if interval is unusual (too short = burst, too long = stall)
if broadcast_interval < 0.01 || broadcast_interval > 0.1 {
debug!(
trace!(
"FLAC: broadcast interval {:.3}s ({}ms) - size={} bytes (count={})",
broadcast_interval,
(broadcast_interval * 1000.0) as u32,
@@ -962,7 +962,7 @@ async fn broadcast_flac_stream(
// Periodic stats
if broadcast_count % 100 == 0 {
debug!(
trace!(
"FLAC: {} broadcasts sent, accumulator={} bytes remaining",
broadcast_count,
accumulator.len()
@@ -973,7 +973,7 @@ async fn broadcast_flac_stream(
if !header_captured && bytes.len() >= 4 && &bytes[0..4] == b"fLaC" {
*header_cache.write().await = Some(bytes.clone());
header_captured = true;
info!("FLAC header captured ({} bytes)", bytes.len());
trace!("FLAC header captured ({} bytes)", bytes.len());
}
let num_receivers = broadcast_tx.receiver_count();
@@ -1013,7 +1013,7 @@ async fn broadcast_flac_stream(
)));
}
info!("Broadcaster task completed successfully");
trace!("Broadcaster task completed successfully");
Ok(())
}
@@ -1062,7 +1062,7 @@ impl StreamingFlacSink {
// Calculate broadcast capacity based on max_lead_time
let broadcast_capacity = calculate_broadcast_capacity(broadcast_max_lead_time);
info!(
debug!(
"StreamingFlacSink: using broadcast capacity of {} items (max_lead_time={:.1}s)",
broadcast_capacity, broadcast_max_lead_time
);

View File

@@ -191,7 +191,7 @@ impl AsyncRead for OggFlacClientStream {
if let Some(header) = header_opt {
self.buffer.extend(header.iter());
info!(
debug!(
"Sending cached OGG-FLAC header to new client ({} bytes)",
header.len()
);
@@ -255,10 +255,10 @@ impl Drop for OggFlacClientStream {
if count == 1 {
if self.handle.auto_stop.load(Ordering::SeqCst) {
info!("Last OGG-FLAC client disconnected, signaling pipeline stop");
debug!("Last OGG-FLAC client disconnected, signaling pipeline stop");
self.handle.stop_token.cancel();
} else {
info!("Last OGG-FLAC client disconnected, keeping pipeline alive");
debug!("Last OGG-FLAC client disconnected, keeping pipeline alive");
}
}
}
@@ -290,7 +290,7 @@ impl StreamingOggFlacSinkLogic {
return Ok(()); // Already initialized
}
info!(
debug!(
"Initializing OGG-FLAC encoder with sample rate: {} Hz",
sample_rate
);
@@ -321,7 +321,7 @@ impl StreamingOggFlacSinkLogic {
AudioError::ProcessingError(format!("Failed to start FLAC encoder: {}", e))
})?;
info!("OGG-FLAC encoder initialized successfully");
debug!("OGG-FLAC encoder initialized successfully");
// Spawn OGG wrapper + broadcaster task with timestamp for pacing
let ogg_broadcast = self.ogg_broadcast.clone();
@@ -343,7 +343,7 @@ impl StreamingOggFlacSinkLogic {
self.encoder_state = Some(EncoderState { broadcaster_task });
info!("OGG broadcaster task spawned");
debug!("OGG broadcaster task spawned");
Ok(())
}
@@ -402,7 +402,7 @@ impl NodeLogic for StreamingOggFlacSinkLogic {
AudioError::ProcessingError("StreamingOggFlacSink requires an input".into())
})?;
info!("StreamingOggFlacSink started");
debug!("StreamingOggFlacSink started");
// TODO: Implement OGG-FLAC encoding logic
// For now, just process segments without encoding
@@ -410,7 +410,7 @@ impl NodeLogic for StreamingOggFlacSinkLogic {
loop {
tokio::select! {
_ = stop_token.cancelled() => {
info!("StreamingOggFlacSink stopped by cancellation");
debug!("StreamingOggFlacSink stopped by cancellation");
break;
}
@@ -423,7 +423,7 @@ impl NodeLogic for StreamingOggFlacSinkLogic {
if self.sample_rate.is_none() {
let sample_rate = chunk.sample_rate();
self.sample_rate = Some(sample_rate);
info!("Detected sample rate: {} Hz", sample_rate);
debug!("Detected sample rate: {} Hz", sample_rate);
// Initialize the FLAC encoder now
self.initialize_encoder(sample_rate).await?;
@@ -460,7 +460,7 @@ impl NodeLogic for StreamingOggFlacSinkLogic {
}
SyncMarker::EndOfStream => {
info!("End of stream marker received");
debug!("End of stream marker received");
break;
}
@@ -478,7 +478,7 @@ impl NodeLogic for StreamingOggFlacSinkLogic {
}
None => {
info!("Input channel closed");
debug!("Input channel closed");
break;
}
}
@@ -486,12 +486,12 @@ impl NodeLogic for StreamingOggFlacSinkLogic {
}
}
info!("StreamingOggFlacSink processing complete");
debug!("StreamingOggFlacSink processing complete");
Ok(())
}
async fn cleanup(&mut self, reason: StopReason) -> Result<(), AudioError> {
info!("StreamingOggFlacSink cleanup: {:?}", reason);
debug!("StreamingOggFlacSink cleanup: {:?}", reason);
Ok(())
}
}
@@ -545,7 +545,7 @@ impl StreamingOggFlacSink {
// Broadcast channel for OGG-FLAC bytes
// Capacity calculated from max_lead_time to ensure enough buffering
let broadcast_capacity = calculate_broadcast_capacity(broadcast_max_lead_time);
tracing::debug!(
tracing::trace!(
"OGG-FLAC broadcast capacity: {} items (for {:.1}s max lead time)",
broadcast_capacity,
broadcast_max_lead_time
@@ -786,7 +786,7 @@ async fn broadcast_ogg_flac_stream(
current_timestamp: Arc<RwLock<f64>>,
broadcast_max_lead_time: f64,
) -> Result<(), AudioError> {
info!(
trace!(
"OGG-FLAC broadcaster task started with FLAC frame boundary detection (max_lead={:.3}s)",
broadcast_max_lead_time
);
@@ -807,16 +807,16 @@ async fn broadcast_ogg_flac_stream(
// Step 1: Read FLAC header (fLaC + metadata blocks)
let flac_header = read_flac_header(&mut flac_stream).await?;
info!("Read FLAC header: {} bytes", flac_header.len());
trace!("Read FLAC header: {} bytes", flac_header.len());
// Extract sample rate from STREAMINFO for granule position calculation
let sample_rate = extract_sample_rate_from_streaminfo(&flac_header)?;
info!("Extracted sample rate from STREAMINFO: {} Hz", sample_rate);
trace!("Extracted sample rate from STREAMINFO: {} Hz", sample_rate);
// Step 2: Create OGG-FLAC identification packet (BOS)
// Format according to https://xiph.org/flac/ogg_mapping.html
let ogg_flac_id = create_ogg_flac_identification(&flac_header)?;
info!(
trace!(
"Created OGG-FLAC identification packet: {} bytes",
ogg_flac_id.len()
);
@@ -835,7 +835,7 @@ async fn broadcast_ogg_flac_stream(
cached_header.extend_from_slice(&comment_bytes);
*header_cache.write().await = Some(Bytes::from(cached_header));
header_captured = true;
info!(
trace!(
"OGG-FLAC header cached ({} bytes: BOS + Vorbis Comment)",
bos_bytes.len() + comment_bytes.len()
);
@@ -871,7 +871,7 @@ async fn broadcast_ogg_flac_stream(
trace!("Broadcast closed before sending final EOS page");
break;
}
info!(
trace!(
"Sent final EOS page with {} bytes of data",
flac_accumulator.len()
);
@@ -885,10 +885,10 @@ async fn broadcast_ogg_flac_stream(
trace!("Broadcast closed before sending empty EOS page");
break;
}
info!("Sent empty EOS page");
trace!("Sent empty EOS page");
}
info!(
trace!(
"OGG-FLAC stream ended, total OGG bytes: {}",
total_ogg_bytes
);
@@ -900,7 +900,7 @@ async fn broadcast_ogg_flac_stream(
total_read_time += read_duration;
if read_duration > 0.01 {
debug!(
trace!(
"OGG: flac_stream.read() took {:.3}s for {} bytes (avg: {:.3}s over {} reads)",
read_duration,
n,
@@ -1002,7 +1002,7 @@ async fn broadcast_ogg_flac_stream(
// Log if interval is unusual (too short = burst, too long = stall)
if broadcast_interval < 0.01 || broadcast_interval > 0.1 {
debug!(
trace!(
"OGG: broadcast interval {:.3}s ({}ms) - frame_size={} bytes, samples={} (count={})",
broadcast_interval,
(broadcast_interval * 1000.0) as u32,
@@ -1014,7 +1014,7 @@ async fn broadcast_ogg_flac_stream(
// Periodic stats
if broadcast_count % 100 == 0 {
debug!(
trace!(
"OGG: {} broadcasts sent, avg_interval={:.3}s, accumulator={} bytes",
broadcast_count,
last_broadcast_time.elapsed().as_secs_f64() / broadcast_count as f64,
@@ -1053,7 +1053,7 @@ async fn broadcast_ogg_flac_stream(
)));
}
info!("OGG-FLAC broadcaster task completed successfully");
trace!("OGG-FLAC broadcaster task completed successfully");
Ok(())
}
@@ -1177,7 +1177,7 @@ fn create_ogg_flac_identification(flac_header: &[u8]) -> Result<Vec<u8>, AudioEr
let block_length =
u32::from_be_bytes([0, flac_header[5], flac_header[6], flac_header[7]]) as usize;
info!("STREAMINFO block_length = {} bytes", block_length);
trace!("STREAMINFO block_length = {} bytes", block_length);
// STREAMINFO should be exactly 34 bytes of data
if block_length != 34 {
@@ -1194,7 +1194,7 @@ fn create_ogg_flac_identification(flac_header: &[u8]) -> Result<Vec<u8>, AudioEr
// Extract just the STREAMINFO block (type + length + data)
let streaminfo = &flac_header[4..4 + streaminfo_size];
info!(
trace!(
"Extracted STREAMINFO: {} bytes (type+length+data)",
streaminfo.len()
);

View File

@@ -15,7 +15,7 @@ use std::{
};
use tokio::sync::Notify;
use tracing::warn;
use tracing::{trace, warn};
/// Paquet diffusé contenant la charge utile + méta timing.
#[derive(Clone)]
@@ -99,7 +99,7 @@ impl<T> State<T> {
}
}
if purged > 0 {
tracing::debug!(
trace!(
"TimedBroadcast: purged {} expired packet(s) (head_seq={})",
purged,
self.head_seq