Nouvelle tentative de broadcast avec considération d'un temps d'expiration.

This commit is contained in:
2025-11-15 12:21:46 +01:00
parent 1c2d30cbe9
commit 4e27255305
5 changed files with 628 additions and 122 deletions

View File

@@ -16,6 +16,9 @@ mod broadcast_pacing;
#[cfg(feature = "http-stream")] #[cfg(feature = "http-stream")]
mod flac_frame_utils; mod flac_frame_utils;
#[cfg(feature = "http-stream")]
mod timed_broadcast;
#[cfg(feature = "http-stream")] #[cfg(feature = "http-stream")]
mod streaming_flac_sink; mod streaming_flac_sink;

View File

@@ -19,7 +19,7 @@
//! ↓ //! ↓
//! [Broadcaster Task] //! [Broadcaster Task]
//! ↓ //! ↓
//! broadcast::channel<Bytes> (FLAC bytes) //! timed_broadcast::channel<Bytes> (FLAC bytes)
//! ↓ //! ↓
//! Multiple clients via StreamHandle::subscribe() //! Multiple clients via StreamHandle::subscribe()
//! ├─ FLAC pure (for standard renderers) //! ├─ FLAC pure (for standard renderers)
@@ -62,7 +62,11 @@ use std::sync::Arc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::time::Duration; use std::time::Duration;
use super::{broadcast_pacing::BroadcastPacer, flac_frame_utils}; use super::{
broadcast_pacing::BroadcastPacer,
flac_frame_utils,
timed_broadcast::{self, TimedPacket, TryRecvError},
};
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use pmoaudio::{ use pmoaudio::{
@@ -73,7 +77,7 @@ use pmoaudio::{
use pmoflac::{encode_flac_stream, EncoderOptions, FlacEncodedStream, PcmFormat}; use pmoflac::{encode_flac_stream, EncoderOptions, FlacEncodedStream, PcmFormat};
use pmometadata::TrackMetadata; use pmometadata::TrackMetadata;
use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf}; use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
use tokio::sync::{broadcast, mpsc, RwLock}; use tokio::sync::{mpsc, RwLock};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, trace, warn}; use tracing::{debug, error, info, trace, warn};
@@ -160,7 +164,7 @@ pub struct MetadataSnapshot {
#[derive(Clone)] #[derive(Clone)]
pub struct StreamHandle { pub struct StreamHandle {
/// Broadcast sender for FLAC bytes (pure mode) /// Broadcast sender for FLAC bytes (pure mode)
flac_broadcast: broadcast::Sender<Bytes>, flac_broadcast: timed_broadcast::Sender<Bytes>,
/// Current track metadata (read-only for consumers) /// Current track metadata (read-only for consumers)
metadata: Arc<RwLock<MetadataSnapshot>>, metadata: Arc<RwLock<MetadataSnapshot>>,
@@ -189,6 +193,7 @@ impl StreamHandle {
finished: false, finished: false,
handle: self.clone(), handle: self.clone(),
state: FlacStreamState::SendingHeader, state: FlacStreamState::SendingHeader,
current_epoch: 0,
} }
} }
@@ -220,6 +225,7 @@ impl StreamHandle {
finished: false, finished: false,
handle: self.clone(), handle: self.clone(),
state: FlacStreamState::SendingHeader, state: FlacStreamState::SendingHeader,
current_epoch: 0,
} }
} }
@@ -247,11 +253,18 @@ enum FlacStreamState {
/// Pure FLAC client stream (implements AsyncRead). /// Pure FLAC client stream (implements AsyncRead).
pub struct FlacClientStream { pub struct FlacClientStream {
rx: broadcast::Receiver<Bytes>, rx: timed_broadcast::Receiver<Bytes>,
buffer: VecDeque<u8>, buffer: VecDeque<u8>,
finished: bool, finished: bool,
handle: StreamHandle, handle: StreamHandle,
state: FlacStreamState, state: FlacStreamState,
current_epoch: u64,
}
impl FlacClientStream {
pub fn current_epoch(&self) -> u64 {
self.current_epoch
}
} }
impl AsyncRead for FlacClientStream { impl AsyncRead for FlacClientStream {
@@ -302,10 +315,11 @@ impl AsyncRead for FlacClientStream {
// Try to receive more data // Try to receive more data
match self.rx.try_recv() { match self.rx.try_recv() {
Ok(bytes) => { Ok(packet) => {
self.buffer.extend(bytes.iter()); self.current_epoch = packet.epoch;
self.buffer.extend(packet.payload.iter());
} }
Err(broadcast::error::TryRecvError::Empty) => { Err(TryRecvError::Empty) => {
// No data available right now. // No data available right now.
// Schedule a wakeup after a small delay to avoid busy-loop polling. // Schedule a wakeup after a small delay to avoid busy-loop polling.
let waker = cx.waker().clone(); let waker = cx.waker().clone();
@@ -315,11 +329,11 @@ impl AsyncRead for FlacClientStream {
}); });
return Poll::Pending; return Poll::Pending;
} }
Err(broadcast::error::TryRecvError::Lagged(skipped)) => { Err(TryRecvError::Lagged(skipped)) => {
warn!("FLAC client lagged, skipped {} messages", skipped); warn!("FLAC client lagged, skipped {} messages", skipped);
// Continue to try receiving again // Continue to try receiving again
} }
Err(broadcast::error::TryRecvError::Closed) => { Err(TryRecvError::Closed) => {
self.finished = true; self.finished = true;
return Poll::Ready(Ok(())); return Poll::Ready(Ok(()));
} }
@@ -345,7 +359,7 @@ impl Drop for FlacClientStream {
/// This stream injects ICY metadata blocks at regular intervals, /// This stream injects ICY metadata blocks at regular intervals,
/// allowing clients to display "Now Playing" information. /// allowing clients to display "Now Playing" information.
pub struct IcyClientStream { pub struct IcyClientStream {
rx: broadcast::Receiver<Bytes>, rx: timed_broadcast::Receiver<Bytes>,
metadata: Arc<RwLock<MetadataSnapshot>>, metadata: Arc<RwLock<MetadataSnapshot>>,
metaint: usize, metaint: usize,
byte_count: usize, byte_count: usize,
@@ -355,6 +369,13 @@ pub struct IcyClientStream {
finished: bool, finished: bool,
handle: StreamHandle, handle: StreamHandle,
state: FlacStreamState, state: FlacStreamState,
current_epoch: u64,
}
impl IcyClientStream {
pub fn current_epoch(&self) -> u64 {
self.current_epoch
}
} }
impl IcyClientStream { impl IcyClientStream {
@@ -491,22 +512,23 @@ impl AsyncRead for IcyClientStream {
// Try to receive audio data // Try to receive audio data
match self.rx.try_recv() { match self.rx.try_recv() {
Ok(bytes) => { Ok(packet) => {
self.current_epoch = packet.epoch;
// Calculate how many bytes until next metadata block // Calculate how many bytes until next metadata block
let until_metadata = self.metaint - (self.byte_count % self.metaint); let until_metadata = self.metaint - (self.byte_count % self.metaint);
let to_buffer = bytes.len().min(until_metadata); let to_buffer = packet.payload.len().min(until_metadata);
self.buffer.extend(bytes[..to_buffer].iter()); self.buffer.extend(packet.payload[..to_buffer].iter());
self.byte_count += to_buffer; self.byte_count += to_buffer;
// If we have more data, we'll process it in the next iteration // If we have more data, we'll process it in the next iteration
if to_buffer < bytes.len() { if to_buffer < packet.payload.len() {
// Save remaining for next iteration // Save remaining for next iteration
// For now, we'll just drop it and get it again // For now, we'll just drop it and get it again
// TODO: Improve this // TODO: Improve this
} }
} }
Err(broadcast::error::TryRecvError::Empty) => { Err(TryRecvError::Empty) => {
// No data available right now. // No data available right now.
// Schedule a wakeup after a small delay to avoid busy-loop polling. // Schedule a wakeup after a small delay to avoid busy-loop polling.
let waker = cx.waker().clone(); let waker = cx.waker().clone();
@@ -516,10 +538,10 @@ impl AsyncRead for IcyClientStream {
}); });
return Poll::Pending; return Poll::Pending;
} }
Err(broadcast::error::TryRecvError::Lagged(skipped)) => { Err(TryRecvError::Lagged(skipped)) => {
warn!("ICY client lagged, skipped {} messages", skipped); warn!("ICY client lagged, skipped {} messages", skipped);
} }
Err(broadcast::error::TryRecvError::Closed) => { Err(TryRecvError::Closed) => {
self.finished = true; self.finished = true;
return Poll::Ready(Ok(())); return Poll::Ready(Ok(()));
} }
@@ -552,7 +574,7 @@ struct StreamingFlacSinkLogic {
pcm_tx: mpsc::Sender<PcmChunk>, pcm_tx: mpsc::Sender<PcmChunk>,
pcm_rx: Option<mpsc::Receiver<PcmChunk>>, pcm_rx: Option<mpsc::Receiver<PcmChunk>>,
metadata: Arc<RwLock<MetadataSnapshot>>, metadata: Arc<RwLock<MetadataSnapshot>>,
flac_broadcast: broadcast::Sender<Bytes>, flac_broadcast: timed_broadcast::Sender<Bytes>,
flac_header: Arc<RwLock<Option<Bytes>>>, flac_header: Arc<RwLock<Option<Bytes>>>,
encoder_state: Option<EncoderState>, encoder_state: Option<EncoderState>,
sample_rate: Option<u32>, sample_rate: Option<u32>,
@@ -749,6 +771,11 @@ impl NodeLogic for StreamingFlacSinkLogic {
break; break;
} }
SyncMarker::TopZeroSync => {
self.flac_broadcast.mark_top_zero();
trace!("TopZeroSync propagated to FLAC broadcast");
}
_ => { _ => {
trace!("Received other sync marker"); trace!("Received other sync marker");
} }
@@ -781,7 +808,7 @@ impl NodeLogic for StreamingFlacSinkLogic {
/// Ensures data is sent at FLAC frame boundaries to prevent sync errors in strict decoders like FFPlay. /// Ensures data is sent at FLAC frame boundaries to prevent sync errors in strict decoders like FFPlay.
async fn broadcast_flac_stream( async fn broadcast_flac_stream(
mut flac_stream: FlacEncodedStream, mut flac_stream: FlacEncodedStream,
broadcast_tx: broadcast::Sender<Bytes>, broadcast_tx: timed_broadcast::Sender<Bytes>,
header_cache: Arc<RwLock<Option<Bytes>>>, header_cache: Arc<RwLock<Option<Bytes>>>,
current_timestamp: Arc<RwLock<f64>>, current_timestamp: Arc<RwLock<f64>>,
broadcast_max_lead_time: f64, broadcast_max_lead_time: f64,
@@ -813,7 +840,11 @@ async fn broadcast_flac_stream(
// EOF - send any remaining data // EOF - send any remaining data
if !accumulator.is_empty() { if !accumulator.is_empty() {
let bytes = Bytes::from(std::mem::take(&mut accumulator)); let bytes = Bytes::from(std::mem::take(&mut accumulator));
let _ = broadcast_tx.send(bytes); let audio_ts = *current_timestamp.read().await;
if broadcast_tx.send(bytes.clone(), audio_ts).await.is_err() {
trace!("Broadcast closed before sending final FLAC data");
break;
}
} }
info!("FLAC encoder stream ended, total bytes: {}", total_bytes); info!("FLAC encoder stream ended, total bytes: {}", total_bytes);
break; break;
@@ -931,15 +962,20 @@ async fn broadcast_flac_stream(
} }
let num_receivers = broadcast_tx.receiver_count(); let num_receivers = broadcast_tx.receiver_count();
if let Err(e) = broadcast_tx.send(bytes.clone()) { match broadcast_tx.send(bytes.clone(), audio_timestamp).await {
// No receivers, but that's okay - clients may not be connected yet Ok(_) => {
trace!("No active receivers for FLAC broadcast: {}", e); if num_receivers > 0 {
} else if num_receivers > 0 { trace!(
trace!( "Broadcasted {} bytes to {} receivers",
"Broadcasted {} bytes to {} receivers", bytes.len(),
bytes.len(), num_receivers
num_receivers );
); }
}
Err(_) => {
trace!("No active receivers for FLAC broadcast, terminating");
return Ok(());
}
} }
} }
} }
@@ -1017,7 +1053,7 @@ impl StreamingFlacSink {
); );
// Broadcast channel for FLAC bytes // Broadcast channel for FLAC bytes
let (flac_broadcast, _) = broadcast::channel(broadcast_capacity); let (flac_broadcast, _) = timed_broadcast::channel(broadcast_capacity);
// FLAC header cache // FLAC header cache
let flac_header = Arc::new(RwLock::new(None)); let flac_header = Arc::new(RwLock::new(None));

View File

@@ -21,7 +21,7 @@
//! ↓ //! ↓
//! [OGG Wrapper Task] - wraps FLAC frames in OGG pages //! [OGG Wrapper Task] - wraps FLAC frames in OGG pages
//! ↓ //! ↓
//! broadcast::channel<Bytes> (OGG-FLAC bytes) //! timed_broadcast::channel<Bytes> (OGG-FLAC bytes)
//! ↓ //! ↓
//! Multiple HTTP clients //! Multiple HTTP clients
//! ``` //! ```
@@ -53,7 +53,11 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use super::{broadcast_pacing::BroadcastPacer, flac_frame_utils}; use super::{
broadcast_pacing::BroadcastPacer,
flac_frame_utils,
timed_broadcast::{self, TimedPacket, TryRecvError},
};
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
use pmoaudio::{ use pmoaudio::{
@@ -64,7 +68,7 @@ use pmoaudio::{
use pmoflac::{encode_flac_stream, EncoderOptions, FlacEncodedStream, PcmFormat}; use pmoflac::{encode_flac_stream, EncoderOptions, FlacEncodedStream, PcmFormat};
use pmometadata::TrackMetadata; use pmometadata::TrackMetadata;
use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf}; use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
use tokio::sync::{broadcast, mpsc, RwLock}; use tokio::sync::{mpsc, RwLock};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, trace, warn}; use tracing::{debug, error, info, trace, warn};
@@ -97,7 +101,7 @@ pub use super::streaming_flac_sink::MetadataSnapshot;
#[derive(Clone)] #[derive(Clone)]
pub struct OggFlacStreamHandle { pub struct OggFlacStreamHandle {
/// Broadcast sender for OGG-FLAC bytes /// Broadcast sender for OGG-FLAC bytes
ogg_broadcast: broadcast::Sender<Bytes>, ogg_broadcast: timed_broadcast::Sender<Bytes>,
/// Current track metadata /// Current track metadata
metadata: Arc<RwLock<MetadataSnapshot>>, metadata: Arc<RwLock<MetadataSnapshot>>,
@@ -126,6 +130,7 @@ impl OggFlacStreamHandle {
finished: false, finished: false,
handle: self.clone(), handle: self.clone(),
state: OggFlacStreamState::SendingHeader, state: OggFlacStreamState::SendingHeader,
current_epoch: 0,
} }
} }
@@ -148,11 +153,19 @@ enum OggFlacStreamState {
/// OGG-FLAC client stream (implements AsyncRead). /// OGG-FLAC client stream (implements AsyncRead).
pub struct OggFlacClientStream { pub struct OggFlacClientStream {
rx: broadcast::Receiver<Bytes>, rx: timed_broadcast::Receiver<Bytes>,
buffer: VecDeque<u8>, buffer: VecDeque<u8>,
finished: bool, finished: bool,
handle: OggFlacStreamHandle, handle: OggFlacStreamHandle,
state: OggFlacStreamState, state: OggFlacStreamState,
current_epoch: u64,
}
impl OggFlacClientStream {
/// Dernier epoch observé (incrémenté à chaque TopZeroSync).
pub fn current_epoch(&self) -> u64 {
self.current_epoch
}
} }
impl AsyncRead for OggFlacClientStream { impl AsyncRead for OggFlacClientStream {
@@ -203,10 +216,11 @@ impl AsyncRead for OggFlacClientStream {
// Try to receive more data // Try to receive more data
match self.rx.try_recv() { match self.rx.try_recv() {
Ok(bytes) => { Ok(packet) => {
self.buffer.extend(bytes.iter()); self.current_epoch = packet.epoch;
self.buffer.extend(packet.payload.iter());
} }
Err(broadcast::error::TryRecvError::Empty) => { Err(TryRecvError::Empty) => {
// No data available right now. // No data available right now.
// Schedule a wakeup after a small delay to avoid busy-loop polling. // Schedule a wakeup after a small delay to avoid busy-loop polling.
let waker = cx.waker().clone(); let waker = cx.waker().clone();
@@ -216,10 +230,10 @@ impl AsyncRead for OggFlacClientStream {
}); });
return Poll::Pending; return Poll::Pending;
} }
Err(broadcast::error::TryRecvError::Lagged(skipped)) => { Err(TryRecvError::Lagged(skipped)) => {
warn!("OGG-FLAC client lagged, skipped {} messages", skipped); warn!("OGG-FLAC client lagged, skipped {} messages", skipped);
} }
Err(broadcast::error::TryRecvError::Closed) => { Err(TryRecvError::Closed) => {
self.finished = true; self.finished = true;
return Poll::Ready(Ok(())); return Poll::Ready(Ok(()));
} }
@@ -252,7 +266,7 @@ struct StreamingOggFlacSinkLogic {
pcm_tx: mpsc::Sender<PcmChunk>, pcm_tx: mpsc::Sender<PcmChunk>,
pcm_rx: Option<mpsc::Receiver<PcmChunk>>, pcm_rx: Option<mpsc::Receiver<PcmChunk>>,
metadata: Arc<RwLock<MetadataSnapshot>>, metadata: Arc<RwLock<MetadataSnapshot>>,
ogg_broadcast: broadcast::Sender<Bytes>, ogg_broadcast: timed_broadcast::Sender<Bytes>,
ogg_header: Arc<RwLock<Option<Bytes>>>, ogg_header: Arc<RwLock<Option<Bytes>>>,
encoder_state: Option<EncoderState>, encoder_state: Option<EncoderState>,
sample_rate: Option<u32>, sample_rate: Option<u32>,
@@ -440,6 +454,11 @@ impl NodeLogic for StreamingOggFlacSinkLogic {
break; break;
} }
SyncMarker::TopZeroSync => {
self.ogg_broadcast.mark_top_zero();
trace!("TopZeroSync propagated to timed broadcast");
}
_ => { _ => {
trace!("Received other sync marker"); trace!("Received other sync marker");
} }
@@ -521,7 +540,7 @@ impl StreamingOggFlacSink {
broadcast_capacity, broadcast_capacity,
broadcast_max_lead_time broadcast_max_lead_time
); );
let (ogg_broadcast, _) = broadcast::channel(broadcast_capacity); let (ogg_broadcast, _) = timed_broadcast::channel(broadcast_capacity);
// OGG-FLAC header cache // OGG-FLAC header cache
let ogg_header = Arc::new(RwLock::new(None)); let ogg_header = Arc::new(RwLock::new(None));
@@ -751,7 +770,7 @@ fn chunk_to_pcm_bytes(chunk: &AudioChunk, bits_per_sample: u8) -> Result<Vec<u8>
/// Ensures FLAC frames are only sent at frame boundaries to prevent sync errors in strict decoders like FFPlay. /// Ensures FLAC frames are only sent at frame boundaries to prevent sync errors in strict decoders like FFPlay.
async fn broadcast_ogg_flac_stream( async fn broadcast_ogg_flac_stream(
mut flac_stream: FlacEncodedStream, mut flac_stream: FlacEncodedStream,
broadcast_tx: broadcast::Sender<Bytes>, broadcast_tx: timed_broadcast::Sender<Bytes>,
header_cache: Arc<RwLock<Option<Bytes>>>, header_cache: Arc<RwLock<Option<Bytes>>>,
current_timestamp: Arc<RwLock<f64>>, current_timestamp: Arc<RwLock<f64>>,
broadcast_max_lead_time: f64, broadcast_max_lead_time: f64,
@@ -811,9 +830,15 @@ async fn broadcast_ogg_flac_stream(
); );
// Broadcast header // Broadcast header
let _ = broadcast_tx.send(bos_bytes); if broadcast_tx.send(bos_bytes.clone(), 0.0).await.is_err() {
trace!("No receivers for BOS page, terminating broadcast");
return Ok(());
}
total_ogg_bytes += comment_bytes.len() as u64; total_ogg_bytes += comment_bytes.len() as u64;
let _ = broadcast_tx.send(comment_bytes); if broadcast_tx.send(comment_bytes.clone(), 0.0).await.is_err() {
trace!("No receivers for comment page, terminating broadcast");
return Ok(());
}
// Step 4: Read FLAC stream and create OGG packets // Step 4: Read FLAC stream and create OGG packets
// Use larger read buffer (16KB) to reduce syscalls and accumulator for frame boundary detection // Use larger read buffer (16KB) to reduce syscalls and accumulator for frame boundary detection
@@ -830,7 +855,11 @@ async fn broadcast_ogg_flac_stream(
let eos_page = ogg_writer.create_page(&flac_accumulator, false, true, false); let eos_page = ogg_writer.create_page(&flac_accumulator, false, true, false);
let eos_bytes = Bytes::from(eos_page); let eos_bytes = Bytes::from(eos_page);
total_ogg_bytes += eos_bytes.len() as u64; total_ogg_bytes += eos_bytes.len() as u64;
let _ = broadcast_tx.send(eos_bytes); let eos_ts = *current_timestamp.read().await;
if broadcast_tx.send(eos_bytes.clone(), eos_ts).await.is_err() {
trace!("Broadcast closed before sending final EOS page");
break;
}
info!( info!(
"Sent final EOS page with {} bytes of data", "Sent final EOS page with {} bytes of data",
flac_accumulator.len() flac_accumulator.len()
@@ -840,7 +869,11 @@ async fn broadcast_ogg_flac_stream(
let eos_page = ogg_writer.create_page(&[], false, true, false); let eos_page = ogg_writer.create_page(&[], false, true, false);
let eos_bytes = Bytes::from(eos_page); let eos_bytes = Bytes::from(eos_page);
total_ogg_bytes += eos_bytes.len() as u64; total_ogg_bytes += eos_bytes.len() as u64;
let _ = broadcast_tx.send(eos_bytes); let eos_ts = *current_timestamp.read().await;
if broadcast_tx.send(eos_bytes.clone(), eos_ts).await.is_err() {
trace!("Broadcast closed before sending empty EOS page");
break;
}
info!("Sent empty EOS page"); info!("Sent empty EOS page");
} }
@@ -979,12 +1012,13 @@ async fn broadcast_ogg_flac_stream(
} }
// Envoyer au broadcast // Envoyer au broadcast
match broadcast_tx.send(ogg_bytes.clone()) { match broadcast_tx.send(ogg_bytes.clone(), audio_timestamp).await {
Ok(n) => { Ok(n) => {
trace!("Broadcasted OGG page with 1 FLAC frame ({} bytes), {} samples ({} bytes total with OGG overhead) to {} receivers", first_frame.len(), first_frame_samples, ogg_bytes.len(), n); trace!("Broadcasted OGG page with 1 FLAC frame ({} bytes), {} samples ({} bytes total with OGG overhead) to {} receivers", first_frame.len(), first_frame_samples, ogg_bytes.len(), n);
} }
Err(e) => { Err(_) => {
trace!("No active receivers for OGG-FLAC broadcast: {}", e); trace!("No active receivers for OGG-FLAC broadcast, terminating loop");
return Ok(());
} }
} }
} }

View File

@@ -0,0 +1,475 @@
//! Broadcast channel avec TTL et propagation de TopZero.
//! Inspiré de `tokio::sync::broadcast` mais ajoute :
//! - Capacité bornée avec blocage des producteurs quand aucun slot nest libre.
//! - Expiration automatique des messages (TTL) pour libérer les slots.
//! - Propagation dun compteur `epoch` incrémenté sur chaque TopZeroSync.
use std::{
collections::VecDeque,
fmt,
sync::{
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
Arc, Mutex, Weak,
},
time::{Duration, Instant},
};
use tokio::sync::Notify;
use tracing::warn;
/// Paquet diffusé contenant la charge utile + méta timing.
#[derive(Clone)]
pub struct TimedPacket<T> {
/// Charge utile diffusée aux clients.
pub payload: T,
/// Timestamp audio relatif (en secondes) pour pacing côté client.
pub audio_timestamp: f64,
/// Compteur incrémenté lorsqu'un TopZeroSync est reçu.
pub epoch: u64,
}
impl<T> fmt::Debug for TimedPacket<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TimedPacket")
.field("audio_timestamp", &self.audio_timestamp)
.field("epoch", &self.epoch)
.finish_non_exhaustive()
}
}
/// Erreur remontée par `Receiver::try_recv`.
#[derive(Debug)]
pub enum TryRecvError {
Empty,
Lagged(u64),
Closed,
}
/// Erreur remontée par `Receiver::recv`.
#[derive(Debug)]
pub enum RecvError {
Lagged(u64),
Closed,
}
/// Erreur remontée par `Sender::send`.
#[derive(Debug)]
pub struct SendError<T>(pub T);
struct Entry<T> {
seq: u64,
expires_at: Instant,
payload: T,
audio_timestamp: f64,
epoch: u64,
}
struct State<T> {
buffer: VecDeque<Entry<T>>,
head_seq: u64,
next_seq: u64,
closed: bool,
epoch: u64,
epoch_start: Instant,
cursors: Vec<Weak<ReceiverCursor>>,
}
impl<T> State<T> {
fn new(capacity: usize, epoch_start: Instant) -> Self {
Self {
buffer: VecDeque::with_capacity(capacity),
head_seq: 0,
next_seq: 0,
closed: false,
epoch: 0,
epoch_start,
cursors: Vec::new(),
}
}
fn purge_expired(&mut self, now: Instant) -> bool {
let mut purged = 0u64;
while let Some(entry) = self.buffer.front() {
if entry.expires_at <= now {
self.buffer.pop_front();
self.head_seq += 1;
purged += 1;
} else {
break;
}
}
if purged > 0 {
warn!(
"TimedBroadcast: purged {} expired packet(s) (head_seq={})",
purged,
self.head_seq
);
return true;
}
false
}
fn prune_consumed(&mut self) -> bool {
let mut min_next = self.next_seq;
let mut has_cursor = false;
self.cursors.retain(|weak| {
if let Some(cursor) = weak.upgrade() {
let pos = cursor.next_seq.load(Ordering::SeqCst);
if pos < min_next {
min_next = pos;
}
has_cursor = true;
true
} else {
false
}
});
if !has_cursor {
return false;
}
let removable = min_next.saturating_sub(self.head_seq) as usize;
if removable == 0 {
return false;
}
for _ in 0..removable {
if self.buffer.pop_front().is_some() {
self.head_seq += 1;
}
}
true
}
}
struct Inner<T> {
state: Mutex<State<T>>,
data_notify: Notify,
space_notify: Notify,
capacity: usize,
sender_count: AtomicUsize,
receiver_count: AtomicUsize,
is_closed: AtomicBool,
}
impl<T> Inner<T> {
fn new(capacity: usize) -> Self {
Self {
state: Mutex::new(State::new(capacity, Instant::now())),
data_notify: Notify::new(),
space_notify: Notify::new(),
capacity,
sender_count: AtomicUsize::new(1),
receiver_count: AtomicUsize::new(0),
is_closed: AtomicBool::new(false),
}
}
fn close(&self) {
if !self
.is_closed
.swap(true, Ordering::SeqCst)
{
if let Ok(mut state) = self.state.lock() {
state.closed = true;
}
self.data_notify.notify_waiters();
self.space_notify.notify_waiters();
}
}
}
/// Créé un channel broadcast temporisé.
pub fn channel<T>(capacity: usize) -> (Sender<T>, Receiver<T>) {
assert!(capacity > 0, "capacity must be > 0");
let inner = Arc::new(Inner::new(capacity));
let next_seq = {
let state = inner
.state
.lock()
.expect("timed broadcast mutex poisoned");
state.next_seq
};
let sender = Sender {
inner: inner.clone(),
};
let cursor = Arc::new(ReceiverCursor {
next_seq: AtomicU64::new(next_seq),
});
{
let mut state = inner
.state
.lock()
.expect("timed broadcast mutex poisoned");
state.cursors.push(Arc::downgrade(&cursor));
}
inner.receiver_count.store(1, Ordering::SeqCst);
let receiver = Receiver {
inner,
next_seq,
cursor,
};
(sender, receiver)
}
/// Sender côté producteur.
pub struct Sender<T> {
inner: Arc<Inner<T>>,
}
impl<T> Clone for Sender<T> {
fn clone(&self) -> Self {
self.inner.sender_count.fetch_add(1, Ordering::SeqCst);
Self {
inner: self.inner.clone(),
}
}
}
impl<T> Sender<T> {
/// Diffuse un paquet. Bloque si la capacité est atteinte avec des paquets non périmés.
pub async fn send(&self, payload: T, audio_timestamp: f64) -> Result<usize, SendError<T>>
where
T: Clone,
{
let mut payload = Some(payload);
loop {
let mut wait_deadline = None;
{
let now = Instant::now();
let mut state = self
.inner
.state
.lock()
.expect("timed broadcast mutex poisoned");
if state.closed {
return Err(SendError(payload.expect("payload already consumed")));
}
if state.purge_expired(now) {
self.inner.space_notify.notify_waiters();
}
if state.prune_consumed() {
self.inner.space_notify.notify_waiters();
}
if state.buffer.len() < self.inner.capacity {
let audio_offset =
Duration::from_secs_f64(audio_timestamp.max(0.0));
let expires_at = state.epoch_start + audio_offset;
let entry = Entry {
seq: state.next_seq,
expires_at,
payload: payload
.take()
.expect("payload already consumed"),
audio_timestamp,
epoch: state.epoch,
};
state.next_seq += 1;
state.buffer.push_back(entry);
let receivers = self.inner.receiver_count.load(Ordering::SeqCst);
drop(state);
self.inner.data_notify.notify_waiters();
return Ok(receivers);
}
wait_deadline = state.buffer.front().map(|entry| entry.expires_at);
}
if let Some(deadline) = wait_deadline {
let deadline = tokio::time::Instant::from_std(deadline);
tokio::select! {
_ = self.inner.space_notify.notified() => {},
_ = tokio::time::sleep_until(deadline) => {},
}
} else {
self.inner.space_notify.notified().await;
}
}
}
/// Crée un nouveau receiver abonné au flux.
pub fn subscribe(&self) -> Receiver<T> {
let mut state = self
.inner
.state
.lock()
.expect("timed broadcast mutex poisoned");
let next_seq = state.next_seq;
let cursor = Arc::new(ReceiverCursor {
next_seq: AtomicU64::new(next_seq),
});
state.cursors.push(Arc::downgrade(&cursor));
state.prune_consumed();
drop(state);
self.inner.receiver_count.fetch_add(1, Ordering::SeqCst);
Receiver {
inner: self.inner.clone(),
next_seq,
cursor,
}
}
/// Marque un TopZero : incrémente l'epoch pour les paquets suivants.
pub fn mark_top_zero(&self) {
let mut state = self
.inner
.state
.lock()
.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();
}
}
/// Nombre actuel de receivers abonnés.
pub fn receiver_count(&self) -> usize {
self.inner.receiver_count.load(Ordering::SeqCst)
}
/// Ferme explicitement le channel.
pub fn close(&self) {
self.inner.close();
}
}
impl<T> Drop for Sender<T> {
fn drop(&mut self) {
if self.inner.sender_count.fetch_sub(1, Ordering::SeqCst) == 1 {
self.inner.close();
}
}
}
/// Receiver côté consommateur.
pub struct Receiver<T> {
inner: Arc<Inner<T>>,
next_seq: u64,
cursor: Arc<ReceiverCursor>,
}
struct ReceiverCursor {
next_seq: AtomicU64,
}
impl<T> Receiver<T>
where
T: Clone,
{
fn poll_entry(&mut self) -> Result<TimedPacket<T>, TryRecvError> {
let mut state = self
.inner
.state
.lock()
.expect("timed broadcast mutex poisoned");
if state.closed && state.buffer.is_empty() {
return Err(TryRecvError::Closed);
}
let now = Instant::now();
if state.purge_expired(now) {
self.inner.space_notify.notify_waiters();
}
if self.next_seq < state.head_seq {
let skipped = state.head_seq - self.next_seq;
self.next_seq = state.head_seq;
return Err(TryRecvError::Lagged(skipped));
}
let offset = (self.next_seq - state.head_seq) as usize;
if offset < state.buffer.len() {
let entry = state
.buffer
.get(offset)
.expect("invalid buffer offset");
let packet = TimedPacket {
payload: entry.payload.clone(),
audio_timestamp: entry.audio_timestamp,
epoch: entry.epoch,
};
self.next_seq += 1;
self.cursor
.next_seq
.store(self.next_seq, Ordering::SeqCst);
if state.prune_consumed() {
self.inner.space_notify.notify_waiters();
}
return Ok(packet);
}
if state.closed {
Err(TryRecvError::Closed)
} else {
Err(TryRecvError::Empty)
}
}
/// Version synchrone utilisée dans `poll_read`.
pub fn try_recv(&mut self) -> Result<TimedPacket<T>, TryRecvError> {
self.poll_entry()
}
/// Attends qu'un paquet soit disponible.
pub async fn recv(&mut self) -> Result<TimedPacket<T>, RecvError> {
loop {
match self.try_recv() {
Ok(packet) => return Ok(packet),
Err(TryRecvError::Empty) => {
self.inner.data_notify.notified().await;
}
Err(TryRecvError::Lagged(skipped)) => return Err(RecvError::Lagged(skipped)),
Err(TryRecvError::Closed) => return Err(RecvError::Closed),
}
}
}
}
impl<T> Clone for Receiver<T> {
fn clone(&self) -> Self {
self.inner.receiver_count.fetch_add(1, Ordering::SeqCst);
let cursor = Arc::new(ReceiverCursor {
next_seq: AtomicU64::new(self.next_seq),
});
{
let mut state = self
.inner
.state
.lock()
.expect("timed broadcast mutex poisoned");
state.cursors.push(Arc::downgrade(&cursor));
}
Self {
inner: self.inner.clone(),
next_seq: self.next_seq,
cursor,
}
}
}
impl<T> Drop for Receiver<T> {
fn drop(&mut self) {
self.cursor
.next_seq
.store(self.next_seq, Ordering::SeqCst);
if let Ok(mut state) = self.inner.state.lock() {
if state.prune_consumed() {
self.inner.space_notify.notify_waiters();
}
}
if self.inner.receiver_count.fetch_sub(1, Ordering::SeqCst) == 1 {
self.inner.space_notify.notify_waiters();
}
}
}

View File

@@ -204,21 +204,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}; };
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
// Pipeline 1: FLAC streaming // Unique pipeline feeding both FLAC and OGG sinks
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
let mut source_flac = RadioParadiseStreamSource::new(client.clone()); let mut source = RadioParadiseStreamSource::new(client);
source_flac.push_block_id(block.event); source.push_block_id(block.event);
source_flac.push_block_id(END_OF_BLOCKS_SIGNAL); // Signal: no more blocks after this one source.push_block_id(END_OF_BLOCKS_SIGNAL); // Signal: no more blocks after this one
tracing::debug!( tracing::debug!(
"RadioParadiseStreamSource (FLAC) created with block {} + END signal", "RadioParadiseStreamSource created with block {} + END signal",
block.event block.event
); );
// Use SMALL channel size to make backpressure more reactive // Use SMALL channel size to make backpressure plus fan-out manageable.
// Instead of trying to buffer 3s of audio (60 chunks), use a much smaller buffer let buffer_sec = 0.1;
// This forces tighter backpressure control
let buffer_sec = 10.0;
let max_lead_time = buffer_sec; let max_lead_time = buffer_sec;
let channel_size = 512; let channel_size = 512;
tracing::debug!( tracing::debug!(
@@ -227,53 +225,31 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
channel_size as f64 * 0.05 channel_size as f64 * 0.05
); );
let mut timer_flac = TimerBufferNode::with_channel_size(buffer_sec, channel_size); let mut timer_node = TimerBufferNode::with_channel_size(buffer_sec, channel_size);
tracing::debug!( tracing::debug!(
"TimerBufferNode (FLAC) created with {:.1}s buffer, {} chunk queue", "TimerBufferNode created with {:.1}s buffer, {} chunk queue",
buffer_sec, buffer_sec,
channel_size channel_size
); );
// StreamingFlacSink doesn't take channel_size - it uses bits_per_sample (16, 24, or 32) // Streaming sinks
let (streaming_sink, stream_handle) = let (streaming_sink, stream_handle) =
StreamingFlacSink::with_max_broadcast_lead(encoder_options.clone(), 16, max_lead_time); StreamingFlacSink::with_max_broadcast_lead(encoder_options.clone(), 16, max_lead_time);
tracing::debug!("StreamingFlacSink created"); tracing::debug!("StreamingFlacSink created");
timer_flac.register(Box::new(streaming_sink));
source_flac.register(Box::new(timer_flac));
tracing::info!(
"Pipeline 1 connected: RadioParadiseStreamSource → TimerBufferNode → StreamingFlacSink"
);
// ─────────────────────────────────────────────────────────────────────────
// Pipeline 2: OGG-FLAC streaming
// ─────────────────────────────────────────────────────────────────────────
let mut source_ogg = RadioParadiseStreamSource::new(client);
source_ogg.push_block_id(block.event);
source_ogg.push_block_id(END_OF_BLOCKS_SIGNAL); // Signal: no more blocks after this one
tracing::debug!(
"RadioParadiseStreamSource (OGG) created with block {} + END signal",
block.event
);
let mut timer_ogg = TimerBufferNode::with_channel_size(buffer_sec, channel_size);
tracing::debug!(
"TimerBufferNode (OGG) created with {:.1}s buffer, {} chunk queue",
buffer_sec,
channel_size
);
// StreamingOggFlacSink doesn't take channel_size - it uses bits_per_sample (16, 24, or 32)
let (ogg_sink, ogg_handle) = let (ogg_sink, ogg_handle) =
StreamingOggFlacSink::with_max_broadcast_lead(encoder_options, 16, max_lead_time); StreamingOggFlacSink::with_max_broadcast_lead(encoder_options, 16, max_lead_time);
tracing::debug!("StreamingOggFlacSink created"); tracing::debug!("StreamingOggFlacSink created");
timer_ogg.register(Box::new(ogg_sink)); // timer_node.register(Box::new(streaming_sink));
source_ogg.register(Box::new(timer_ogg)); // timer_node.register(Box::new(ogg_sink));
// source.register(Box::new(timer_node));
source.register(Box::new(streaming_sink));
source.register(Box::new(ogg_sink));
tracing::info!( tracing::info!(
"Pipeline 2 connected: RadioParadiseStreamSource → TimerBufferNode → StreamingOggFlacSink" "Pipeline connected: StreamSource → TimerBufferNode → {{FLAC, OGG}} sinks"
); );
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
@@ -332,27 +308,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════
let stop_token = CancellationToken::new(); let stop_token = CancellationToken::new();
let stop_token_flac = stop_token.clone(); let pipeline_stop = stop_token.clone();
let stop_token_ogg = stop_token.clone();
// Start FLAC pipeline in background // Start shared pipeline in background
let pipeline_flac_handle = tokio::spawn(async move { let pipeline_handle = tokio::spawn(async move {
tracing::info!("[PIPELINE-FLAC] Starting..."); tracing::info!("[PIPELINE] Starting...");
let result = Box::new(source_flac).run(stop_token_flac).await; let result = Box::new(source).run(pipeline_stop).await;
match &result { match &result {
Ok(()) => tracing::info!("[PIPELINE-FLAC] Completed successfully"), Ok(()) => tracing::info!("[PIPELINE] Completed successfully"),
Err(e) => tracing::error!("[PIPELINE-FLAC] Error: {}", e), Err(e) => tracing::error!("[PIPELINE] Error: {}", e),
}
result
});
// Start OGG-FLAC pipeline in background
let pipeline_ogg_handle = tokio::spawn(async move {
tracing::info!("[PIPELINE-OGG] Starting...");
let result = Box::new(source_ogg).run(stop_token_ogg).await;
match &result {
Ok(()) => tracing::info!("[PIPELINE-OGG] Completed successfully"),
Err(e) => tracing::error!("[PIPELINE-OGG] Error: {}", e),
} }
result result
}); });
@@ -366,17 +330,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing::info!("Server stopped, canceling pipelines..."); tracing::info!("Server stopped, canceling pipelines...");
stop_token.cancel(); stop_token.cancel();
// Wait for both pipelines to finish // Wait for pipeline to finish
match pipeline_flac_handle.await { match pipeline_handle.await {
Ok(Ok(())) => tracing::info!("FLAC pipeline completed successfully"), Ok(Ok(())) => tracing::info!("Pipeline completed successfully"),
Ok(Err(e)) => tracing::error!("FLAC pipeline error: {}", e), Ok(Err(e)) => tracing::error!("Pipeline error: {}", e),
Err(e) => tracing::error!("FLAC pipeline task error: {}", e), Err(e) => tracing::error!("Pipeline task error: {}", e),
}
match pipeline_ogg_handle.await {
Ok(Ok(())) => tracing::info!("OGG-FLAC pipeline completed successfully"),
Ok(Err(e)) => tracing::error!("OGG-FLAC pipeline error: {}", e),
Err(e) => tracing::error!("OGG-FLAC pipeline task error: {}", e),
} }
tracing::info!("Shutdown complete"); tracing::info!("Shutdown complete");