⬆️ ureq v2→v3, add continuous stream detection

- Updateurequest dependency from v2 to latest major (v3.14)
- Add ureq as optional dependency in pmoaudio-ext
- Introduce is_continuous flag to UriSource and PlayerState for proper StreamType handling (Finite vs Continuous)
- Implement detect_continuous_stream() with URL pattern matching and HTTP HEAD header inspection (ICY, chunked encoding)
- Update send_track_boundary() to accept StreamType parameter
This commit is contained in:
2026-04-04 18:00:20 +02:00
parent 8924552696
commit 340c69cb2b
5 changed files with 144 additions and 17 deletions

View File

@@ -165,6 +165,7 @@ impl NodeLogic for PlayerSourceLogic {
let mut current_uri: Option<String> = None;
let mut next_uri: Option<String> = None;
let mut paused_at_sec: f64 = 0.0;
let mut is_continuous: bool = false;
info!("PlayerSource: started");
@@ -182,7 +183,7 @@ impl NodeLogic for PlayerSourceLogic {
None => break,
Some(cmd) => {
self.handle_command(
cmd, &mut state, &mut current_uri,
cmd, is_continuous, &mut state, &mut current_uri,
&mut next_uri, &mut paused_at_sec,
&output, &stop_token,
).await?;
@@ -213,15 +214,17 @@ impl NodeLogic for PlayerSourceLogic {
};
let duration_sec = source.duration_sec();
let is_continuous = source.is_continuous();
let _ = self.event_tx.send(PlayerEvent::Playing {
uri: uri.clone(),
duration_sec,
});
info!("PlayerSource: playing {:?} from {:.1}s", uri, paused_at_sec);
info!("PlayerSource: playing {:?} from {:.1}s continuous={}", uri, paused_at_sec, is_continuous);
// Pompe audio — s'arrête sur EOF, Pause, Stop, ou commande
let result = self.pump(
source,
is_continuous,
&mut state,
&mut current_uri,
&mut next_uri,
@@ -256,6 +259,7 @@ impl PlayerSourceLogic {
async fn handle_command(
&mut self,
cmd: PlayerCommand,
is_continuous: bool,
state: &mut TransportState,
current_uri: &mut Option<String>,
next_uri: &mut Option<String>,
@@ -282,14 +286,16 @@ impl PlayerSourceLogic {
TransportState::Paused => {
info!("PlayerSource: Play (resume from {:.1}s)", paused_at_sec);
// Injecter un TrackBoundary pour EOS + nouveau BOS OGG propre
send_track_boundary(current_uri.as_deref(), output, *paused_at_sec, stop_token).await?;
let stream_type = if is_continuous { StreamType::Continuous } else { StreamType::Finite };
send_track_boundary(current_uri.as_deref(), output, *paused_at_sec, stream_type, stop_token).await?;
*state = TransportState::Playing;
}
TransportState::Loaded => {
info!("PlayerSource: Play (start)");
*paused_at_sec = 0.0;
// TrackBoundary initial pour le premier BOS OGG
send_track_boundary(current_uri.as_deref(), output, 0.0, stop_token).await?;
let stream_type = if is_continuous { StreamType::Continuous } else { StreamType::Finite };
send_track_boundary(current_uri.as_deref(), output, 0.0, stream_type, stop_token).await?;
*state = TransportState::Playing;
}
TransportState::Idle => {
@@ -321,7 +327,8 @@ impl PlayerSourceLogic {
if current_uri.is_some() {
info!("PlayerSource: Seek to {:.1}s", pos);
*paused_at_sec = pos;
send_track_boundary(current_uri.as_deref(), output, pos, stop_token).await?;
let stream_type = if is_continuous { StreamType::Continuous } else { StreamType::Finite };
send_track_boundary(current_uri.as_deref(), output, pos, stream_type, stop_token).await?;
*state = TransportState::Playing;
}
}
@@ -336,6 +343,7 @@ impl PlayerSourceLogic {
async fn pump(
&mut self,
source: UriSource,
is_continuous: bool,
state: &mut TransportState,
current_uri: &mut Option<String>,
next_uri: &mut Option<String>,
@@ -395,7 +403,8 @@ impl PlayerSourceLogic {
*next_uri = None;
*paused_at_sec = 0.0;
// TrackBoundary pour clore le bitstream OGG proprement
if let Err(e) = send_track_boundary(current_uri.as_deref(), output, 0.0, stop_token).await {
let stream_type = if is_continuous { StreamType::Continuous } else { StreamType::Finite };
if let Err(e) = send_track_boundary(current_uri.as_deref(), output, 0.0, stream_type, stop_token).await {
result = Err(e);
}
*state = TransportState::Playing;
@@ -409,7 +418,8 @@ impl PlayerSourceLogic {
info!("PlayerSource: Seek to {:.1}s", pos);
source_stop.cancel();
*paused_at_sec = pos;
if let Err(e) = send_track_boundary(current_uri.as_deref(), output, pos, stop_token).await {
let stream_type = if is_continuous { StreamType::Continuous } else { StreamType::Finite };
if let Err(e) = send_track_boundary(current_uri.as_deref(), output, pos, stream_type, stop_token).await {
result = Err(e);
break;
}
@@ -435,7 +445,8 @@ impl PlayerSourceLogic {
info!("PlayerSource: gapless transition to {:?}", next);
*current_uri = Some(next);
*paused_at_sec = 0.0;
if let Err(e) = send_track_boundary(current_uri.as_deref(), output, 0.0, stop_token).await {
let stream_type = if is_continuous { StreamType::Continuous } else { StreamType::Finite };
if let Err(e) = send_track_boundary(current_uri.as_deref(), output, 0.0, stream_type, stop_token).await {
result = Err(e);
}
*state = TransportState::Playing;
@@ -495,6 +506,7 @@ async fn send_track_boundary(
uri: Option<&str>,
output: &[mpsc::Sender<Arc<AudioSegment>>],
timestamp_sec: f64,
stream_type: StreamType,
stop_token: &CancellationToken,
) -> Result<(), AudioError> {
if output.is_empty() || stop_token.is_cancelled() {
@@ -506,7 +518,7 @@ async fn send_track_boundary(
let _ = meta.set_title(Some(u.to_string())).await;
}
let meta_arc = Arc::new(tokio::sync::RwLock::new(meta));
let boundary = AudioSegment::new_track_boundary(0, timestamp_sec, meta_arc, StreamType::Finite);
let boundary = AudioSegment::new_track_boundary(0, timestamp_sec, meta_arc, stream_type);
send_to_children("PlayerSource", output, boundary).await
}