Refactor audio pipeline for multi-client streaming and improved error handling
Refactor the audio pipeline to support multi-client streaming with new OggFlacStreamHandle and StreamingOggFlacSink. - Replace DirectOggFlacSink with StreamingOggFlacSink in pipeline - Update documentation and comments to reflect multi-client support - Add detailed warning logs for buffer underruns and client disconnections - Remove TimerBufferNode from pipeline as it's no longer needed - Update connect() calls to subscribe() for new streaming behavior This change enables multiple clients to subscribe to the same audio stream, with each subscription getting a live feed from the current point in time.
This commit is contained in:
@@ -44,7 +44,7 @@ use pmoflac::{EncoderOptions, PcmFormat};
|
|||||||
use tokio::io::{AsyncRead, ReadBuf};
|
use tokio::io::{AsyncRead, ReadBuf};
|
||||||
use tokio::sync::{mpsc, watch, Mutex};
|
use tokio::sync::{mpsc, watch, Mutex};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::debug;
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::sinks::byte_stream_reader::{ByteStreamReader, PcmChunk};
|
use crate::sinks::byte_stream_reader::{ByteStreamReader, PcmChunk};
|
||||||
use crate::sinks::chunk_to_pcm::chunk_to_pcm_bytes;
|
use crate::sinks::chunk_to_pcm::chunk_to_pcm_bytes;
|
||||||
@@ -228,7 +228,11 @@ impl NodeLogic for DirectOggFlacSinkLogic {
|
|||||||
duration_sec,
|
duration_sec,
|
||||||
};
|
};
|
||||||
if tx.send(pcm_chunk).await.is_err() {
|
if tx.send(pcm_chunk).await.is_err() {
|
||||||
debug!("DirectOggFlacSink: pcm_tx send failed (client disconnected), clearing pcm_tx");
|
warn!(
|
||||||
|
ts = seg.timestamp_sec,
|
||||||
|
"DirectOggFlacSink: chunk dropped (client disconnected at {:.3}s), waiting for reconnect",
|
||||||
|
seg.timestamp_sec,
|
||||||
|
);
|
||||||
*self.pcm_tx.lock().await = None;
|
*self.pcm_tx.lock().await = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,7 +352,13 @@ async fn run_ogg_encoder(
|
|||||||
|
|
||||||
let ogg_page = Bytes::from(ogg.create_page(&frame, false, false, false));
|
let ogg_page = Bytes::from(ogg.create_page(&frame, false, false, false));
|
||||||
if pipe_writer.write_all(&ogg_page).await.is_err() {
|
if pipe_writer.write_all(&ogg_page).await.is_err() {
|
||||||
// Client déconnecté
|
// Client déconnecté — le pipe HTTP s'est rompu
|
||||||
|
warn!(
|
||||||
|
samples = encoded_samples,
|
||||||
|
"DirectOggFlacSink: OGG pipe broken after {} samples ({:.3}s), client disconnected",
|
||||||
|
encoded_samples,
|
||||||
|
encoded_samples as f64 / DIRECT_OGG_FLAC_SAMPLE_RATE as f64,
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,14 @@ impl TimerBufferNodeLogic {
|
|||||||
self.buffer.len()
|
self.buffer.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if self.buffer.is_empty() && self.buffered_time_sec < self.capacity_sec * 0.1 {
|
||||||
|
tracing::warn!(
|
||||||
|
"TimerBufferNode: buffer underrun at ts={:.3}s (capacity={:.1}s) — source too slow or stalled",
|
||||||
|
segment.timestamp_sec,
|
||||||
|
self.capacity_sec,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
send_to_children(std::any::type_name::<Self>(), output, segment).await?;
|
send_to_children(std::any::type_name::<Self>(), output, segment).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -7,9 +7,12 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use pmoaudio::{AudioSegment, PositionHandle, PositionTrackerNode, ResamplingNode, TimerBufferNode, ToI24Node};
|
use pmoaudio::{AudioSegment, PositionTrackerNode, ResamplingNode, ToI24Node};
|
||||||
use pmometadata::{MemoryTrackMetadata, TrackMetadata};
|
use pmometadata::{MemoryTrackMetadata, TrackMetadata};
|
||||||
use pmoaudio_ext::sinks::{DirectOggFlacHandle, DirectOggFlacSink, DIRECT_OGG_FLAC_SAMPLE_RATE};
|
use pmoaudio_ext::sinks::{
|
||||||
|
OggFlacStreamHandle, StreamingOggFlacSink,
|
||||||
|
DIRECT_OGG_FLAC_BITS_PER_SAMPLE, DIRECT_OGG_FLAC_SAMPLE_RATE,
|
||||||
|
};
|
||||||
use pmoaudio_ext::UriSource;
|
use pmoaudio_ext::UriSource;
|
||||||
use pmoflac::EncoderOptions;
|
use pmoflac::EncoderOptions;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
@@ -56,10 +59,10 @@ impl PipelineHandle {
|
|||||||
/// Pipeline audio complet pour une instance WebRenderer.
|
/// Pipeline audio complet pour une instance WebRenderer.
|
||||||
///
|
///
|
||||||
/// Créé au `POST /register`. Le flux OGG-FLAC est accessible via `flac_handle`
|
/// Créé au `POST /register`. Le flux OGG-FLAC est accessible via `flac_handle`
|
||||||
/// (un seul client à la fois, reconnectable à chaque Play).
|
/// (multi-clients, chaque `subscribe()` retourne un nouveau flux depuis le point courant).
|
||||||
pub struct InstancePipeline {
|
pub struct InstancePipeline {
|
||||||
/// Handle vers le sink OGG-FLAC — clonable, reconnectable à chaque Play.
|
/// Handle vers le sink OGG-FLAC — clonable, chaque subscribe() donne un flux live.
|
||||||
pub flac_handle: DirectOggFlacHandle,
|
pub flac_handle: OggFlacStreamHandle,
|
||||||
pub pipeline_handle: PipelineHandle,
|
pub pipeline_handle: PipelineHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,21 +82,18 @@ impl InstancePipeline {
|
|||||||
// La backpressure remonte naturellement depuis le pipe duplex jusqu'à la source.
|
// La backpressure remonte naturellement depuis le pipe duplex jusqu'à la source.
|
||||||
use pmoaudio::pipeline::AudioPipelineNode;
|
use pmoaudio::pipeline::AudioPipelineNode;
|
||||||
|
|
||||||
let (sink, flac_handle) = DirectOggFlacSink::new(EncoderOptions::default());
|
let (sink, flac_handle) = StreamingOggFlacSink::new(
|
||||||
|
EncoderOptions::default(),
|
||||||
|
DIRECT_OGG_FLAC_BITS_PER_SAMPLE,
|
||||||
|
);
|
||||||
|
|
||||||
// Nœud de suivi de position : lit le timestamp des chunks sortant du buffer
|
// Nœud de suivi de position : lit le timestamp des chunks sortant vers le sink
|
||||||
let (mut position_tracker, position_handle) = PositionTrackerNode::new();
|
let (mut position_tracker, position_handle) = PositionTrackerNode::new();
|
||||||
position_tracker.register(sink.boxed());
|
position_tracker.register(sink.boxed());
|
||||||
|
|
||||||
// Nœud de pacing : régule le débit pour éviter les rafales et pertes de segments
|
|
||||||
// 2s de buffer absorbe les irrégularités de la source réseau
|
|
||||||
let mut timer_buffer = TimerBufferNode::new(2.0);
|
|
||||||
timer_buffer.register(position_tracker.boxed());
|
|
||||||
|
|
||||||
// Nœud de conversion de profondeur : tout type entier → I24
|
// Nœud de conversion de profondeur : tout type entier → I24
|
||||||
// Placé avant le buffer pour réduire la mémoire utilisée
|
|
||||||
let mut to_i24 = ToI24Node::new();
|
let mut to_i24 = ToI24Node::new();
|
||||||
to_i24.register(timer_buffer.boxed());
|
to_i24.register(position_tracker.boxed());
|
||||||
|
|
||||||
// Nœud de rééchantillonnage : n'importe quel sample rate → 96 kHz
|
// Nœud de rééchantillonnage : n'importe quel sample rate → 96 kHz
|
||||||
let mut resampler = ResamplingNode::new(DIRECT_OGG_FLAC_SAMPLE_RATE);
|
let mut resampler = ResamplingNode::new(DIRECT_OGG_FLAC_SAMPLE_RATE);
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ pub struct WebRendererInstance {
|
|||||||
pub udn: String,
|
pub udn: String,
|
||||||
pub device_instance: Arc<DeviceInstance>,
|
pub device_instance: Arc<DeviceInstance>,
|
||||||
pub state: SharedState,
|
pub state: SharedState,
|
||||||
/// Handle vers le sink OGG-FLAC — clonable, reconnectable à chaque Play.
|
/// Handle vers le sink OGG-FLAC — clonable, chaque subscribe() donne un flux live.
|
||||||
pub flac_handle: pmoaudio_ext::sinks::DirectOggFlacHandle,
|
pub flac_handle: pmoaudio_ext::sinks::OggFlacStreamHandle,
|
||||||
pub pipeline: PipelineHandle,
|
pub pipeline: PipelineHandle,
|
||||||
pub created_at: SystemTime,
|
pub created_at: SystemTime,
|
||||||
}
|
}
|
||||||
@@ -116,11 +116,11 @@ impl RendererRegistry {
|
|||||||
Ok((stream_url, udn))
|
Ok((stream_url, udn))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retourne le DirectOggFlacHandle pour l'endpoint /stream (clonable).
|
/// Retourne le OggFlacStreamHandle pour l'endpoint /stream (clonable).
|
||||||
pub fn get_flac_handle(
|
pub fn get_flac_handle(
|
||||||
&self,
|
&self,
|
||||||
instance_id: &str,
|
instance_id: &str,
|
||||||
) -> Option<pmoaudio_ext::sinks::DirectOggFlacHandle> {
|
) -> Option<pmoaudio_ext::sinks::OggFlacStreamHandle> {
|
||||||
self.instances
|
self.instances
|
||||||
.read()
|
.read()
|
||||||
.get(instance_id)
|
.get(instance_id)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ pub async fn stream_handler(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let stream = handle.connect().await;
|
let stream = handle.subscribe();
|
||||||
|
|
||||||
info!(instance_id = %instance_id, "FLAC stream started");
|
info!(instance_id = %instance_id, "FLAC stream started");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user