Fin de la correction de l'implémentation par ChatGPT.

This commit is contained in:
2025-11-16 08:34:33 +01:00
parent c81a4651d6
commit 66416dafa8
20 changed files with 567 additions and 131 deletions

View File

@@ -20,6 +20,8 @@ pub struct BroadcastPacer {
max_lead_time: f64,
/// Label for logging (e.g., "FLAC" or "OGG")
label: String,
/// Pending reset flag - will reset timer on next chunk
pending_reset: bool,
}
impl BroadcastPacer {
@@ -34,15 +36,17 @@ impl BroadcastPacer {
start_time: Instant::now(),
max_lead_time: max_lead_time.max(0.0),
label: label.into(),
pending_reset: false,
}
}
/// Check timing and apply pacing
///
/// This function:
/// 1. Detects TopZeroSync (audio_timestamp < 0.1 after >1s) and resets timer
/// 2. Drops frames that are late (audio_ts < elapsed)
/// 3. Sleeps if too far ahead (lead_time > max_lead_time)
/// 1. Detects TopZeroSync (audio_timestamp < 0.1 after >1s) and marks pending reset
/// 2. On next chunk, resets timer with elapsed=0 guarantee
/// 3. Drops frames that are late (audio_ts < elapsed)
/// 4. Sleeps if too far ahead (lead_time > max_lead_time)
///
/// # Returns
///
@@ -50,28 +54,43 @@ impl BroadcastPacer {
/// - `Err(SkipFrame)` if frame is too late and should be dropped
pub async fn check_and_pace(&mut self, audio_timestamp: f64) -> Result<(), SkipFrame> {
// ╔═══════════════════════════════════════════════════════════════╗
// ║ 1. DÉTECTION TopZeroSync
// ║ Si le timestamp revient proche de 0, reset l'horloge
// ║ 1. DÉTECTION timestamp proche de 0 → marquer reset
// ║ Quand timestamp < 0.1s, c'est un nouveau morceau
// ╚═══════════════════════════════════════════════════════════════╝
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();
if audio_timestamp < 0.1 && !self.pending_reset {
trace!(
"{} broadcaster: TopZeroSync detected, resetting timer",
"{} broadcaster: Timestamp near zero detected, will reset timer on next chunk",
self.label
);
self.pending_reset = true;
}
// ╔═══════════════════════════════════════════════════════════════╗
// ║ 2. CALCUL DU LEAD TIME
// ║ 2. RESET TIMER si pending
// ║ Le reset se fait AVANT le calcul d'elapsed pour garantir ║
// ║ elapsed=0 pour le premier chunk du nouveau morceau ║
// ╚═══════════════════════════════════════════════════════════════╝
let elapsed = if self.pending_reset {
self.start_time = Instant::now();
self.pending_reset = false;
trace!(
"{} broadcaster: Timer reset at audio_ts={:.3}s",
self.label, audio_timestamp
);
0.0 // Garantit elapsed=0 pour ce chunk
} else {
self.start_time.elapsed().as_secs_f64()
};
// ╔═══════════════════════════════════════════════════════════════╗
// ║ 3. CALCUL DU LEAD TIME ║
// ║ lead_time > 0 : en avance (OK) ║
// ║ lead_time < 0 : en retard (SKIP) ║
// ╚═══════════════════════════════════════════════════════════════╝
let elapsed = self.start_time.elapsed().as_secs_f64();
let lead_time = audio_timestamp - elapsed;
// ╔═══════════════════════════════════════════════════════════════╗
// ║ 3. DROP FRAMES EN RETARD (tolérance zéro)
// ║ 4. DROP FRAMES EN RETARD
// ╚═══════════════════════════════════════════════════════════════╝
if lead_time < 0.0 {
warn!(
@@ -82,7 +101,7 @@ impl BroadcastPacer {
}
// ╔═══════════════════════════════════════════════════════════════╗
// ║ 4. BACKPRESSURE NATURELLE - Pas de sleep ! ║
// ║ 5. BACKPRESSURE NATURELLE - Pas de sleep ! ║
// ║ ║
// ║ Le pacing vient de : ║
// ║ - TimerBufferNode en amont (envoi régulier à 50ms/chunk) ║

View File

@@ -305,6 +305,10 @@ impl<T> Sender<T> {
}
/// Marque un TopZero : incrémente l'epoch pour les paquets suivants.
///
/// Reset le timer epoch_start sans effacer le buffer. Les paquets
/// du morceau précédent continueront à être distribués naturellement.
/// Cela évite de perdre les dernières frames FLAC à la transition entre morceaux.
pub fn mark_top_zero(&self) {
let mut state = self
.inner
@@ -313,11 +317,8 @@ impl<T> Sender<T> {
.expect("timed broadcast mutex poisoned");
state.epoch = state.epoch.wrapping_add(1);
state.epoch_start = Instant::now();
if !state.buffer.is_empty() {
state.head_seq = state.next_seq;
state.buffer.clear();
self.inner.space_notify.notify_waiters();
}
// Ne PAS effacer le buffer - laisser les paquets du morceau précédent
// se vider naturellement pour éviter de perdre les dernières frames
}
/// Nombre actuel de receivers abonnés.

View File

@@ -212,7 +212,13 @@ impl NodeLogic for PlaylistSourceLogic {
t
},
Ok(None) => {
// Playlist vide, attendre avant retry
// Playlist vide, attendre avant retry et réinitialiser la synchro
if !first_track {
tracing::debug!(
"PlaylistSourceLogic: playlist drained, resetting top-zero sync"
);
}
first_track = true;
tracing::trace!(
"PlaylistSourceLogic: playlist empty, waiting {}ms",
self.poll_interval_ms
@@ -237,14 +243,6 @@ impl NodeLogic for PlaylistSourceLogic {
}
};
// Émettre TopZeroSync pour la première piste seulement
if first_track {
tracing::debug!("PlaylistSourceLogic: emitting TopZeroSync");
let top_zero = AudioSegment::new_top_zero_sync();
send_to_children!(top_zero);
first_track = false;
}
// Émettre TrackBoundary avec metadata du cache
let metadata = match track.track_metadata() {
Ok(m) => m,
@@ -257,6 +255,28 @@ impl NodeLogic for PlaylistSourceLogic {
}
};
let metadata_guard = metadata.read().await;
let artist = metadata_guard
.get_artist()
.await
.ok()
.flatten()
.unwrap_or_else(|| "Unknown artist".to_string());
let title = metadata_guard
.get_title()
.await
.ok()
.flatten()
.unwrap_or_else(|| "Untitled".to_string());
drop(metadata_guard);
let remaining = self.playlist_handle.remaining().await.unwrap_or(0);
tracing::info!(
"PlaylistSource: starting track {} - {} ({} remaining)",
artist,
title,
remaining
);
tracing::debug!("PlaylistSourceLogic: emitting TrackBoundary");
let boundary = AudioSegment::new_track_boundary(0, 0.0, metadata);
send_to_children!(boundary);
@@ -278,6 +298,10 @@ impl NodeLogic for PlaylistSourceLogic {
// Décoder et émettre les chunks PCM
// Passer le cache et pk pour gérer le cache progressif
let cache_pk = track.cache_pk();
// Réinitialiser la synchro au début de chaque piste
let emit_top_zero = true;
first_track = false;
match decode_and_emit_track(
&file_path,
self.chunk_frames,
@@ -285,10 +309,12 @@ impl NodeLogic for PlaylistSourceLogic {
&stop_token,
&self.cache,
cache_pk,
emit_top_zero,
)
.await
{
Ok(()) => {
tracing::info!("PlaylistSource: finished track {} - {}", artist, title);
// Piste décodée avec succès, transférer vers l'historique si configuré
if let Some(ref history) = self.history_playlist {
if let Err(e) = history.push(cache_pk.to_string()).await {
@@ -306,7 +332,8 @@ impl NodeLogic for PlaylistSourceLogic {
}
Err(e) => {
tracing::error!("PlaylistSourceLogic: error decoding track: {}", e);
let error_marker = AudioSegment::new_error(0, 0.0, format!("Decode error: {}", e));
let error_marker =
AudioSegment::new_error(0, 0.0, format!("Decode error: {}", e));
send_to_children!(error_marker);
// Continue vers la piste suivante
}
@@ -335,6 +362,7 @@ async fn decode_and_emit_track(
stop_token: &CancellationToken,
cache: &Arc<AudioCache>,
cache_pk: &str,
emit_top_zero: bool,
) -> Result<(), AudioError> {
// Attendre que le fichier soit suffisamment gros pour le sniffing
// Le cache progressif permet de commencer la lecture après le prebuffer (512 KB)
@@ -460,6 +488,16 @@ async fn decode_and_emit_track(
timestamp_sec,
)?;
if emit_top_zero && total_frames == 0 {
tracing::debug!("decode_and_emit_track: emitting TopZeroSync (first chunk)");
let top_zero = AudioSegment::new_top_zero_sync();
for tx in output {
tx.send(top_zero.clone())
.await
.map_err(|_| AudioError::ChildDied)?;
}
}
for tx in output {
tx.send(segment.clone())
.await
@@ -493,6 +531,13 @@ async fn decode_and_emit_track(
.await
.map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?;
if !cache.is_download_complete(cache_pk) {
tracing::warn!(
"PlaylistSource: finished reading cache entry {} but download is not complete",
cache_pk
);
}
Ok(())
}