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

View File

@@ -42,4 +42,4 @@ mod streaming_ogg_flac_sink;
pub use streaming_ogg_flac_sink::{OggFlacClientStream, OggFlacStreamHandle, StreamingOggFlacSink}; pub use streaming_ogg_flac_sink::{OggFlacClientStream, OggFlacStreamHandle, StreamingOggFlacSink};
#[cfg(feature = "http-stream")] #[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::io;
use std::pin::Pin; use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@@ -65,7 +64,7 @@ use std::time::Duration;
use super::{ use super::{
broadcast_pacing::BroadcastPacer, broadcast_pacing::BroadcastPacer,
flac_frame_utils, flac_frame_utils,
timed_broadcast::{self, SendError, TryRecvError}, timed_broadcast::{self, SendError},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
@@ -77,12 +76,13 @@ use pmoflac::{EncoderOptions, FlacEncodedStream};
use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf}; use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf};
use tokio::sync::{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, trace, warn};
use crate::byte_stream_reader::{PcmChunk}; use crate::byte_stream_reader::{PcmChunk};
use crate::chunk_to_pcm::chunk_to_pcm_bytes; use crate::chunk_to_pcm::chunk_to_pcm_bytes;
use crate::sinks::streaming_sink_common::{ use crate::sinks::streaming_sink_common::{
MetadataSnapshot, SharedClientStream, SharedSinkContext, SharedStreamHandleInner, MetadataSnapshot, SharedClientStream, SharedSinkContext, SharedStreamHandleInner,
StreamingSinkOptions,
}; };
use crate::sinks::timed_broadcast::{ use crate::sinks::timed_broadcast::{
calculate_broadcast_capacity, DEFAULT_BROADCAST_MAX_LEAD_TIME, calculate_broadcast_capacity, DEFAULT_BROADCAST_MAX_LEAD_TIME,
@@ -214,8 +214,8 @@ impl NodeLogic for StreamingFlacSinkLogic {
self.ctx.first_chunk_timestamp_checked = true; self.ctx.first_chunk_timestamp_checked = true;
if seg.timestamp_sec.abs() > 1e-6 { if seg.timestamp_sec.abs() > 1e-6 {
warn!( warn!(
"StreamingFlacSink: first chunk timestamp is {:.6}s (expected 0.0)", "StreamingFlacSink: first chunk timestamp is {:.3}ms (expected 0.0)",
seg.timestamp_sec seg.timestamp_sec * 1000.0
); );
} else { } else {
trace!("StreamingFlacSink: first chunk timestamp verified at 0.0s"); trace!("StreamingFlacSink: first chunk timestamp verified at 0.0s");
@@ -318,8 +318,11 @@ impl NodeLogic for StreamingFlacSinkLogic {
debug!("StreamingFlacSink: SyncMarker::TrackBoundary {:?}",metadata.read().await.get_duration().await); debug!("StreamingFlacSink: SyncMarker::TrackBoundary {:?}",metadata.read().await.get_duration().await);
if self.ctx.restart_encoder_on_track_boundary {
// Only restart encoder if it's already initialized (not the first track) // 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() { if self.ctx.sample_rate.is_some()
&& self.ctx.encoder_state.is_some()
{
// Restart encoder to emit new header and reset timestamps // Restart encoder to emit new header and reset timestamps
if let Err(e) = self if let Err(e) = self
.ctx .ctx
@@ -346,12 +349,23 @@ impl NodeLogic for StreamingFlacSinkLogic {
) )
.await .await
{ {
error!("Failed to restart encoder for new track: {}", e); error!(
"Failed to restart encoder for new track: {}",
e
);
break; break;
} }
} else { } else {
trace!("Skipping encoder restart for first track (encoder not yet initialized)"); trace!("Skipping encoder restart for first track (encoder not yet initialized)");
} }
} else {
// 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 // Update metadata for the new track
if let Err(e) = self.ctx.update_metadata(metadata, seg.timestamp_sec).await { if let Err(e) = self.ctx.update_metadata(metadata, seg.timestamp_sec).await {
@@ -422,6 +436,21 @@ impl StreamingFlacSink {
encoder_options: EncoderOptions, encoder_options: EncoderOptions,
bits_per_sample: u8, bits_per_sample: u8,
broadcast_max_lead_time: f64, 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) { ) -> (Self, StreamHandle) {
// Validate bit depth // Validate bit depth
if ![16, 24, 32].contains(&bits_per_sample) { if ![16, 24, 32].contains(&bits_per_sample) {
@@ -465,6 +494,11 @@ impl StreamingFlacSink {
ctx: SharedSinkContext { ctx: SharedSinkContext {
encoder_options, encoder_options,
bits_per_sample, 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_tx: Some(pcm_tx),
pcm_rx: Some(pcm_rx), pcm_rx: Some(pcm_rx),
metadata, metadata,

View File

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

View File

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

View File

@@ -45,6 +45,63 @@ pub struct MetadataSnapshot {
pub version: u64, 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. /// Shared handle state for streaming sinks.
pub struct SharedStreamHandleInner { pub struct SharedStreamHandleInner {
pub broadcast: timed_broadcast::Sender<Bytes>, pub broadcast: timed_broadcast::Sender<Bytes>,
@@ -202,6 +259,14 @@ pub struct EncoderState {
pub struct SharedSinkContext { pub struct SharedSinkContext {
pub encoder_options: EncoderOptions, pub encoder_options: EncoderOptions,
pub bits_per_sample: u8, 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_tx: Option<mpsc::Sender<PcmChunk>>,
pub pcm_rx: Option<mpsc::Receiver<PcmChunk>>, pub pcm_rx: Option<mpsc::Receiver<PcmChunk>>,
pub metadata: Arc<RwLock<MetadataSnapshot>>, pub metadata: Arc<RwLock<MetadataSnapshot>>,
@@ -311,6 +376,15 @@ impl SharedSinkContext {
// Always pass the metadata handle to the encoder so Vorbis comments are emitted. // Always pass the metadata handle to the encoder so Vorbis comments are emitted.
self.encoder_options.metadata = Some(metadata_lock.clone()); 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. // Capture duration (if any) to set total_samples.
let duration_opt = { let duration_opt = {
let metadata = metadata_lock.read().await; let metadata = metadata_lock.read().await;
@@ -339,6 +413,12 @@ impl SharedSinkContext {
self.sample_rate 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 { if let Some(total) = self.pending_total_samples {
self.encoder_options.total_samples = Some(total); self.encoder_options.total_samples = Some(total);
info!( info!(
@@ -430,8 +510,24 @@ impl SharedSinkContext {
let metadata = metadata_lock.read().await; let metadata = metadata_lock.read().await;
let mut snapshot = self.metadata.write().await; let mut snapshot = self.metadata.write().await;
snapshot.title = metadata.get_title().await.ok().flatten(); // Title / artist with default fallback or forced default.
snapshot.artist = metadata.get_artist().await.ok().flatten(); 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.album = metadata.get_album().await.ok().flatten();
snapshot.duration = metadata.get_duration().await.ok().flatten(); snapshot.duration = metadata.get_duration().await.ok().flatten();
snapshot.cover_url = metadata.get_cover_url().await.ok().flatten(); snapshot.cover_url = metadata.get_cover_url().await.ok().flatten();
@@ -443,6 +539,9 @@ impl SharedSinkContext {
snapshot.track_number = extra snapshot.track_number = extra
.get("track_number") .get("track_number")
.and_then(|s| s.parse::<u32>().ok()); .and_then(|s| s.parse::<u32>().ok());
} else {
snapshot.genre = None;
snapshot.track_number = None;
} }
snapshot.audio_timestamp_sec = timestamp_sec; snapshot.audio_timestamp_sec = timestamp_sec;

View File

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

View File

@@ -20,6 +20,7 @@ use pmoparadise::{
channels::{ChannelDescriptor, ALL_CHANNELS}, channels::{ChannelDescriptor, ALL_CHANNELS},
ParadiseHistoryBuilder, ParadiseStreamChannel, ParadiseStreamChannelConfig, ParadiseHistoryBuilder, ParadiseStreamChannel, ParadiseStreamChannelConfig,
}; };
use pmoaudio_ext::StreamingSinkOptions;
use pmoplaylist::register_audio_cache as register_playlist_audio_cache; use pmoplaylist::register_audio_cache as register_playlist_audio_cache;
use std::{fs, net::SocketAddr, sync::Arc}; use std::{fs, net::SocketAddr, sync::Arc};
use tokio::net::TcpListener; use tokio::net::TcpListener;
@@ -62,10 +63,16 @@ async fn main() -> anyhow::Result<()> {
history_builder.collection_prefix = Some(format!("single-channel-{}", descriptor.slug)); history_builder.collection_prefix = Some(format!("single-channel-{}", descriptor.slug));
let history_opts = history_builder.build_for_channel(&descriptor).await?; let history_opts = history_builder.build_for_channel(&descriptor).await?;
let mut channel_config = ParadiseStreamChannelConfig::default();
channel_config.flac_options = StreamingSinkOptions::flac_defaults()
.with_default_artist(Some("Radio Paradise".to_string()))
.with_default_title(descriptor.display_name.to_string())
.with_only_default_metadata(true);
let channel = Arc::new( let channel = Arc::new(
ParadiseStreamChannel::new( ParadiseStreamChannel::new(
descriptor, descriptor,
ParadiseStreamChannelConfig::default(), channel_config,
Some(cover_cache), Some(cover_cache),
Some(history_opts), Some(history_opts),
) )

View File

@@ -26,6 +26,7 @@ use pmoaudio::{AudioError, AudioPipelineNode};
use pmoaudio_ext::{ use pmoaudio_ext::{
FlacClientStream, IcyClientStream, MetadataSnapshot, OggFlacClientStream, OggFlacStreamHandle, FlacClientStream, IcyClientStream, MetadataSnapshot, OggFlacClientStream, OggFlacStreamHandle,
PlaylistSource, StreamHandle, StreamingFlacSink, StreamingOggFlacSink, TrackBoundaryCoverNode, PlaylistSource, StreamHandle, StreamingFlacSink, StreamingOggFlacSink, TrackBoundaryCoverNode,
StreamingSinkOptions,
}; };
use pmoaudiocache::{get_audio_cache, Cache as AudioCache}; use pmoaudiocache::{get_audio_cache, Cache as AudioCache};
use pmocovers::{get_cover_cache, Cache as CoverCache}; use pmocovers::{get_cover_cache, Cache as CoverCache};
@@ -43,12 +44,18 @@ use tracing::{error, info, warn};
pub struct ParadiseStreamChannelConfig { pub struct ParadiseStreamChannelConfig {
/// Durée maximale (en secondes) d'avance acceptée par le broadcast. /// Durée maximale (en secondes) d'avance acceptée par le broadcast.
pub max_lead_seconds: f64, pub max_lead_seconds: f64,
/// Options pour le flux FLAC pur.
pub flac_options: StreamingSinkOptions,
/// Options pour le flux OGG-FLAC.
pub ogg_options: StreamingSinkOptions,
} }
impl Default for ParadiseStreamChannelConfig { impl Default for ParadiseStreamChannelConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
max_lead_seconds: 1.0, max_lead_seconds: 1.0,
flac_options: StreamingSinkOptions::flac_defaults(),
ogg_options: StreamingSinkOptions::ogg_defaults(),
} }
} }
} }
@@ -136,6 +143,8 @@ impl ParadiseStreamChannelConfig {
if let Some(v) = num.as_f64() { if let Some(v) = num.as_f64() {
Self { Self {
max_lead_seconds: v.max(0.1), max_lead_seconds: v.max(0.1),
flac_options: StreamingSinkOptions::flac_defaults(),
ogg_options: StreamingSinkOptions::ogg_defaults(),
} }
} else { } else {
let default = Self::default(); let default = Self::default();
@@ -148,6 +157,8 @@ impl ParadiseStreamChannelConfig {
if let Ok(v) = s.parse::<f64>() { if let Ok(v) = s.parse::<f64>() {
Self { Self {
max_lead_seconds: v.max(0.1), max_lead_seconds: v.max(0.1),
flac_options: StreamingSinkOptions::flac_defaults(),
ogg_options: StreamingSinkOptions::ogg_defaults(),
} }
} else { } else {
let default = Self::default(); let default = Self::default();
@@ -238,15 +249,17 @@ impl ParadiseStreamChannel {
}; };
// 4. Créer les sinks de broadcast (FLAC + OGG) // 4. Créer les sinks de broadcast (FLAC + OGG)
let (flac_sink, stream_handle) = StreamingFlacSink::with_max_broadcast_lead( let (flac_sink, stream_handle) = StreamingFlacSink::with_options(
EncoderOptions::default(), EncoderOptions::default(),
16, 16,
config.max_lead_seconds, config.max_lead_seconds,
config.flac_options.clone(),
); );
let (ogg_sink, ogg_handle) = StreamingOggFlacSink::with_max_broadcast_lead( let (ogg_sink, ogg_handle) = StreamingOggFlacSink::with_options(
EncoderOptions::default(), EncoderOptions::default(),
16, 16,
config.max_lead_seconds, config.max_lead_seconds,
config.ogg_options.clone(),
); );
let mut downstream_children: Vec<Box<dyn AudioPipelineNode>> = Vec::new(); let mut downstream_children: Vec<Box<dyn AudioPipelineNode>> = Vec::new();

View File

@@ -19,7 +19,7 @@ use pmoaudio::{nodes::DEFAULT_CHANNEL_SIZE, AudioPipelineNode};
use pmoaudio_ext::{ use pmoaudio_ext::{
FlacCacheSink, FlacClientStream, IcyClientStream, MetadataSnapshot, OggFlacClientStream, FlacCacheSink, FlacClientStream, IcyClientStream, MetadataSnapshot, OggFlacClientStream,
OggFlacStreamHandle, PlaylistSource, StreamHandle, StreamingFlacSink, StreamingOggFlacSink, OggFlacStreamHandle, PlaylistSource, StreamHandle, StreamingFlacSink, StreamingOggFlacSink,
TrackBoundaryCoverNode, TrackBoundaryCoverNode, StreamingSinkOptions,
}; };
use pmoaudiocache::Cache as AudioCache; use pmoaudiocache::Cache as AudioCache;
use pmocovers::Cache as CoverCache; use pmocovers::Cache as CoverCache;
@@ -37,12 +37,16 @@ use tracing::{error, info, warn};
pub struct ParadiseStreamChannelConfig { pub struct ParadiseStreamChannelConfig {
/// Durée maximale (en secondes) d'avance acceptée par le broadcast. /// Durée maximale (en secondes) d'avance acceptée par le broadcast.
pub max_lead_seconds: f64, pub max_lead_seconds: f64,
pub flac_options: StreamingSinkOptions,
pub ogg_options: StreamingSinkOptions,
} }
impl Default for ParadiseStreamChannelConfig { impl Default for ParadiseStreamChannelConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
max_lead_seconds: 1.0, max_lead_seconds: 1.0,
flac_options: StreamingSinkOptions::flac_defaults(),
ogg_options: StreamingSinkOptions::ogg_defaults(),
} }
} }
} }
@@ -189,15 +193,17 @@ impl ParadiseStreamChannel {
let mut source = RadioParadiseStreamSource::new(client.clone()); let mut source = RadioParadiseStreamSource::new(client.clone());
let block_handle = source.block_handle(); let block_handle = source.block_handle();
let (flac_sink, stream_handle) = StreamingFlacSink::with_max_broadcast_lead( let (flac_sink, stream_handle) = StreamingFlacSink::with_options(
EncoderOptions::default(), EncoderOptions::default(),
16, 16,
config.max_lead_seconds, config.max_lead_seconds,
config.flac_options.clone(),
); );
let (ogg_sink, ogg_handle) = StreamingOggFlacSink::with_max_broadcast_lead( let (ogg_sink, ogg_handle) = StreamingOggFlacSink::with_options(
EncoderOptions::default(), EncoderOptions::default(),
16, 16,
config.max_lead_seconds, config.max_lead_seconds,
config.ogg_options.clone(),
); );
let mut downstream_children: Vec<Box<dyn AudioPipelineNode>> = Vec::new(); let mut downstream_children: Vec<Box<dyn AudioPipelineNode>> = Vec::new();
@@ -361,10 +367,11 @@ impl ParadiseStreamChannel {
.await .await
.map_err(|e| HistoryStreamError::Playlist(e.to_string()))?; .map_err(|e| HistoryStreamError::Playlist(e.to_string()))?;
let mut source = PlaylistSource::new(reader, history.audio_cache.clone()); let mut source = PlaylistSource::new(reader, history.audio_cache.clone());
let (flac_sink, handle) = StreamingFlacSink::with_max_broadcast_lead( let (flac_sink, handle) = StreamingFlacSink::with_options(
EncoderOptions::default(), EncoderOptions::default(),
16, 16,
history.replay_max_lead_seconds, history.replay_max_lead_seconds,
self.state.config.flac_options.clone(),
); );
source.register(Box::new(flac_sink)); source.register(Box::new(flac_sink));
let stop_token = CancellationToken::new(); let stop_token = CancellationToken::new();
@@ -397,10 +404,11 @@ impl ParadiseStreamChannel {
.await .await
.map_err(|e| HistoryStreamError::Playlist(e.to_string()))?; .map_err(|e| HistoryStreamError::Playlist(e.to_string()))?;
let mut source = PlaylistSource::new(reader, history.audio_cache.clone()); let mut source = PlaylistSource::new(reader, history.audio_cache.clone());
let (ogg_sink, handle) = StreamingOggFlacSink::with_max_broadcast_lead( let (ogg_sink, handle) = StreamingOggFlacSink::with_options(
EncoderOptions::default(), EncoderOptions::default(),
16, 16,
history.replay_max_lead_seconds, history.replay_max_lead_seconds,
self.state.config.ogg_options.clone(),
); );
source.register(Box::new(ogg_sink)); source.register(Box::new(ogg_sink));
let stop_token = CancellationToken::new(); let stop_token = CancellationToken::new();