Files
pmomusic/pmowebrenderer/src/pipeline.rs

212 lines
8.3 KiB
Rust
Raw Normal View History

//! Pipeline audio serveur par instance WebRenderer
//!
//! Chaque instance WebRenderer possède un pipeline indépendant :
//! - Une `PlayerSource` qui gère le cycle de vie AVTransport (Play/Pause/Stop/Seek/LoadUri)
//! - Un `StreamingOggFlacSink` qui encode et diffuse le flux OGG-FLAC aux clients HTTP
//! - Des nœuds de normalisation (resampling → 96 kHz, conversion → I24)
use std::sync::Arc;
use pmoaudio::{ResamplingNode, ToI24Node};
use pmoaudio_ext::{PlayerCommand, PlayerHandle, PlayerSource};
use pmoaudio_ext::sinks::{OggFlacStreamHandle, StreamingOggFlacSink};
use pmoflac::EncoderOptions;
use tokio_util::sync::CancellationToken;
use tracing::{debug, warn};
use crate::state::SharedState;
// ─── Ré-export des commandes pour les handlers ────────────────────────────────
/// Commandes de transport — alias vers PlayerCommand pour compatibilité handlers
pub use pmoaudio_ext::PlayerCommand as PipelineControl;
// ─── Handle vers le pipeline ─────────────────────────────────────────────────
/// Handle partageable vers le pipeline audio d'une instance.
///
/// Expose le `PlayerHandle` pour les commandes AVTransport et le `CancellationToken`
/// pour l'arrêt complet du pipeline.
#[derive(Clone)]
pub struct PipelineHandle {
pub player: PlayerHandle,
pub stop_token: CancellationToken,
/// Volume courant (géré localement, pas dans PlayerSource)
state: SharedState,
}
impl PipelineHandle {
/// Envoie une commande de transport. Gère SetVolume/SetMute localement.
pub async fn send(&self, cmd: PipelineControl) {
match cmd {
PlayerCommand::LoadUri(uri) => self.player.load_uri(uri).await,
PlayerCommand::LoadNextUri(uri) => self.player.load_next_uri(uri).await,
PlayerCommand::Play => self.player.play().await,
PlayerCommand::Pause => self.player.pause().await,
PlayerCommand::Stop => self.player.stop().await,
PlayerCommand::Seek(pos) => self.player.seek(pos).await,
}
}
}
// ─── Pipeline instancié ──────────────────────────────────────────────────────
/// Pipeline audio complet pour une instance WebRenderer.
///
/// Créé au `POST /register`. Le flux OGG-FLAC est accessible via `flac_handle`
/// (multi-client broadcast, chaque `subscribe()` crée un flux indépendant).
pub struct InstancePipeline {
/// Handle vers le sink OGG-FLAC — clonable, subscribe() crée un flux indépendant par client.
pub flac_handle: OggFlacStreamHandle,
pub pipeline_handle: PipelineHandle,
}
impl InstancePipeline {
/// Crée et démarre le pipeline en background.
/// Retourne immédiatement avec les handles nécessaires.
pub fn start(
state: SharedState,
#[cfg(feature = "pmoserver")]
control_point: Arc<pmocontrol::ControlPoint>,
udn: String,
) -> Self {
let stop_token = CancellationToken::new();
use pmoaudio::pipeline::AudioPipelineNode;
// Sink broadcast OGG-FLAC (multi-client, pacé à 0.5s max d'avance)
let (sink, flac_handle) = StreamingOggFlacSink::new(EncoderOptions::default(), 24);
// Nœud de conversion de profondeur : tout type entier → I24
let mut to_i24 = ToI24Node::new();
to_i24.register(sink.boxed());
// Nœud de rééchantillonnage : n'importe quel sample rate → 96 kHz
let mut resampler = ResamplingNode::new(96_000);
resampler.register(to_i24.boxed());
// Source avec contrôle AVTransport complet
let (mut player_source, player_handle) = PlayerSource::new();
player_source.register(resampler.boxed());
// Lancer le pipeline en background
let sink_stop = stop_token.clone();
tokio::spawn(async move {
if let Err(e) = player_source.boxed().run(sink_stop).await {
warn!("Audio pipeline error: {:?}", e);
}
debug!("Pipeline task terminated");
});
// Écouter les événements PlayerSource pour mettre à jour le state UPnP
let event_rx = player_handle.subscribe_events();
let state_clone = state.clone();
let udn_clone = udn.clone();
#[cfg(feature = "pmoserver")]
let cp_clone = control_point.clone();
tokio::spawn(async move {
run_event_listener(
event_rx,
state_clone,
udn_clone,
#[cfg(feature = "pmoserver")]
cp_clone,
).await;
});
let pipeline_handle = PipelineHandle {
player: player_handle,
stop_token: stop_token.clone(),
state,
};
Self {
flac_handle,
pipeline_handle,
}
}
}
// ─── Listener d'événements ────────────────────────────────────────────────────
async fn run_event_listener(
mut event_rx: tokio::sync::broadcast::Receiver<pmoaudio_ext::PlayerEvent>,
state: SharedState,
udn: String,
#[cfg(feature = "pmoserver")]
control_point: Arc<pmocontrol::ControlPoint>,
) {
use pmoaudio_ext::PlayerEvent;
use crate::messages::PlaybackState;
loop {
match event_rx.recv().await {
Ok(event) => match event {
PlayerEvent::Playing { uri, duration_sec } => {
let mut s = state.write();
s.playback_state = PlaybackState::Playing;
s.current_uri = Some(uri);
s.duration = duration_sec.map(seconds_to_upnp_time);
s.position = None;
// Effacer next_uri/next_metadata : la nouvelle piste est maintenant courante
s.next_uri = None;
s.next_metadata = None;
}
PlayerEvent::Paused { position_sec } => {
let mut s = state.write();
s.playback_state = PlaybackState::Paused;
s.position = Some(seconds_to_upnp_time(position_sec));
}
PlayerEvent::Stopped => {
let mut s = state.write();
s.playback_state = PlaybackState::Stopped;
s.position = None;
}
PlayerEvent::Position { position_sec } => {
state.write().position = Some(seconds_to_upnp_time(position_sec));
}
PlayerEvent::TrackEnded => {
#[cfg(feature = "pmoserver")]
{
let cp = control_point.clone();
let udn_c = udn.clone();
tokio::spawn(async move {
cp.advance_queue_and_prefetch(&pmocontrol::DeviceId(udn_c));
});
}
}
PlayerEvent::Error(e) => {
tracing::warn!(udn = %udn, "PlayerSource error: {}", e);
state.write().playback_state = PlaybackState::Stopped;
}
},
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
tracing::warn!(udn = %udn, "Event listener lagged {} events", n);
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break;
}
}
}
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
pub fn seconds_to_upnp_time(s: f64) -> String {
let s = s as u64;
let h = s / 3600;
let m = (s % 3600) / 60;
let sec = s % 60;
format!("{}:{:02}:{:02}", h, m, sec)
}
pub fn upnp_time_to_seconds(t: &str) -> f64 {
let parts: Vec<f64> = t.split(':').filter_map(|p| p.parse().ok()).collect();
match parts.as_slice() {
[h, m, s] => h * 3600.0 + m * 60.0 + s,
[m, s] => m * 60.0 + s,
[s] => *s,
_ => 0.0,
}
}