Merge pull request #30 from coissac/claude/optimize-prebuffer-playlist-011CUtQ8GG3eu5ao2bZP5W3x
Claude/optimize prebuffer playlist 011 c ut q8 gg3eu5ao2b zp5 w3x
This commit is contained in:
@@ -133,9 +133,24 @@ impl NodeLogic for FlacFileSinkLogic {
|
||||
AudioError::ProcessingError(format!("Failed to create {:?}: {}", track_path, e))
|
||||
})?;
|
||||
|
||||
// Exécuter pump et copy en parallèle avec tokio::select! en boucle
|
||||
let pump_future =
|
||||
pump_track_segments(first_segment, &mut rx, pcm_tx, bits_per_sample, sample_rate, &stop_token);
|
||||
// Créer un channel dédié pour dispatcher les chunks vers ce pump
|
||||
let (track_tx, track_rx) = mpsc::channel::<Arc<AudioSegment>>(16);
|
||||
|
||||
// Lancer le pump en arrière-plan avec son channel dédié
|
||||
// Cela permet à plusieurs pumps de tourner simultanément (cache progressive compliant)
|
||||
let pump_handle = tokio::spawn(pump_track_segments_from_channel(
|
||||
first_segment,
|
||||
track_rx,
|
||||
pcm_tx,
|
||||
bits_per_sample,
|
||||
sample_rate,
|
||||
));
|
||||
|
||||
// Dispatcher les segments vers track_tx en parallèle de l'écriture du fichier
|
||||
// Utiliser tokio::select! pour éviter le deadlock et permettre cache progressif
|
||||
tracing::debug!("FlacFileSink: Starting dispatcher loop with file write");
|
||||
|
||||
// Pin la future pour pouvoir l'utiliser dans select!
|
||||
let copy_future = async {
|
||||
let copy_result = tokio::io::copy(&mut flac_stream, &mut output).await;
|
||||
let flush_result = output.flush().await;
|
||||
@@ -150,22 +165,118 @@ impl NodeLogic for FlacFileSinkLogic {
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Encoder failed: {}", e)))?;
|
||||
Ok::<_, AudioError>(())
|
||||
};
|
||||
tokio::pin!(copy_future);
|
||||
|
||||
// Attendre les deux tâches en parallèle
|
||||
let (copy_result, pump_result) = tokio::join!(copy_future, pump_future);
|
||||
copy_result?;
|
||||
let stop_reason = pump_result?;
|
||||
// Phase 1: Dispatcher jusqu'à ce que le fichier soit complètement écrit
|
||||
let mut copy_done = false;
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Attendre l'écriture du fichier
|
||||
result = &mut copy_future, if !copy_done => {
|
||||
result?;
|
||||
tracing::info!("FlacFileSink: File write complete for track {}", track_number);
|
||||
copy_done = true;
|
||||
// Continue dispatching jusqu'au TrackBoundary
|
||||
}
|
||||
|
||||
// Vérifier le stop_reason pour savoir si on continue
|
||||
match stop_reason {
|
||||
StopReason::TrackBoundary(_metadata) => {
|
||||
// Continuer avec la prochaine track
|
||||
track_number += 1;
|
||||
continue;
|
||||
}
|
||||
StopReason::EndOfStream | StopReason::ChannelClosed | StopReason::Cancelled => {
|
||||
// Fin de l'encodage
|
||||
return Ok(());
|
||||
// Dispatcher les segments depuis rx vers track_tx
|
||||
result = rx.recv() => {
|
||||
match result {
|
||||
Some(segment) => {
|
||||
match &segment.segment {
|
||||
crate::_AudioSegment::Chunk(_) => {
|
||||
// Dispatcher vers le pump
|
||||
if track_tx.send(segment).await.is_err() {
|
||||
// Le pump est mort - erreur fatale
|
||||
tracing::error!("FlacFileSink: pump died unexpectedly");
|
||||
return Err(AudioError::ProcessingError("Pump task died".to_string()));
|
||||
}
|
||||
}
|
||||
crate::_AudioSegment::Sync(marker) => match &**marker {
|
||||
SyncMarker::TrackBoundary { .. } => {
|
||||
// Nouveau morceau - fermer le pump et passer au suivant
|
||||
tracing::debug!("FlacFileSink: TrackBoundary received");
|
||||
|
||||
// Vérifier que copy est terminé avant de continuer
|
||||
if !copy_done {
|
||||
copy_future.await?;
|
||||
tracing::info!("FlacFileSink: File write complete for track {}", track_number);
|
||||
}
|
||||
|
||||
drop(track_tx); // Ferme le channel, le pump se termine proprement
|
||||
drop(pump_handle);
|
||||
|
||||
// Créer le marqueur de complétude
|
||||
let completion_marker = track_path.with_extension("flac.complete");
|
||||
if let Err(e) = tokio::fs::File::create(&completion_marker).await {
|
||||
tracing::warn!("FlacFileSink: Failed to create completion marker {:?}: {}", completion_marker, e);
|
||||
} else {
|
||||
tracing::debug!("FlacFileSink: Created completion marker {:?}", completion_marker);
|
||||
}
|
||||
|
||||
track_number += 1;
|
||||
break; // Sort de la Phase 1, retour à la loop externe pour next track
|
||||
}
|
||||
SyncMarker::EndOfStream => {
|
||||
tracing::debug!("FlacFileSink: EndOfStream received");
|
||||
|
||||
// Vérifier que copy est terminé
|
||||
if !copy_done {
|
||||
copy_future.await?;
|
||||
tracing::info!("FlacFileSink: File write complete for track {}", track_number);
|
||||
}
|
||||
|
||||
drop(track_tx);
|
||||
drop(pump_handle);
|
||||
|
||||
// Créer le marqueur de complétude
|
||||
let completion_marker = track_path.with_extension("flac.complete");
|
||||
if let Err(e) = tokio::fs::File::create(&completion_marker).await {
|
||||
tracing::warn!("FlacFileSink: Failed to create completion marker {:?}: {}", completion_marker, e);
|
||||
} else {
|
||||
tracing::debug!("FlacFileSink: Created completion marker {:?}", completion_marker);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
// Transmettre les autres syncmarkers au pump
|
||||
let _ = track_tx.send(segment).await;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// EOF sur rx
|
||||
tracing::debug!("FlacFileSink: EOF on rx");
|
||||
|
||||
// Vérifier que copy est terminé
|
||||
if !copy_done {
|
||||
copy_future.await?;
|
||||
tracing::info!("FlacFileSink: File write complete for track {}", track_number);
|
||||
}
|
||||
|
||||
drop(track_tx);
|
||||
drop(pump_handle);
|
||||
|
||||
// Créer le marqueur de complétude
|
||||
let completion_marker = track_path.with_extension("flac.complete");
|
||||
if let Err(e) = tokio::fs::File::create(&completion_marker).await {
|
||||
tracing::warn!("FlacFileSink: Failed to create completion marker {:?}: {}", completion_marker, e);
|
||||
} else {
|
||||
tracing::debug!("FlacFileSink: Created completion marker {:?}", completion_marker);
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = stop_token.cancelled() => {
|
||||
drop(track_tx);
|
||||
drop(pump_handle);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,6 +478,71 @@ async fn pump_track_segments(
|
||||
}
|
||||
}
|
||||
|
||||
/// Pompe les segments pour une seule track depuis un channel dédié.
|
||||
///
|
||||
/// Cette version permet d'avoir plusieurs pumps en parallèle (cache progressive compliant),
|
||||
/// car chaque pump a son propre channel et ne bloque pas le traitement des tracks suivantes.
|
||||
async fn pump_track_segments_from_channel(
|
||||
first_segment: Arc<AudioSegment>,
|
||||
mut track_rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||
pcm_tx: mpsc::Sender<Vec<u8>>,
|
||||
bits_per_sample: u8,
|
||||
expected_rate: u32,
|
||||
) -> Result<(), AudioError> {
|
||||
// Traiter le premier segment
|
||||
if let Some(chunk) = first_segment.as_chunk() {
|
||||
let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?;
|
||||
if !pcm_bytes.is_empty() {
|
||||
if pcm_tx.send(pcm_bytes).await.is_err() {
|
||||
drop(pcm_tx);
|
||||
tracing::debug!("pump_track_segments_from_channel: pcm_tx closed on first segment");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Boucle sur les segments depuis le channel dédié
|
||||
loop {
|
||||
let segment = match track_rx.recv().await {
|
||||
Some(seg) => seg,
|
||||
None => {
|
||||
// Channel fermé - la track est terminée (TrackBoundary a été reçu en amont)
|
||||
drop(pcm_tx);
|
||||
tracing::debug!("pump_track_segments_from_channel: channel closed, track finished");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
match &segment.segment {
|
||||
crate::_AudioSegment::Chunk(chunk) => {
|
||||
if chunk.sample_rate() != expected_rate {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"FlacFileSink: inconsistent sample rate ({} vs {})",
|
||||
chunk.sample_rate(),
|
||||
expected_rate
|
||||
)));
|
||||
}
|
||||
|
||||
let pcm_bytes = chunk_to_pcm_bytes(&chunk, bits_per_sample)?;
|
||||
if pcm_bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if pcm_tx.send(pcm_bytes).await.is_err() {
|
||||
// Le fichier a fermé le channel (erreur)
|
||||
drop(pcm_tx);
|
||||
tracing::debug!("pump_track_segments_from_channel: pcm_tx closed");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
crate::_AudioSegment::Sync(_marker) => {
|
||||
// Ignorer les syncmarkers - le TrackBoundary est géré en amont
|
||||
// Le channel sera fermé quand le TrackBoundary est détecté
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Détermine la profondeur de bit d'un chunk audio
|
||||
fn get_chunk_bit_depth(chunk: &AudioChunk) -> u8 {
|
||||
match chunk {
|
||||
|
||||
Reference in New Issue
Block a user