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:
Claude
2025-11-12 11:32:40 +00:00
parent e44bef2021
commit dbb809261a
3 changed files with 42 additions and 33 deletions

View File

@@ -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);

View File

@@ -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::{

View File

@@ -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::info!("Stop token cancelled while waiting for block_id");
break None;
}
if stop_token.is_cancelled() {
tracing::debug!("stop_token cancelled while waiting for block_id");
return 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;
}
break Some(id);
}
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");
// 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;
}
};