Prblen enxt chanson en flac
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user