fin du debuggage des sink

This commit is contained in:
2025-11-20 21:28:37 +01:00
parent 7fd8d50b8b
commit 75e4ad28b7
6 changed files with 65 additions and 21 deletions

View File

@@ -430,7 +430,7 @@ impl StreamingFlacSink {
); );
// Broadcast channel for FLAC bytes // Broadcast channel for FLAC bytes
let (broadcast, _) = timed_broadcast::channel(broadcast_capacity); let (broadcast, _) = timed_broadcast::channel("Flac", broadcast_capacity);
// FLAC header cache // FLAC header cache
let header = Arc::new(RwLock::new(None)); let header = Arc::new(RwLock::new(None));

View File

@@ -178,6 +178,18 @@ impl NodeLogic for StreamingOggFlacSinkLogic {
Some(seg) => { Some(seg) => {
match &seg.segment { match &seg.segment {
_AudioSegment::Chunk(chunk) => { _AudioSegment::Chunk(chunk) => {
if !self.ctx.first_chunk_timestamp_checked {
self.ctx.first_chunk_timestamp_checked = true;
if seg.timestamp_sec.abs() > 1e-6 {
warn!(
"StreamingFlacSink: first chunk timestamp is {:.6}s (expected 0.0)",
seg.timestamp_sec
);
} else {
trace!("StreamingFlacSink: first chunk timestamp verified at 0.0s");
}
}
// Detect sample rate from first chunk and initialize encoder // Detect sample rate from first chunk and initialize encoder
if self.ctx.sample_rate.is_none() { if self.ctx.sample_rate.is_none() {
let sample_rate = chunk.sample_rate(); let sample_rate = chunk.sample_rate();
@@ -375,7 +387,7 @@ impl StreamingOggFlacSink {
"Streaming Sink: using broadcast capacity of {} items (max_lead_time={:.1}s)", "Streaming Sink: using broadcast capacity of {} items (max_lead_time={:.1}s)",
broadcast_capacity, broadcast_max_lead_time broadcast_capacity, broadcast_max_lead_time
); );
let (broadcast, _) = timed_broadcast::channel(broadcast_capacity); let (broadcast, _) = timed_broadcast::channel("Ogg-Flac",broadcast_capacity);
// OGG-FLAC header cache // OGG-FLAC header cache
let header = Arc::new(RwLock::new(None)); let header = Arc::new(RwLock::new(None));

View File

@@ -347,7 +347,10 @@ impl SharedSinkContext {
self.pcm_tx = Some(pcm_tx); self.pcm_tx = Some(pcm_tx);
self.pcm_rx = Some(pcm_rx); self.pcm_rx = Some(pcm_rx);
self.initialize_encoder(sample_rate, self.timestamp_offset_sec, broadcaster) // self.initialize_encoder(sample_rate, self.timestamp_offset_sec, broadcaster)
// .await?;
self.initialize_encoder(sample_rate, 0.0, broadcaster)
.await?; .await?;
debug!("FLAC encoder restarted successfully for new track"); debug!("FLAC encoder restarted successfully for new track");

View File

@@ -5,17 +5,13 @@
//! - Propagation dun compteur `epoch` incrémenté sur chaque TopZeroSync. //! - Propagation dun compteur `epoch` incrémenté sur chaque TopZeroSync.
use std::{ use std::{
collections::VecDeque, collections::VecDeque, fmt, string, sync::{
fmt, Arc, Mutex, Weak, atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}
sync::{ }, time::{Duration, Instant}
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
Arc, Mutex, Weak,
},
time::{Duration, Instant},
}; };
use tokio::sync::Notify; use tokio::sync::Notify;
use tracing::{info, trace, warn}; use tracing::{info, debug, trace, warn};
/// Tolérance pour détecter un timestamp à zéro (TopZero). /// Tolérance pour détecter un timestamp à zéro (TopZero).
const TOP_ZERO_EPSILON: f64 = 1e-9; const TOP_ZERO_EPSILON: f64 = 1e-9;
@@ -83,6 +79,7 @@ struct Entry<T> {
} }
struct State<T> { struct State<T> {
name: String,
buffer: VecDeque<Entry<T>>, buffer: VecDeque<Entry<T>>,
head_seq: u64, head_seq: u64,
next_seq: u64, next_seq: u64,
@@ -96,8 +93,9 @@ struct State<T> {
} }
impl<T> State<T> { impl<T> State<T> {
fn new(capacity: usize, epoch_start: Instant) -> Self { fn new(name: &str,capacity: usize, epoch_start: Instant) -> Self {
Self { Self {
name: name.to_string() ,
buffer: VecDeque::with_capacity(capacity), buffer: VecDeque::with_capacity(capacity),
head_seq: 0, head_seq: 0,
next_seq: 0, next_seq: 0,
@@ -112,8 +110,8 @@ impl<T> State<T> {
} }
fn purge_expired(&mut self, now: Instant) -> bool { fn purge_expired(&mut self, now: Instant) -> bool {
// Throttling : purger au maximum toutes les 100ms // Throttling : purger au maximum toutes les 20ms
if now.duration_since(self.last_purge) < Duration::from_millis(100) { if now.duration_since(self.last_purge) < Duration::from_millis(20) {
return false; return false;
} }
self.last_purge = now; self.last_purge = now;
@@ -122,7 +120,7 @@ impl<T> State<T> {
while let Some(entry) = self.buffer.front() { while let Some(entry) = self.buffer.front() {
if entry.expires_at <= now { if entry.expires_at <= now {
let delta = now - entry.expires_at; let delta = now - entry.expires_at;
// info!("TimedBroadcast: purging expired packet (epoch={},delta={})", entry.epoch, delta.as_millis()); debug!("TimedBroadcast[{}]: purging expired packet (@{} epoch={},delta={})", self.name,entry.seq, entry.epoch, delta.as_millis());
self.buffer.pop_front(); self.buffer.pop_front();
self.head_seq += 1; self.head_seq += 1;
purged += 1; purged += 1;
@@ -167,7 +165,11 @@ impl<T> State<T> {
} }
for _ in 0..removable { for _ in 0..removable {
if self.buffer.pop_front().is_some() { let oentry = self.buffer.pop_front();
if oentry.is_some() {
let entry= oentry.unwrap();
debug!("TimedBroadcast[{}]: pruning played packet (@{} epoch={})", self.name,entry.seq, entry.epoch);
self.head_seq += 1; self.head_seq += 1;
} }
} }
@@ -186,9 +188,9 @@ struct Inner<T> {
} }
impl<T> Inner<T> { impl<T> Inner<T> {
fn new(capacity: usize) -> Self { fn new(name: &str,capacity: usize) -> Self {
Self { Self {
state: Mutex::new(State::new(capacity, Instant::now())), state: Mutex::new(State::new(name,capacity, Instant::now())),
data_notify: Notify::new(), data_notify: Notify::new(),
space_notify: Notify::new(), space_notify: Notify::new(),
capacity, capacity,
@@ -210,9 +212,9 @@ impl<T> Inner<T> {
} }
/// Créé un channel broadcast temporisé. /// Créé un channel broadcast temporisé.
pub fn channel<T>(capacity: usize) -> (Sender<T>, Receiver<T>) { pub fn channel<T>(name: &str, capacity: usize) -> (Sender<T>, Receiver<T>) {
assert!(capacity > 0, "capacity must be > 0"); assert!(capacity > 0, "capacity must be > 0");
let inner = Arc::new(Inner::new(capacity)); let inner = Arc::new(Inner::new(name,capacity));
let next_seq = { let next_seq = {
let state = inner.state.lock().expect("timed broadcast mutex poisoned"); let state = inner.state.lock().expect("timed broadcast mutex poisoned");
state.next_seq state.next_seq
@@ -312,7 +314,10 @@ impl<T> Sender<T> {
audio_timestamp audio_timestamp
); );
} else if is_top_zero { } else if is_top_zero {
// Restart epoch on TopZero relative to current wall-clock time to avoid
// expired packets when there's a long gap between tracks.
state.epoch_start = state.last_segment_end.unwrap_or(now); state.epoch_start = state.last_segment_end.unwrap_or(now);
// state.epoch_start = now;
state.epoch = state.epoch.wrapping_add(1); state.epoch = state.epoch.wrapping_add(1);
info!( info!(
"TimedBroadcast: new epoch={} (continuous={})", "TimedBroadcast: new epoch={} (continuous={})",

View File

@@ -79,11 +79,13 @@ async fn main() -> anyhow::Result<()> {
let app = Router::new() let app = Router::new()
.route("/stream/flac", get(stream_flac)) .route("/stream/flac", get(stream_flac))
.route("/stream/ogg", get(stream_ogg))
.with_state(state); .with_state(state);
let addr: SocketAddr = ([0, 0, 0, 0], 8080).into(); let addr: SocketAddr = ([0, 0, 0, 0], 8080).into();
info!("HTTP server listening on http://{addr}/stream/flac"); info!("HTTP server listening on http://{addr}/stream/flac and /stream/ogg");
info!("Connect with a FLAC player (e.g. ffplay http://localhost:8080/stream/flac)"); info!("Connect with a FLAC player (e.g. ffplay http://localhost:8080/stream/flac)");
info!("Connect with an OGG/OGG-FLAC player (e.g. ffplay http://localhost:8080/stream/ogg)");
let listener = TcpListener::bind(addr).await?; let listener = TcpListener::bind(addr).await?;
axum::serve(listener, app.into_make_service()).await?; axum::serve(listener, app.into_make_service()).await?;
@@ -108,6 +110,23 @@ async fn stream_flac(State(state): State<AppState>) -> Result<Response, StatusCo
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
} }
async fn stream_ogg(State(state): State<AppState>) -> Result<Response, StatusCode> {
let stream = state.channel.subscribe_ogg();
let body = Body::from_stream(ReaderStream::new(stream));
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "audio/ogg")
.header(
"X-PMO-Channel",
format!(
"{} ({})",
state.descriptor.display_name, state.descriptor.slug
),
)
.body(body)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
fn pick_descriptor(arg: Option<String>) -> anyhow::Result<ChannelDescriptor> { fn pick_descriptor(arg: Option<String>) -> anyhow::Result<ChannelDescriptor> {
if let Some(token) = arg { if let Some(token) = arg {
if let Some(desc) = ALL_CHANNELS.iter().find(|c| c.slug == token) { if let Some(desc) = ALL_CHANNELS.iter().find(|c| c.slug == token) {

View File

@@ -376,6 +376,11 @@ mod tests {
cover: None, cover: None,
rating: None, rating: None,
extra: HashMap::new(), extra: HashMap::new(),
gapless_url: todo!(),
sched_time_millis: todo!(),
song_id: todo!(),
artist_id: todo!(),
cover_large: todo!(),
}; };
assert_eq!(song.end_time_ms(), 6000); assert_eq!(song.end_time_ms(), 6000);