Fin de la correction de l'implémentation par ChatGPT.
This commit is contained in:
@@ -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) ║
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user