Prblen enxt chanson en flac

This commit is contained in:
2025-11-22 04:59:29 +01:00
parent e00d0ce4d8
commit a51d7c551e
10 changed files with 272 additions and 86 deletions

View File

@@ -6,13 +6,14 @@
//! - Paces broadcast to match audio playback rate
use std::time::Instant;
use tracing::{trace, warn};
use tracing::trace;
/// Error returned when a frame should be skipped (too late)
#[derive(Debug)]
pub struct SkipFrame;
/// Manages broadcast pacing with TopZeroSync detection
#[allow(dead_code)]
pub struct BroadcastPacer {
/// Start time (reset on TopZeroSync)
start_time: Instant,

View File

@@ -42,4 +42,4 @@ mod streaming_ogg_flac_sink;
pub use streaming_ogg_flac_sink::{OggFlacClientStream, OggFlacStreamHandle, StreamingOggFlacSink};
#[cfg(feature = "http-stream")]
pub use streaming_sink_common::MetadataSnapshot;
pub use streaming_sink_common::{MetadataSnapshot, StreamingSinkOptions};

View File

@@ -54,7 +54,6 @@
//! }
//! ```
use std::collections::VecDeque;
use std::io;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -65,7 +64,7 @@ use std::time::Duration;
use super::{
broadcast_pacing::BroadcastPacer,
flac_frame_utils,
timed_broadcast::{self, SendError, TryRecvError},
timed_broadcast::{self, SendError},
};
use async_trait::async_trait;
use bytes::Bytes;
@@ -77,12 +76,13 @@ use pmoflac::{EncoderOptions, FlacEncodedStream};
use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
use tokio::sync::{mpsc, RwLock};
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, trace, warn};
use tracing::{debug, error, trace, warn};
use crate::byte_stream_reader::{PcmChunk};
use crate::chunk_to_pcm::chunk_to_pcm_bytes;
use crate::sinks::streaming_sink_common::{
MetadataSnapshot, SharedClientStream, SharedSinkContext, SharedStreamHandleInner,
StreamingSinkOptions,
};
use crate::sinks::timed_broadcast::{
calculate_broadcast_capacity, DEFAULT_BROADCAST_MAX_LEAD_TIME,
@@ -214,8 +214,8 @@ impl NodeLogic for StreamingFlacSinkLogic {
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
"StreamingFlacSink: first chunk timestamp is {:.3}ms (expected 0.0)",
seg.timestamp_sec * 1000.0
);
} else {
trace!("StreamingFlacSink: first chunk timestamp verified at 0.0s");
@@ -318,39 +318,53 @@ impl NodeLogic for StreamingFlacSinkLogic {
debug!("StreamingFlacSink: SyncMarker::TrackBoundary {:?}",metadata.read().await.get_duration().await);
// Only restart encoder if it's already initialized (not the first track)
if self.ctx.sample_rate.is_some() && self.ctx.encoder_state.is_some() {
// Restart encoder to emit new header and reset timestamps
if let Err(e) = self
.ctx
.restart_encoder_for_new_track(
|flac_stream,
broadcast,
header,
current_timestamp,
current_duration,
max_lead,
sample_rate,
timestamp_offset_sec| {
broadcast_flac_stream(
flac_stream,
broadcast,
header,
current_timestamp,
current_duration,
max_lead,
sample_rate,
timestamp_offset_sec,
)
},
)
.await
if self.ctx.restart_encoder_on_track_boundary {
// Only restart encoder if it's already initialized (not the first track)
if self.ctx.sample_rate.is_some()
&& self.ctx.encoder_state.is_some()
{
error!("Failed to restart encoder for new track: {}", e);
break;
// Restart encoder to emit new header and reset timestamps
if let Err(e) = self
.ctx
.restart_encoder_for_new_track(
|flac_stream,
broadcast,
header,
current_timestamp,
current_duration,
max_lead,
sample_rate,
timestamp_offset_sec| {
broadcast_flac_stream(
flac_stream,
broadcast,
header,
current_timestamp,
current_duration,
max_lead,
sample_rate,
timestamp_offset_sec,
)
},
)
.await
{
error!(
"Failed to restart encoder for new track: {}",
e
);
break;
}
} else {
trace!("Skipping encoder restart for first track (encoder not yet initialized)");
}
} else {
trace!("Skipping encoder restart for first track (encoder not yet initialized)");
// For raw FLAC streaming we keep a single continuous encoder.
// Restarting would insert a new STREAMINFO header mid-stream and many
// clients treat that as end-of-file.
trace!(
"StreamingFlacSink: keeping encoder alive across track boundary"
);
}
// Update metadata for the new track
@@ -422,6 +436,21 @@ impl StreamingFlacSink {
encoder_options: EncoderOptions,
bits_per_sample: u8,
broadcast_max_lead_time: f64,
) -> (Self, StreamHandle) {
Self::with_options(
encoder_options,
bits_per_sample,
broadcast_max_lead_time,
StreamingSinkOptions::flac_defaults(),
)
}
/// Create a sink with a custom broadcast pacing limit and options.
pub fn with_options(
encoder_options: EncoderOptions,
bits_per_sample: u8,
broadcast_max_lead_time: f64,
options: StreamingSinkOptions,
) -> (Self, StreamHandle) {
// Validate bit depth
if ![16, 24, 32].contains(&bits_per_sample) {
@@ -465,6 +494,11 @@ impl StreamingFlacSink {
ctx: SharedSinkContext {
encoder_options,
bits_per_sample,
enable_total_samples: options.enable_total_samples,
restart_encoder_on_track_boundary: options.restart_encoder_on_track_boundary,
default_title: options.default_title.clone(),
default_artist: options.default_artist.clone(),
use_only_default_metadata: options.use_only_default_metadata,
pcm_tx: Some(pcm_tx),
pcm_rx: Some(pcm_rx),
metadata,

View File

@@ -6,7 +6,7 @@ use crate::{MetadataSnapshot, sinks::{flac_frame_utils::FlacStreamState, streami
use bytes::Bytes;
use std::io;
use tracing::{debug, error, info, trace, warn};
use tracing::{debug, warn};
/// ICY-wrapped FLAC client stream (implements AsyncRead).
///
@@ -97,6 +97,7 @@ impl IcyClientStream {
}
/// Get metadata block if it needs to be inserted.
#[allow(dead_code)]
async fn get_metadata_if_changed(&mut self) -> Option<Bytes> {
let meta = self.metadata.read().await;
if meta.version > self.current_metadata_version {

View File

@@ -46,7 +46,6 @@
//! - Pages are broadcast immediately to connected clients
//! - TrackBoundary only triggers encoder flush (no data accumulation)
use std::collections::VecDeque;
use std::io;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -56,7 +55,7 @@ use std::task::{Context, Poll};
use super::{
broadcast_pacing::BroadcastPacer,
flac_frame_utils,
timed_broadcast::{self, SendError, TryRecvError},
timed_broadcast::{self, SendError},
};
use async_trait::async_trait;
use bytes::Bytes;
@@ -68,13 +67,14 @@ use pmoflac::{EncoderOptions, FlacEncodedStream};
use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
use tokio::sync::{mpsc, RwLock};
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, trace, warn};
use tracing::{debug, error, trace, warn};
use crate::byte_stream_reader::{PcmChunk};
use crate::chunk_to_pcm::chunk_to_pcm_bytes;
use crate::sinks::flac_frame_utils::{extract_sample_rate_from_streaminfo, read_flac_header};
use crate::sinks::streaming_sink_common::{
MetadataSnapshot, SharedClientStream, SharedSinkContext, SharedStreamHandleInner,
StreamingSinkOptions,
};
use crate::sinks::timed_broadcast::{
calculate_broadcast_capacity, DEFAULT_BROADCAST_MAX_LEAD_TIME,
@@ -210,7 +210,7 @@ impl NodeLogic for StreamingOggFlacSinkLogic {
current_timestamp,
current_duration,
max_lead,
sample_rate,
_sample_rate,
timestamp_offset_sec| {
broadcast_ogg_flac_stream(
flac_stream,
@@ -273,38 +273,44 @@ impl NodeLogic for StreamingOggFlacSinkLogic {
error!("Failed to prepare encoder options for new track: {}", e);
}
// Only restart encoder if it's already initialized (not the first track)
if self.ctx.sample_rate.is_some() && self.ctx.encoder_state.is_some() {
// Restart encoder to emit new OGG stream header and reset timestamps
if let Err(e) = self
.ctx
.restart_encoder_for_new_track(
|flac_stream,
broadcast,
header,
current_timestamp,
current_duration,
max_lead,
sample_rate,
timestamp_offset_sec| {
broadcast_ogg_flac_stream(
flac_stream,
broadcast,
header,
current_timestamp,
current_duration,
max_lead,
timestamp_offset_sec,
)
},
)
.await
if self.ctx.restart_encoder_on_track_boundary {
// Only restart encoder if it's already initialized (not the first track)
if self.ctx.sample_rate.is_some()
&& self.ctx.encoder_state.is_some()
{
error!("Failed to restart OGG encoder for new track: {}", e);
break;
// Restart encoder to emit new OGG stream header and reset timestamps
if let Err(e) = self
.ctx
.restart_encoder_for_new_track(
|flac_stream,
broadcast,
header,
current_timestamp,
current_duration,
max_lead,
_sample_rate,
timestamp_offset_sec| {
broadcast_ogg_flac_stream(
flac_stream,
broadcast,
header,
current_timestamp,
current_duration,
max_lead,
timestamp_offset_sec,
)
},
)
.await
{
error!("Failed to restart OGG encoder for new track: {}", e);
break;
}
} else {
trace!("Skipping OGG encoder restart for first track (encoder not yet initialized)");
}
} else {
trace!("Skipping OGG encoder restart for first track (encoder not yet initialized)");
trace!("StreamingOggFlacSink: restart disabled; continuing encoder across track boundary");
}
// Update metadata for the new track
@@ -379,6 +385,21 @@ impl StreamingOggFlacSink {
encoder_options: EncoderOptions,
bits_per_sample: u8,
broadcast_max_lead_time: f64,
) -> (Self, OggFlacStreamHandle) {
Self::with_options(
encoder_options,
bits_per_sample,
broadcast_max_lead_time,
StreamingSinkOptions::ogg_defaults(),
)
}
/// Create a sink with a custom broadcast pacing limit and options.
pub fn with_options(
encoder_options: EncoderOptions,
bits_per_sample: u8,
broadcast_max_lead_time: f64,
options: StreamingSinkOptions,
) -> (Self, OggFlacStreamHandle) {
// Validate bit depth
if ![16, 24, 32].contains(&bits_per_sample) {
@@ -420,6 +441,11 @@ impl StreamingOggFlacSink {
ctx: SharedSinkContext {
encoder_options,
bits_per_sample,
enable_total_samples: options.enable_total_samples,
restart_encoder_on_track_boundary: options.restart_encoder_on_track_boundary,
default_title: options.default_title.clone(),
default_artist: options.default_artist.clone(),
use_only_default_metadata: options.use_only_default_metadata,
pcm_tx: Some(pcm_tx),
pcm_rx: Some(pcm_rx),
metadata,
@@ -494,9 +520,7 @@ async fn broadcast_ogg_flac_stream(
let mut ogg_writer = OggPageWriter::new(stream_serial);
let mut total_bytes = 0u64;
let mut header_captured = false;
let mut pacer = BroadcastPacer::new(broadcast_max_lead_time, "OGG");
let mut last_granule_update_time = 0.0f64;
// Timing instrumentation for burst detection
let mut last_broadcast_time = std::time::Instant::now();
@@ -538,7 +562,6 @@ async fn broadcast_ogg_flac_stream(
cached_header.extend_from_slice(&bos_bytes);
cached_header.extend_from_slice(&comment_bytes);
*header_cache.write().await = Some(Bytes::from(cached_header));
header_captured = true;
trace!(
"OGG-FLAC header cached ({} bytes: BOS + Vorbis Comment)",
bos_bytes.len() + comment_bytes.len()

View File

@@ -45,6 +45,63 @@ pub struct MetadataSnapshot {
pub version: u64,
}
/// Configuration options shared by streaming sinks.
#[derive(Clone, Debug)]
pub struct StreamingSinkOptions {
pub restart_encoder_on_track_boundary: bool,
pub enable_total_samples: bool,
pub default_title: Option<String>,
pub default_artist: Option<String>,
pub use_only_default_metadata: bool,
}
impl StreamingSinkOptions {
pub fn flac_defaults() -> Self {
Self {
restart_encoder_on_track_boundary: false,
enable_total_samples: false,
default_title: None,
default_artist: None,
use_only_default_metadata: false,
}
}
pub fn ogg_defaults() -> Self {
Self {
restart_encoder_on_track_boundary: true,
enable_total_samples: true,
default_title: None,
default_artist: None,
use_only_default_metadata: false,
}
}
pub fn with_restart(mut self, restart: bool) -> Self {
self.restart_encoder_on_track_boundary = restart;
self
}
pub fn with_total_samples(mut self, enable: bool) -> Self {
self.enable_total_samples = enable;
self
}
pub fn with_default_title(mut self, title: impl Into<Option<String>>) -> Self {
self.default_title = title.into();
self
}
pub fn with_default_artist(mut self, artist: impl Into<Option<String>>) -> Self {
self.default_artist = artist.into();
self
}
pub fn with_only_default_metadata(mut self, only_default: bool) -> Self {
self.use_only_default_metadata = only_default;
self
}
}
/// Shared handle state for streaming sinks.
pub struct SharedStreamHandleInner {
pub broadcast: timed_broadcast::Sender<Bytes>,
@@ -202,6 +259,14 @@ pub struct EncoderState {
pub struct SharedSinkContext {
pub encoder_options: EncoderOptions,
pub bits_per_sample: u8,
/// Whether to propagate total_samples into STREAMINFO.
/// For unbounded live streams (raw FLAC), this must stay false to avoid
/// players stopping after they reach the advertised length.
pub enable_total_samples: bool,
pub restart_encoder_on_track_boundary: bool,
pub default_title: Option<String>,
pub default_artist: Option<String>,
pub use_only_default_metadata: bool,
pub pcm_tx: Option<mpsc::Sender<PcmChunk>>,
pub pcm_rx: Option<mpsc::Receiver<PcmChunk>>,
pub metadata: Arc<RwLock<MetadataSnapshot>>,
@@ -311,6 +376,15 @@ impl SharedSinkContext {
// Always pass the metadata handle to the encoder so Vorbis comments are emitted.
self.encoder_options.metadata = Some(metadata_lock.clone());
// In raw FLAC live streaming we must NOT advertise a total_samples value,
// otherwise players think the stream ends after the first track.
if !self.enable_total_samples {
self.pending_track_duration = None;
self.pending_total_samples = None;
self.encoder_options.total_samples = None;
return Ok(());
}
// Capture duration (if any) to set total_samples.
let duration_opt = {
let metadata = metadata_lock.read().await;
@@ -339,6 +413,12 @@ impl SharedSinkContext {
self.sample_rate
);
if !self.enable_total_samples {
self.encoder_options.total_samples = None;
info!("Encoder metadata: total_samples disabled for live streaming");
return;
}
if let Some(total) = self.pending_total_samples {
self.encoder_options.total_samples = Some(total);
info!(
@@ -430,8 +510,24 @@ impl SharedSinkContext {
let metadata = metadata_lock.read().await;
let mut snapshot = self.metadata.write().await;
snapshot.title = metadata.get_title().await.ok().flatten();
snapshot.artist = metadata.get_artist().await.ok().flatten();
// Title / artist with default fallback or forced default.
if self.use_only_default_metadata {
snapshot.title = self.default_title.clone();
snapshot.artist = self.default_artist.clone();
} else {
snapshot.title = metadata
.get_title()
.await
.ok()
.flatten()
.or_else(|| self.default_title.clone());
snapshot.artist = metadata
.get_artist()
.await
.ok()
.flatten()
.or_else(|| self.default_artist.clone());
}
snapshot.album = metadata.get_album().await.ok().flatten();
snapshot.duration = metadata.get_duration().await.ok().flatten();
snapshot.cover_url = metadata.get_cover_url().await.ok().flatten();
@@ -443,6 +539,9 @@ impl SharedSinkContext {
snapshot.track_number = extra
.get("track_number")
.and_then(|s| s.parse::<u32>().ok());
} else {
snapshot.genre = None;
snapshot.track_number = None;
}
snapshot.audio_timestamp_sec = timestamp_sec;

View File

@@ -6,7 +6,7 @@
use std::{
collections::VecDeque,
fmt, string,
fmt,
sync::{
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
Arc, Mutex, Weak,
@@ -15,7 +15,7 @@ use std::{
};
use tokio::sync::Notify;
use tracing::{debug, info, trace, warn};
use tracing::{info, trace, warn};
/// Tolérance pour détecter un timestamp à zéro (TopZero).
const TOP_ZERO_EPSILON: f64 = 1e-9;