Refactor WebRenderer to use server-side streaming with OGG-FLAC sink

This commit refactors the WebRenderer to use a server-side streaming architecture with OGG-FLAC sink instead of the previous WebSocket-based approach. The changes include:

- Replaced WebSocket communication with HTTP streaming using DirectOggFlacSink
- Implemented a new pipeline architecture with dedicated handlers for UPnP commands
- Added new modules for registration, registry, and streaming
- Updated the renderer to work with a pipeline control system
- Removed old WebSocket session management
- Added support for HTTP streaming with gapless playback
- Updated dependencies and features for the new architecture

The WebRenderer now acts as a MediaRenderer UPnP device that serves audio streams via HTTP endpoints, with commands relayed to the audio pipeline through a new control system.
This commit is contained in:
2026-02-26 19:58:37 +01:00
parent 4b3e8a961c
commit 306e691c61
32 changed files with 2797 additions and 1653 deletions

View File

@@ -1,7 +1,7 @@
//! Action handlers SOAP → WebSocket pour le WebRenderer
//! Action handlers SOAP → Pipeline pour le WebRenderer serveur
//!
//! Chaque handler bridge une action UPnP vers une commande WebSocket
//! envoyée au navigateur, ou lit l'état partagé pour les requêtes GET.
//! Chaque handler bridge une action UPnP vers une commande `PipelineControl`
//! envoyée au pipeline audio serveur, ou lit l'état partagé pour les requêtes GET.
use std::sync::Arc;
@@ -10,106 +10,89 @@ use pmoupnp::actions::{ActionData, ActionError, ActionHandler, get_value};
use pmoupnp::{get, set};
use pmoutils::ToXmlElement;
use crate::messages::{CommandParams, PlaybackState, ServerMessage, TransportAction};
use crate::state::{SharedSender, SharedState};
use crate::messages::PlaybackState;
use crate::pipeline::{PipelineControl, PipelineHandle, seconds_to_upnp_time, upnp_time_to_seconds};
use crate::state::SharedState;
type ActionFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Result<ActionData, ActionError>> + Send>>;
// ─── AVTransport Handlers ───────────────────────────────────────────────────
pub fn play_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
pub fn play_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let pipeline = pipeline.clone();
let state = state.clone();
Box::pin(async move {
ws.send(ServerMessage::Command {
action: TransportAction::Play,
params: None,
});
state.write().playback_state = PlaybackState::Playing;
// Ne pas écrire Playing ici : c'est stream_source qui le fera
// une fois que les premiers bytes FLAC ont été produits.
// Écrire Transitioning pour signaler que la lecture va démarrer.
state.write().playback_state = PlaybackState::Transitioning;
pipeline.send(PipelineControl::Play).await;
Ok(data)
})
})
}
pub fn stop_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
pub fn stop_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let pipeline = pipeline.clone();
let state = state.clone();
Box::pin(async move {
ws.send(ServerMessage::Command {
action: TransportAction::Stop,
params: None,
});
pipeline.send(PipelineControl::Stop).await;
state.write().playback_state = PlaybackState::Stopped;
Ok(data)
})
})
}
pub fn pause_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
pub fn pause_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let pipeline = pipeline.clone();
let state = state.clone();
Box::pin(async move {
ws.send(ServerMessage::Command {
action: TransportAction::Pause,
params: None,
});
pipeline.send(PipelineControl::Pause).await;
state.write().playback_state = PlaybackState::Paused;
Ok(data)
})
})
}
pub fn next_handler(ws: SharedSender) -> ActionHandler {
pub fn next_handler(pipeline: PipelineHandle) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let pipeline = pipeline.clone();
Box::pin(async move {
ws.send(ServerMessage::Command {
action: TransportAction::Play,
params: None,
});
pipeline.send(PipelineControl::Play).await;
Ok(data)
})
})
}
pub fn previous_handler(ws: SharedSender) -> ActionHandler {
pub fn previous_handler(pipeline: PipelineHandle) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let pipeline = pipeline.clone();
Box::pin(async move {
ws.send(ServerMessage::Command {
action: TransportAction::Play,
params: None,
});
pipeline.send(PipelineControl::Play).await;
Ok(data)
})
})
}
pub fn seek_handler(ws: SharedSender) -> ActionHandler {
pub fn seek_handler(pipeline: PipelineHandle) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let pipeline = pipeline.clone();
Box::pin(async move {
let target: String = get!(&data, "Target", String);
ws.send(ServerMessage::Command {
action: TransportAction::Seek,
params: Some(CommandParams {
uri: None,
metadata: None,
position: Some(target),
}),
});
let pos_sec = upnp_time_to_seconds(&target);
pipeline.send(PipelineControl::Seek(pos_sec)).await;
Ok(data)
})
})
}
pub fn set_uri_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
pub fn set_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let pipeline = pipeline.clone();
let state = state.clone();
Box::pin(async move {
let uri: String = get!(&data, "CurrentURI", String);
@@ -119,14 +102,10 @@ pub fn set_uri_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
.map(|didl| didl.to_xml())
})
.unwrap_or_default();
ws.send(ServerMessage::Command {
action: TransportAction::SetUri,
params: Some(CommandParams {
uri: Some(uri.clone()),
metadata: Some(metadata.clone()),
position: None,
}),
});
// Envoyer l'URI au pipeline serveur (remplace l'envoi WebSocket)
pipeline.send(PipelineControl::LoadUri(uri.clone())).await;
{
let mut s = state.write();
s.current_uri = Some(uri);
@@ -138,9 +117,9 @@ pub fn set_uri_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
})
}
pub fn set_next_uri_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
pub fn set_next_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let pipeline = pipeline.clone();
let state = state.clone();
Box::pin(async move {
let uri: String = get!(&data, "NextURI", String);
@@ -150,14 +129,9 @@ pub fn set_next_uri_handler(ws: SharedSender, state: SharedState) -> ActionHandl
.map(|didl| didl.to_xml())
})
.unwrap_or_default();
ws.send(ServerMessage::Command {
action: TransportAction::SetNextUri,
params: Some(CommandParams {
uri: Some(uri.clone()),
metadata: Some(metadata.clone()),
position: None,
}),
});
pipeline.send(PipelineControl::LoadNextUri(uri.clone())).await;
{
let mut s = state.write();
s.next_uri = Some(uri);
@@ -215,18 +189,14 @@ pub fn get_transport_info_handler(state: SharedState) -> ActionHandler {
Box::pin(async move {
let mut data = data;
let s = state.read();
tracing::info!("[WebRenderer] GetTransportInfo handler called, state={:?}", s.playback_state);
tracing::info!("[WebRenderer] GetTransportInfo: state={:?}", s.playback_state);
let transport_state = match s.playback_state {
PlaybackState::Stopped => "STOPPED",
PlaybackState::Playing => "PLAYING",
PlaybackState::Paused => "PAUSED_PLAYBACK",
PlaybackState::Transitioning => "TRANSITIONING",
};
set!(
&mut data,
"CurrentTransportState",
transport_state.to_string()
);
set!(&mut data, "CurrentTransportState", transport_state.to_string());
set!(&mut data, "CurrentTransportStatus", "OK".to_string());
set!(&mut data, "CurrentSpeed", "1".to_string());
Ok(data)
@@ -245,26 +215,10 @@ pub fn get_media_info_handler(state: SharedState) -> ActionHandler {
"NrTracks",
if s.current_uri.is_some() { 1u32 } else { 0u32 }
);
set!(
&mut data,
"CurrentURI",
s.current_uri.clone().unwrap_or_default()
);
set!(
&mut data,
"CurrentURIMetaData",
s.current_metadata.clone().unwrap_or_default()
);
set!(
&mut data,
"NextURI",
s.next_uri.clone().unwrap_or_default()
);
set!(
&mut data,
"NextURIMetaData",
s.next_metadata.clone().unwrap_or_default()
);
set!(&mut data, "CurrentURI", s.current_uri.clone().unwrap_or_default());
set!(&mut data, "CurrentURIMetaData", s.current_metadata.clone().unwrap_or_default());
set!(&mut data, "NextURI", s.next_uri.clone().unwrap_or_default());
set!(&mut data, "NextURIMetaData", s.next_metadata.clone().unwrap_or_default());
Ok(data)
})
})
@@ -272,13 +226,13 @@ pub fn get_media_info_handler(state: SharedState) -> ActionHandler {
// ─── RenderingControl Handlers ──────────────────────────────────────────────
pub fn set_volume_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
pub fn set_volume_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let pipeline = pipeline.clone();
let state = state.clone();
Box::pin(async move {
let volume: u16 = get!(&data, "DesiredVolume", u16);
ws.send(ServerMessage::SetVolume { volume });
pipeline.send(PipelineControl::SetVolume(volume)).await;
state.write().volume = volume;
Ok(data)
})
@@ -297,13 +251,13 @@ pub fn get_volume_handler(state: SharedState) -> ActionHandler {
})
}
pub fn set_mute_handler(ws: SharedSender, state: SharedState) -> ActionHandler {
pub fn set_mute_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
Arc::new(move |data: ActionData| -> ActionFuture {
let ws = ws.clone();
let pipeline = pipeline.clone();
let state = state.clone();
Box::pin(async move {
let mute: bool = get!(&data, "DesiredMute", bool);
ws.send(ServerMessage::SetMute { mute });
pipeline.send(PipelineControl::SetMute(mute)).await;
state.write().mute = mute;
Ok(data)
})
@@ -329,10 +283,11 @@ pub fn get_protocol_info_handler() -> ActionHandler {
Box::pin(async move {
let mut data = data;
set!(&mut data, "Source", String::new());
// Serveur-side streaming : on produit du FLAC uniquement
set!(
&mut data,
"Sink",
"http-get:*:audio/mpeg:*,http-get:*:audio/mp4:*,http-get:*:audio/ogg:*,http-get:*:audio/flac:*,http-get:*:audio/wav:*,http-get:*:audio/x-flac:*,http-get:*:audio/aac:*,http-get:*:audio/webm:*".to_string()
"http-get:*:audio/flac:*,http-get:*:audio/x-flac:*".to_string()
);
Ok(data)
})