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

@@ -126,6 +126,7 @@ pub use nodes::{
resampling_node::ResamplingNode,
timer_buffer_node::TimerBufferNode,
timer_node::TimerNode,
position_tracker_node::{PositionHandle, PositionTrackerNode},
AudioError, AudioNode, TypedAudioNode,
};

View File

@@ -27,6 +27,7 @@ pub mod http_source;
pub mod resampling_node;
pub mod timer_buffer_node;
pub mod timer_node;
pub mod position_tracker_node;
// Modules temporairement désactivés
/*

View File

@@ -0,0 +1,127 @@
//! PositionTrackerNode — nœud transparent de suivi de position.
//!
//! Laisse passer tous les segments sans modification, et maintient
//! un compteur de position courant basé sur le `timestamp_sec` des chunks.
//!
//! Placé après un `TimerBufferNode`, il reflète la position de l'audio
//! effectivement sorti du buffer, pas celui encore en attente.
use crate::{
nodes::AudioError,
pipeline::{send_to_children, AudioPipelineNode, Node, NodeLogic},
type_constraints::TypeRequirement,
AudioSegment, _AudioSegment,
};
use std::sync::{Arc, atomic::{AtomicU64, Ordering}};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
// ─── Handle public ────────────────────────────────────────────────────────────
/// Handle partageable pour lire la position courante.
#[derive(Clone)]
pub struct PositionHandle {
/// Position encodée en microsecondes dans un AtomicU64 (pas de lock nécessaire).
position_us: Arc<AtomicU64>,
}
impl PositionHandle {
/// Retourne la position courante en secondes.
pub fn current_position_sec(&self) -> f64 {
self.position_us.load(Ordering::Relaxed) as f64 / 1_000_000.0
}
}
// ─── Logique du nœud ─────────────────────────────────────────────────────────
struct PositionTrackerLogic {
position_us: Arc<AtomicU64>,
}
#[async_trait::async_trait]
impl NodeLogic for PositionTrackerLogic {
async fn process(
&mut self,
input: Option<mpsc::Receiver<Arc<AudioSegment>>>,
output: Vec<mpsc::Sender<Arc<AudioSegment>>>,
stop_token: CancellationToken,
) -> Result<(), AudioError> {
let mut input = input.ok_or_else(|| {
AudioError::ProcessingError("PositionTrackerNode requires an input".into())
})?;
loop {
tokio::select! {
_ = stop_token.cancelled() => break,
segment = input.recv() => {
match segment {
None => break,
Some(seg) => {
// Mettre à jour la position sur les chunks audio uniquement
if let _AudioSegment::Chunk(_) = &seg.segment {
let us = (seg.timestamp_sec * 1_000_000.0) as u64;
self.position_us.store(us, Ordering::Relaxed);
}
// Passer le segment sans modification
send_to_children("PositionTrackerNode", &output, seg).await?;
}
}
}
}
}
Ok(())
}
async fn cleanup(
&mut self,
_reason: crate::pipeline::StopReason,
) -> Result<(), AudioError> {
Ok(())
}
}
// ─── Nœud public ─────────────────────────────────────────────────────────────
pub struct PositionTrackerNode {
inner: Node<PositionTrackerLogic>,
}
impl PositionTrackerNode {
pub fn new() -> (Self, PositionHandle) {
let position_us = Arc::new(AtomicU64::new(0));
let logic = PositionTrackerLogic { position_us: position_us.clone() };
let handle = PositionHandle { position_us };
let node = Self { inner: Node::new_with_input(logic, 16) };
(node, handle)
}
}
#[async_trait::async_trait]
impl AudioPipelineNode for PositionTrackerNode {
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
self.inner.get_tx()
}
fn register(&mut self, child: Box<dyn AudioPipelineNode>) {
self.inner.register(child);
}
async fn run(self: Box<Self>, stop_token: CancellationToken) -> Result<(), AudioError> {
Box::new(self.inner).run(stop_token).await
}
fn start(self: Box<Self>) -> crate::pipeline::PipelineHandle {
Box::new(self.inner).start()
}
}
impl crate::TypedAudioNode for PositionTrackerNode {
fn input_type(&self) -> Option<TypeRequirement> {
None // Accepte tout
}
fn output_type(&self) -> Option<TypeRequirement> {
None // Passe tout
}
}