Replace HTTP timeout with idle mode + END_OF_BLOCKS_SIGNAL
MAJOR ARCHITECTURAL IMPROVEMENT: Instead of using arbitrary timeouts that don't solve the real problem, implement proper idle mode and explicit end-of-stream signaling. Changes: 1. **Remove block_id timeout completely** - No more BLOCK_ID_TIMEOUT_SECS - Source enters idle mode when queue is empty - Waits indefinitely for new block_ids (poll every 100ms) - Only exits on cancellation or END_OF_BLOCKS_SIGNAL 2. **Introduce END_OF_BLOCKS_SIGNAL (EventId::MAX)** - Special block_id value to signal "no more blocks" - Source terminates cleanly after processing current block - Allows proper shutdown without cancellation - Exported from pmoparadise crate for public use 3. **Update HTTP timeout to 24 hours** - Effectively infinite timeout for block downloads - HTTP stream stays open as long as needed - Closed by pipeline termination, not arbitrary timeout 4. **Update stream_block example** - Push END_OF_BLOCKS_SIGNAL after the single block - Demonstrates clean termination after one block - Documents pattern for continuous vs. bounded streaming Benefits: - No arbitrary timeouts that might truncate valid streams - Clean separation: cancellation (external) vs. completion (internal) - Supports both continuous radio and bounded playlists - Proper idle mode for on-demand streaming applications Usage pattern: ```rust // Single block then stop source.push_block_id(block_id); source.push_block_id(END_OF_BLOCKS_SIGNAL); // Continuous streaming source.push_block_id(block1); source.push_block_id(block2); // ... keep pushing or wait in idle mode // Graceful shutdown source.push_block_id(END_OF_BLOCKS_SIGNAL); ```
This commit is contained in:
@@ -4,6 +4,9 @@
|
||||
//! using the StreamingFlacSink over HTTP via pmoserver. Perfect for
|
||||
//! testing with VLC or other media players that support HTTP streaming.
|
||||
//!
|
||||
//! The example streams ONE block then terminates cleanly using END_OF_BLOCKS_SIGNAL.
|
||||
//! For continuous streaming, push multiple block_ids without the END signal.
|
||||
//!
|
||||
//! Architecture:
|
||||
//! ```text
|
||||
//! RadioParadiseStreamSource → TimerNode → StreamingFlacSink
|
||||
@@ -38,7 +41,7 @@ use axum::{
|
||||
use pmoaudio::{AudioPipelineNode, TimerNode};
|
||||
use pmoaudio_ext::{StreamingFlacSink, StreamingOggFlacSink};
|
||||
use pmoflac::EncoderOptions;
|
||||
use pmoparadise::{RadioParadiseClient, RadioParadiseStreamSource};
|
||||
use pmoparadise::{RadioParadiseClient, RadioParadiseStreamSource, END_OF_BLOCKS_SIGNAL};
|
||||
use pmoserver::{ServerBuilder, init_logging};
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
@@ -206,7 +209,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
let mut source_flac = RadioParadiseStreamSource::new(client.clone());
|
||||
source_flac.push_block_id(block.event);
|
||||
tracing::debug!("RadioParadiseStreamSource (FLAC) created with block {}", block.event);
|
||||
source_flac.push_block_id(END_OF_BLOCKS_SIGNAL); // Signal: no more blocks after this one
|
||||
tracing::debug!("RadioParadiseStreamSource (FLAC) created with block {} + END signal", block.event);
|
||||
|
||||
// Calculate channel size to match max_lead_time
|
||||
// With 50ms chunks and 3.0s lead time: 3.0 / 0.05 = 60 chunks
|
||||
@@ -232,7 +236,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
let mut source_ogg = RadioParadiseStreamSource::new(client);
|
||||
source_ogg.push_block_id(block.event);
|
||||
tracing::debug!("RadioParadiseStreamSource (OGG) created with block {}", 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 = TimerNode::with_channel_size(max_lead_time, channel_size);
|
||||
tracing::debug!("TimerNode (OGG) created with {:.1}s max lead time, {} chunk buffer", max_lead_time, channel_size);
|
||||
|
||||
@@ -232,7 +232,7 @@ pub use models::{Block, DurationMs, EventId, NowPlaying, Song};
|
||||
pub use source::RadioParadiseSource;
|
||||
|
||||
#[cfg(feature = "pmoaudio")]
|
||||
pub use radio_paradise_stream_source::RadioParadiseStreamSource;
|
||||
pub use radio_paradise_stream_source::{RadioParadiseStreamSource, END_OF_BLOCKS_SIGNAL};
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub use pmoserver_ext::{
|
||||
|
||||
@@ -25,10 +25,10 @@ use tokio::io::AsyncReadExt;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio_util::{io::StreamReader, sync::CancellationToken};
|
||||
|
||||
/// Timeout pour attendre un nouveau block ID
|
||||
/// Pour une radio en temps réel, 3s est raisonnable
|
||||
/// Pour des tests avec un seul bloc, on veut quelque chose de plus long
|
||||
const BLOCK_ID_TIMEOUT_SECS: u64 = 3600; // 1 heure
|
||||
/// Signal spécial pour indiquer qu'il n'y aura plus de blocs
|
||||
/// Quand ce blockid est poussé dans la queue, le source termine proprement
|
||||
/// après avoir fini de traiter le bloc en cours
|
||||
pub const END_OF_BLOCKS_SIGNAL: EventId = EventId::MAX;
|
||||
|
||||
/// Nombre de blocs récents à mémoriser pour éviter les re-téléchargements
|
||||
const RECENT_BLOCKS_CACHE_SIZE: usize = 10;
|
||||
@@ -512,34 +512,38 @@ impl NodeLogic for RadioParadiseStreamSourceLogic {
|
||||
let mut last_start_instant: Option<Instant> = None;
|
||||
|
||||
loop {
|
||||
// Attendre un block ID (timeout court pour une radio)
|
||||
tracing::debug!("Waiting for block_id from queue (timeout={}s)...", BLOCK_ID_TIMEOUT_SECS);
|
||||
let event_id = match tokio::time::timeout(
|
||||
Duration::from_secs(BLOCK_ID_TIMEOUT_SECS),
|
||||
async {
|
||||
while self.block_queue.is_empty() {
|
||||
tracing::trace!("block_queue is empty, sleeping...");
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// Attendre un block ID depuis la queue (pas de timeout - mode idle)
|
||||
tracing::debug!("Waiting for block_id from queue (idle mode, no timeout)...");
|
||||
let event_id = loop {
|
||||
// Vérifier d'abord le stop_token
|
||||
if stop_token.is_cancelled() {
|
||||
tracing::debug!("stop_token cancelled while waiting for block_id");
|
||||
return None;
|
||||
tracing::info!("Stop token cancelled while waiting for block_id");
|
||||
break None;
|
||||
}
|
||||
}
|
||||
self.block_queue.pop_front()
|
||||
}
|
||||
).await {
|
||||
Ok(Some(id)) => {
|
||||
|
||||
// Essayer de pop un event_id
|
||||
if let Some(id) = self.block_queue.pop_front() {
|
||||
tracing::debug!("Got event_id {} from queue", id);
|
||||
id
|
||||
|
||||
// Vérifier si c'est le signal de fin
|
||||
if id == END_OF_BLOCKS_SIGNAL {
|
||||
tracing::info!("Received END_OF_BLOCKS_SIGNAL, finishing after current block");
|
||||
break None;
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::debug!("Loop cancelled, breaking");
|
||||
break;
|
||||
} // Cancelled
|
||||
Err(_) => {
|
||||
// Timeout - pas de nouveau bloc, on termine
|
||||
tracing::warn!("Timeout waiting for block_id, breaking");
|
||||
|
||||
break Some(id);
|
||||
}
|
||||
|
||||
// Queue vide, attendre un peu et réessayer
|
||||
tracing::trace!("block_queue is empty, sleeping 100ms...");
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
};
|
||||
|
||||
// Si on n'a pas d'event_id, on termine
|
||||
let event_id = match event_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
tracing::info!("No more blocks to process, exiting loop");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user