♻️ Reorganize crates architecture (T5) — separate core and browser adapters
- Migrate pmomediarenderer: extract all core logic (adapter, handlers, pipeline etc.) into dedicated modules - Rename types to MediaRenderer* for clarity (Web → Mediarenderer) pmowebrenderer becomes a pure browser adapter crate - Update Cargo.toml dependencies accordingly, add pmoserver feature propagation PMOMusic: remove obsolete MEDIA_RENDERER import - Update all imports across workspace to reflect new crate boundaries
This commit is contained in:
@@ -1,44 +1,13 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum DeviceCommand {
|
||||
Stream { url: String },
|
||||
Play,
|
||||
Pause,
|
||||
Seek { position_sec: f64 },
|
||||
Flush,
|
||||
Stop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DevicePlaybackState {
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped,
|
||||
Buffering,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceStateReport {
|
||||
pub position_sec: Option<f64>,
|
||||
pub duration_sec: Option<f64>,
|
||||
pub playback_state: Option<DevicePlaybackState>,
|
||||
}
|
||||
|
||||
pub trait DeviceAdapter: Send + Sync + 'static {
|
||||
fn deliver(&self, command: DeviceCommand);
|
||||
fn poll_state(&self) -> Option<DeviceStateReport>;
|
||||
}
|
||||
use pmomediarenderer::{
|
||||
DeviceAdapter, DeviceCommand, DeviceStateReport, SharedState,
|
||||
};
|
||||
|
||||
pub struct BrowserAdapter {
|
||||
pub state: crate::state::SharedState,
|
||||
pub state: SharedState,
|
||||
}
|
||||
|
||||
impl BrowserAdapter {
|
||||
pub fn new(state: crate::state::SharedState) -> Self {
|
||||
pub fn new(state: SharedState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,14 @@ use axum::{
|
||||
use pmocontrol::ControlPoint;
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use crate::error::WebRendererError;
|
||||
use pmomediarenderer::MediaRendererError;
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use crate::register::{
|
||||
nowplaying_handler, pause_handler, play_handler, position_update_handler,
|
||||
register_handler, report_handler, set_uri_handler, state_handler, unregister_handler,
|
||||
};
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use crate::registry::RendererRegistry;
|
||||
use pmomediarenderer::MediaRendererRegistry;
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use crate::stream::stream_handler;
|
||||
|
||||
@@ -34,7 +34,7 @@ pub trait WebRendererExt {
|
||||
async fn register_web_renderer(
|
||||
&mut self,
|
||||
control_point: Arc<ControlPoint>,
|
||||
) -> Result<(), WebRendererError>;
|
||||
) -> Result<(), MediaRendererError>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
@@ -43,8 +43,8 @@ impl WebRendererExt for pmoserver::Server {
|
||||
async fn register_web_renderer(
|
||||
&mut self,
|
||||
control_point: Arc<ControlPoint>,
|
||||
) -> Result<(), WebRendererError> {
|
||||
let registry = Arc::new(RendererRegistry::new(control_point));
|
||||
) -> Result<(), MediaRendererError> {
|
||||
let registry = Arc::new(MediaRendererRegistry::new(control_point));
|
||||
|
||||
// POST /api/webrenderer/register
|
||||
self.add_post_handler_with_state(
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
//! Erreurs liées au WebRenderer
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum WebRendererError {
|
||||
#[error("Invalid argument: {0}")]
|
||||
InvalidArgument(String),
|
||||
|
||||
#[error("Failed to create device: {0}")]
|
||||
DeviceCreationError(String),
|
||||
|
||||
#[error("Failed to register device: {0}")]
|
||||
RegistrationError(String),
|
||||
|
||||
#[error("Server not available")]
|
||||
ServerNotAvailable,
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
//! Action handlers SOAP → Pipeline pour le WebRenderer serveur
|
||||
//!
|
||||
//! 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 pmodidl::DIDLLite;
|
||||
use pmodidl::ToXmlElement;
|
||||
use pmoupnp::actions::{get_value, ActionHandler};
|
||||
use pmoupnp::{action_handler, get, set};
|
||||
|
||||
use crate::messages::PlaybackState;
|
||||
use crate::pipeline::{upnp_time_to_seconds, PipelineControl, PipelineHandle};
|
||||
use crate::state::SharedState;
|
||||
|
||||
// ─── AVTransport : commandes de transport ─────────────────────────────────────
|
||||
|
||||
pub fn play_handler(
|
||||
pipeline: PipelineHandle,
|
||||
state: SharedState,
|
||||
instance_id: String,
|
||||
) -> ActionHandler {
|
||||
action_handler!(
|
||||
captures(pipeline, state, instance_id) | data | {
|
||||
tracing::info!("[WebRenderer] UPnP Play action invoked");
|
||||
let has_uri = state.read().current_uri.is_some();
|
||||
if !has_uri {
|
||||
tracing::warn!("[WebRenderer] UPnP Play ignored: no URI loaded");
|
||||
return Ok(data);
|
||||
}
|
||||
{
|
||||
let mut s = state.write();
|
||||
s.playback_state = PlaybackState::Transitioning;
|
||||
s.push_command(crate::adapter::DeviceCommand::Stream {
|
||||
url: format!("/api/webrenderer/{}/stream", instance_id),
|
||||
});
|
||||
tracing::info!("UPnP Play: stored stream command for frontend polling");
|
||||
}
|
||||
pipeline.flac_handle.resume();
|
||||
pipeline.send(PipelineControl::Play).await;
|
||||
Ok(data)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn stop_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
|
||||
action_handler!(
|
||||
captures(pipeline, state) | data | {
|
||||
pipeline.send(PipelineControl::Stop).await;
|
||||
pipeline.flac_handle.pause();
|
||||
pipeline
|
||||
.adapter
|
||||
.deliver(crate::adapter::DeviceCommand::Flush);
|
||||
pipeline
|
||||
.adapter
|
||||
.deliver(crate::adapter::DeviceCommand::Stop);
|
||||
state.write().playback_state = PlaybackState::Stopped;
|
||||
Ok(data)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn pause_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
|
||||
action_handler!(
|
||||
captures(pipeline, state) | data | {
|
||||
pipeline.send(PipelineControl::Pause).await;
|
||||
pipeline.flac_handle.pause();
|
||||
pipeline
|
||||
.adapter
|
||||
.deliver(crate::adapter::DeviceCommand::Pause);
|
||||
state.write().playback_state = PlaybackState::Paused;
|
||||
Ok(data)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn next_handler(pipeline: PipelineHandle) -> ActionHandler {
|
||||
action_handler!(
|
||||
captures(pipeline) | data | {
|
||||
pipeline.send(PipelineControl::Play).await;
|
||||
Ok(data)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn previous_handler(pipeline: PipelineHandle) -> ActionHandler {
|
||||
action_handler!(
|
||||
captures(pipeline) | data | {
|
||||
pipeline.send(PipelineControl::Play).await;
|
||||
Ok(data)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn seek_handler(pipeline: PipelineHandle) -> ActionHandler {
|
||||
action_handler!(
|
||||
captures(pipeline) | data | {
|
||||
let target: String = get!(&data, "Target", String);
|
||||
let pos_sec = upnp_time_to_seconds(&target);
|
||||
pipeline.send(PipelineControl::Seek(pos_sec)).await;
|
||||
Ok(data)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ─── AVTransport : chargement de média ────────────────────────────────────────
|
||||
|
||||
pub fn set_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
|
||||
action_handler!(captures(pipeline, state) |mut data| {
|
||||
tracing::info!("[WebRenderer] UPnP SetAVTransportURI action invoked");
|
||||
let uri: String = get!(&data, "CurrentURI", String);
|
||||
let metadata: String = get_value::<String>(&data, "CurrentURIMetaData")
|
||||
.or_else(|_| get_value::<DIDLLite>(&data, "CurrentURIMetaData").map(|didl| didl.to_xml()))
|
||||
.unwrap_or_default();
|
||||
|
||||
tracing::info!(uri = %uri, "SetAVTransportURI handler called - loading URI into pipeline");
|
||||
{
|
||||
let mut s = state.write();
|
||||
s.current_uri = Some(uri.clone());
|
||||
s.current_metadata = Some(metadata);
|
||||
s.playback_state = PlaybackState::Transitioning;
|
||||
}
|
||||
pipeline.send(PipelineControl::LoadUri(uri)).await;
|
||||
Ok(data)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_next_uri_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandler {
|
||||
action_handler!(captures(pipeline, state) |mut data| {
|
||||
let uri: String = get!(&data, "NextURI", String);
|
||||
let metadata: String = get_value::<String>(&data, "NextURIMetaData")
|
||||
.or_else(|_| get_value::<DIDLLite>(&data, "NextURIMetaData").map(|didl| didl.to_xml()))
|
||||
.unwrap_or_default();
|
||||
|
||||
{
|
||||
let mut s = state.write();
|
||||
s.next_uri = Some(uri.clone());
|
||||
s.next_metadata = Some(metadata);
|
||||
}
|
||||
pipeline.send(PipelineControl::LoadNextUri(uri)).await;
|
||||
Ok(data)
|
||||
})
|
||||
}
|
||||
|
||||
// ─── AVTransport : getters ─────────────────────────────────────────────────────
|
||||
|
||||
pub fn get_position_info_handler(state: SharedState) -> ActionHandler {
|
||||
action_handler!(captures(state) |mut data| {
|
||||
let s = state.read();
|
||||
set!(&mut data, "Track", if s.current_uri.is_some() { 1u32 } else { 0u32 });
|
||||
set!(&mut data, "TrackDuration", s.duration.clone().unwrap_or_else(|| "00:00:00".to_string()));
|
||||
set!(&mut data, "TrackURI", s.current_uri.clone().unwrap_or_default());
|
||||
set!(&mut data, "TrackMetaData", s.current_metadata.clone().unwrap_or_default());
|
||||
set!(&mut data, "RelTime", s.position.clone().unwrap_or_else(|| "00:00:00".to_string()));
|
||||
set!(&mut data, "AbsTime", s.position.clone().unwrap_or_else(|| "00:00:00".to_string()));
|
||||
Ok(data)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_transport_info_handler(state: SharedState) -> ActionHandler {
|
||||
action_handler!(captures(state) |mut data| {
|
||||
let s = state.read();
|
||||
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, "CurrentTransportStatus", "OK".to_string());
|
||||
set!(&mut data, "CurrentSpeed", "1".to_string());
|
||||
Ok(data)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_media_info_handler(state: SharedState) -> ActionHandler {
|
||||
action_handler!(captures(state) |mut data| {
|
||||
let s = state.read();
|
||||
set!(&mut data, "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());
|
||||
Ok(data)
|
||||
})
|
||||
}
|
||||
|
||||
// ─── ConnectionManager ─────────────────────────────────────────────────────────
|
||||
|
||||
pub fn get_protocol_info_handler() -> ActionHandler {
|
||||
action_handler!(|mut data| {
|
||||
set!(&mut data, "Source", String::new());
|
||||
set!(
|
||||
&mut data,
|
||||
"Sink",
|
||||
"http-get:*:audio/flac:*,http-get:*:audio/x-flac:*".to_string()
|
||||
);
|
||||
Ok(data)
|
||||
})
|
||||
}
|
||||
|
||||
// ─── RenderingControl ──────────────────────────────────────────────────────────
|
||||
|
||||
pub fn set_volume_handler(state: SharedState) -> ActionHandler {
|
||||
action_handler!(captures(state) |mut data| {
|
||||
let volume: u16 = get!(&data, "DesiredVolume", u16);
|
||||
state.write().volume = volume;
|
||||
Ok(data)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_volume_handler(state: SharedState) -> ActionHandler {
|
||||
action_handler!(captures(state) |mut data| {
|
||||
let volume = state.read().volume;
|
||||
set!(&mut data, "CurrentVolume", volume);
|
||||
Ok(data)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_mute_handler(state: SharedState) -> ActionHandler {
|
||||
action_handler!(captures(state) |mut data| {
|
||||
let mute: bool = get!(&data, "DesiredMute", bool);
|
||||
state.write().mute = mute;
|
||||
Ok(data)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_mute_handler(state: SharedState) -> ActionHandler {
|
||||
action_handler!(captures(state) |mut data| {
|
||||
let mute = state.read().mute;
|
||||
set!(&mut data, "CurrentMute", mute);
|
||||
Ok(data)
|
||||
})
|
||||
}
|
||||
@@ -6,26 +6,13 @@
|
||||
//! - Les commandes UPnP sont relayées vers le pipeline audio via PipelineControl
|
||||
|
||||
mod adapter;
|
||||
mod error;
|
||||
mod handlers;
|
||||
mod messages;
|
||||
mod pipeline;
|
||||
mod register;
|
||||
mod registry;
|
||||
mod renderer;
|
||||
mod state;
|
||||
mod stream;
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
mod config;
|
||||
|
||||
pub use adapter::{BrowserAdapter, DeviceAdapter, DeviceCommand, DevicePlaybackState, DeviceStateReport};
|
||||
pub use error::WebRendererError;
|
||||
pub use messages::PlaybackState;
|
||||
pub use pipeline::{PipelineControl, PipelineHandle, seconds_to_upnp_time};
|
||||
pub use registry::{RendererRegistry, WebRendererInstance};
|
||||
pub use renderer::{FactoryError, WebRendererFactory};
|
||||
pub use state::{RendererState, SharedState};
|
||||
pub use adapter::BrowserAdapter;
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub use config::WebRendererExt;
|
||||
pub use config::WebRendererExt;
|
||||
@@ -1,12 +0,0 @@
|
||||
//! Types de messages pour le WebRenderer
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum PlaybackState {
|
||||
Stopped,
|
||||
Playing,
|
||||
Paused,
|
||||
Transitioning,
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
//! 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,
|
||||
pub flac_handle: pmoaudio_ext::sinks::OggFlacStreamHandle,
|
||||
pub adapter: Arc<dyn crate::adapter::DeviceAdapter>,
|
||||
#[allow(dead_code)]
|
||||
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,
|
||||
adapter: Arc<dyn crate::adapter::DeviceAdapter>,
|
||||
) -> 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();
|
||||
let adapter_clone = Arc::downgrade(&adapter);
|
||||
#[cfg(feature = "pmoserver")]
|
||||
let cp_clone = control_point.clone();
|
||||
tokio::spawn(async move {
|
||||
run_event_listener(
|
||||
event_rx,
|
||||
state_clone,
|
||||
adapter_clone,
|
||||
udn_clone,
|
||||
#[cfg(feature = "pmoserver")]
|
||||
cp_clone,
|
||||
).await;
|
||||
});
|
||||
|
||||
let pipeline_handle = PipelineHandle {
|
||||
player: player_handle,
|
||||
stop_token: stop_token.clone(),
|
||||
flac_handle: flac_handle.clone(),
|
||||
adapter,
|
||||
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,
|
||||
adapter: std::sync::Weak<dyn crate::adapter::DeviceAdapter>,
|
||||
udn: String,
|
||||
#[cfg(feature = "pmoserver")]
|
||||
control_point: Arc<pmocontrol::ControlPoint>,
|
||||
) {
|
||||
use pmoaudio_ext::PlayerEvent;
|
||||
use crate::messages::PlaybackState;
|
||||
use crate::adapter::DeviceCommand;
|
||||
|
||||
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 => {
|
||||
state.write().playback_state = PlaybackState::Transitioning;
|
||||
// Vider le buffer du device avant la nouvelle piste.
|
||||
if let Some(adapter) = adapter.upgrade() {
|
||||
adapter.deliver(DeviceCommand::Flush);
|
||||
}
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,11 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::messages::PlaybackState;
|
||||
use crate::pipeline::PipelineControl;
|
||||
use crate::registry::RendererRegistry;
|
||||
use pmomediarenderer::PlaybackState;
|
||||
use pmomediarenderer::PipelineControl;
|
||||
use pmomediarenderer::{DeviceAdapter, DeviceCommand, MediaRendererRegistry, PlayerStateReport, SharedState};
|
||||
|
||||
use crate::adapter::BrowserAdapter;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RegisterRequest {
|
||||
@@ -26,14 +28,12 @@ pub struct RegisterRequest {
|
||||
pub struct RegisterResponse {
|
||||
pub stream_url: String,
|
||||
pub udn: String,
|
||||
/// true si le backend est déjà en lecture — le frontend doit démarrer immédiatement
|
||||
pub should_play: bool,
|
||||
}
|
||||
|
||||
/// POST /api/webrenderer/register
|
||||
#[axum::debug_handler]
|
||||
pub async fn register_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Json(req): Json<RegisterRequest>,
|
||||
) -> impl IntoResponse {
|
||||
tracing::info!(
|
||||
@@ -42,8 +42,13 @@ pub async fn register_handler(
|
||||
"WebRenderer: register request"
|
||||
);
|
||||
|
||||
let state: SharedState = Arc::new(parking_lot::RwLock::new(
|
||||
pmomediarenderer::RendererState::default()
|
||||
));
|
||||
let adapter: Arc<dyn DeviceAdapter> = Arc::new(BrowserAdapter::new(state));
|
||||
|
||||
match registry
|
||||
.register_or_reconnect(&req.instance_id, &req.user_agent)
|
||||
.register_or_reconnect(&req.instance_id, &req.user_agent, adapter)
|
||||
.await
|
||||
{
|
||||
Ok((stream_url, udn, should_play)) => {
|
||||
@@ -73,12 +78,9 @@ pub struct PositionUpdateRequest {
|
||||
pub duration_sec: Option<f64>,
|
||||
}
|
||||
|
||||
/// POST /api/webrenderer/{id}/position
|
||||
/// position_sec est ignoré (géré par PlayerEvent::Position côté serveur).
|
||||
/// duration_sec est utilisé comme fallback si la source ne connaît pas la durée (flux radio).
|
||||
#[axum::debug_handler]
|
||||
pub async fn position_update_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Path(instance_id): Path<String>,
|
||||
Json(req): Json<PositionUpdateRequest>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -86,10 +88,9 @@ pub async fn position_update_handler(
|
||||
StatusCode::NO_CONTENT
|
||||
}
|
||||
|
||||
/// DELETE /api/webrenderer/{id}
|
||||
#[axum::debug_handler]
|
||||
pub async fn unregister_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Path(instance_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
tracing::info!(instance_id = %instance_id, "WebRenderer: explicit unregister");
|
||||
@@ -102,10 +103,9 @@ pub struct UriRequest {
|
||||
pub uri: String,
|
||||
}
|
||||
|
||||
/// POST /api/webrenderer/{id}/set_uri - charge une URI et joue
|
||||
#[axum::debug_handler]
|
||||
pub async fn set_uri_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Path(instance_id): Path<String>,
|
||||
Json(req): Json<UriRequest>,
|
||||
) -> impl IntoResponse {
|
||||
@@ -117,48 +117,31 @@ pub async fn set_uri_handler(
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
/// POST /api/webrenderer/{id}/pause
|
||||
#[axum::debug_handler]
|
||||
pub async fn pause_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Path(instance_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
tracing::info!(instance_id = %instance_id, "WebRenderer: pause request");
|
||||
if let Some(instance) = registry.get_instance(instance_id.as_str()) {
|
||||
instance.adapter.deliver(crate::adapter::DeviceCommand::Pause);
|
||||
instance.adapter.deliver(DeviceCommand::Pause);
|
||||
}
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
// ─── Rapports du player ─────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct PlayerStateReport {
|
||||
pub position_sec: Option<f64>,
|
||||
pub duration_sec: Option<f64>,
|
||||
pub state: Option<String>,
|
||||
pub ready_state: Option<String>,
|
||||
}
|
||||
|
||||
/// POST /api/webrenderer/{id}/report - recoit rapports position/state du player
|
||||
#[axum::debug_handler]
|
||||
pub async fn report_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Path(instance_id): Path<String>,
|
||||
Json(report): Json<PlayerStateReport>,
|
||||
) -> impl IntoResponse {
|
||||
// Registry met à jour l'état avec les rapports du player
|
||||
registry.update_player_state(&instance_id, report).await;
|
||||
StatusCode::OK
|
||||
}
|
||||
|
||||
// ─── Commandes vers le player ─────────────────────────────────
|
||||
|
||||
/// GET /api/webrenderer/{id}/command - recupere commande pending pour le player
|
||||
#[axum::debug_handler]
|
||||
pub async fn command_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Path(instance_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match registry.get_pending_command(&instance_id).await {
|
||||
@@ -167,13 +150,11 @@ pub async fn command_handler(
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /api/webrenderer/{id}/play - tell player to stream and play
|
||||
#[axum::debug_handler]
|
||||
pub async fn play_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Path(instance_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
// Check if there's a valid URI loaded - if not, ignore the play command
|
||||
let instance = match registry.get_instance(&instance_id) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
@@ -191,18 +172,14 @@ pub async fn play_handler(
|
||||
|
||||
tracing::info!(instance_id = %instance_id, "WebRenderer: play request");
|
||||
|
||||
// Send Stream command to the adapter (via pending_commands queue)
|
||||
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
|
||||
instance.adapter.deliver(crate::adapter::DeviceCommand::Stream { url: stream_url });
|
||||
instance.adapter.deliver(DeviceCommand::Stream { url: stream_url });
|
||||
|
||||
// Also tell pipeline to play (if not already)
|
||||
instance.pipeline.send(PipelineControl::Play).await;
|
||||
|
||||
(StatusCode::OK, "OK").into_response()
|
||||
}
|
||||
|
||||
// ─── Metadata endpoints ─────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NowPlayingResponse {
|
||||
pub state: String,
|
||||
@@ -216,7 +193,7 @@ pub struct NowPlayingResponse {
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn nowplaying_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Path(instance_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let state = match registry.get_state(&instance_id) {
|
||||
@@ -258,7 +235,7 @@ pub struct RendererStateResponse {
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn state_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Path(instance_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let (state, udn) = match registry.get_state_and_udn(&instance_id) {
|
||||
@@ -285,4 +262,4 @@ pub async fn state_handler(
|
||||
mute: s.mute,
|
||||
};
|
||||
(StatusCode::OK, Json(response)).into_response()
|
||||
}
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
//! Registre des instances WebRenderer actives.
|
||||
//!
|
||||
//! Remplace `SessionManager` et `websocket.rs`. La session est maintenant liée
|
||||
//! au flux FLAC HTTP, pas à une connexion WebSocket.
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use pmoupnp::devices::DeviceInstance;
|
||||
|
||||
use crate::error::WebRendererError;
|
||||
use crate::pipeline::{InstancePipeline, PipelineControl, PipelineHandle};
|
||||
use crate::renderer::WebRendererFactory;
|
||||
use crate::state::{RendererState, SharedState};
|
||||
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use pmocontrol::{ControlPoint, DeviceId};
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use pmocontrol::model::{RendererCapabilities, RendererProtocol};
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use pmoupnp::UpnpTypedInstance;
|
||||
|
||||
/// Une instance WebRenderer côté serveur
|
||||
pub struct WebRendererInstance {
|
||||
pub instance_id: String,
|
||||
pub udn: String,
|
||||
pub device_instance: Arc<DeviceInstance>,
|
||||
pub state: SharedState,
|
||||
/// Handle vers le sink OGG-FLAC — clonable, subscribe() crée un flux indépendant par client.
|
||||
pub flac_handle: pmoaudio_ext::sinks::OggFlacStreamHandle,
|
||||
pub pipeline: PipelineHandle,
|
||||
pub created_at: SystemTime,
|
||||
/// Adapter device-spécifique pour la livraison des commandes.
|
||||
pub adapter: Arc<dyn crate::adapter::DeviceAdapter>,
|
||||
}
|
||||
|
||||
/// Registre global des instances WebRenderer
|
||||
pub struct RendererRegistry {
|
||||
/// Map instance_id → instance
|
||||
instances: RwLock<HashMap<String, Arc<WebRendererInstance>>>,
|
||||
/// Map udn → instance (pour retrouver depuis les handlers UPnP)
|
||||
by_udn: RwLock<HashMap<String, Arc<WebRendererInstance>>>,
|
||||
/// Tokens d'annulation des unregister différés (instance_id → token)
|
||||
pending_unregister: RwLock<HashMap<String, tokio_util::sync::CancellationToken>>,
|
||||
#[cfg(feature = "pmoserver")]
|
||||
control_point: Arc<ControlPoint>,
|
||||
}
|
||||
|
||||
impl RendererRegistry {
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub fn new(control_point: Arc<ControlPoint>) -> Self {
|
||||
Self {
|
||||
instances: RwLock::new(HashMap::new()),
|
||||
by_udn: RwLock::new(HashMap::new()),
|
||||
pending_unregister: RwLock::new(HashMap::new()),
|
||||
control_point,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "pmoserver"))]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
instances: RwLock::new(HashMap::new()),
|
||||
by_udn: RwLock::new(HashMap::new()),
|
||||
pending_unregister: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Enregistre ou reconnecte une instance.
|
||||
/// Retourne `(stream_url, udn, should_play)`.
|
||||
/// `should_play` est true si le backend est déjà en lecture : le frontend doit démarrer immédiatement.
|
||||
pub async fn register_or_reconnect(
|
||||
&self,
|
||||
instance_id: &str,
|
||||
user_agent: &str,
|
||||
) -> Result<(String, String, bool), WebRendererError> {
|
||||
// Annuler tout unregister différé pour cet instance_id
|
||||
if let Some(cancel) = self.pending_unregister.write().remove(instance_id) {
|
||||
tracing::info!(instance_id = %instance_id, "WebRenderer: cancelled pending unregister (page reload)");
|
||||
cancel.cancel();
|
||||
}
|
||||
|
||||
// Reconnexion : l'instance existe déjà (ou vient d'être conservée)
|
||||
{
|
||||
let instances = self.instances.read();
|
||||
if let Some(existing) = instances.get(instance_id) {
|
||||
tracing::info!(instance_id = %instance_id, "WebRenderer: reconnecting existing instance");
|
||||
#[cfg(feature = "pmoserver")]
|
||||
self.register_with_control_point(&existing.device_instance)?;
|
||||
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
|
||||
let should_play = {
|
||||
let s = existing.state.read();
|
||||
s.current_uri.is_some() && matches!(
|
||||
s.playback_state,
|
||||
crate::messages::PlaybackState::Playing | crate::messages::PlaybackState::Transitioning
|
||||
)
|
||||
};
|
||||
return Ok((stream_url, existing.udn.clone(), should_play));
|
||||
}
|
||||
}
|
||||
|
||||
// Première connexion : créer device UPnP + pipeline
|
||||
let instance = self.create_instance(instance_id, user_agent).await?;
|
||||
let instance = Arc::new(instance);
|
||||
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
|
||||
let udn = instance.udn.clone();
|
||||
|
||||
{
|
||||
let mut instances = self.instances.write();
|
||||
instances.insert(instance_id.to_string(), instance.clone());
|
||||
}
|
||||
{
|
||||
let mut by_udn = self.by_udn.write();
|
||||
by_udn.insert(instance.udn.clone(), instance.clone());
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
instance_id = %instance_id,
|
||||
udn = %udn,
|
||||
"WebRenderer: new instance registered"
|
||||
);
|
||||
|
||||
Ok((stream_url, udn, false))
|
||||
}
|
||||
|
||||
/// Retourne un OggFlacClientStream indépendant pour l'endpoint /stream.
|
||||
/// Chaque appel crée un nouveau subscriber broadcast — safe pour connexions simultanées.
|
||||
pub fn get_stream(
|
||||
&self,
|
||||
instance_id: &str,
|
||||
) -> Option<pmoaudio_ext::sinks::OggFlacClientStream> {
|
||||
let instances = self.instances.read();
|
||||
match instances.get(instance_id) {
|
||||
Some(i) => {
|
||||
tracing::debug!(instance_id = %instance_id, "Found instance, getting flac_handle");
|
||||
Some(i.flac_handle.subscribe())
|
||||
}
|
||||
None => {
|
||||
tracing::error!(instance_id = %instance_id, "Instance not found in registry!");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne le PipelineHandle par UDN (pour les handlers UPnP)
|
||||
pub fn get_pipeline_by_udn(&self, udn: &str) -> Option<PipelineHandle> {
|
||||
self.by_udn
|
||||
.read()
|
||||
.get(udn)
|
||||
.map(|i| i.pipeline.clone())
|
||||
}
|
||||
|
||||
/// Retourne l'instance par instance_id
|
||||
pub fn get_instance(&self, instance_id: &str) -> Option<Arc<WebRendererInstance>> {
|
||||
self.instances.read().get(instance_id).cloned()
|
||||
}
|
||||
|
||||
/// Retourne le SharedState par instance_id
|
||||
pub fn get_state(&self, instance_id: &str) -> Option<SharedState> {
|
||||
self.instances
|
||||
.read()
|
||||
.get(instance_id)
|
||||
.map(|i| i.state.clone())
|
||||
}
|
||||
|
||||
/// Retourne le PipelineHandle par instance_id
|
||||
pub fn get_pipeline(&self, instance_id: &str) -> Option<PipelineHandle> {
|
||||
self.instances.read().get(instance_id).map(|i| i.pipeline.clone())
|
||||
}
|
||||
|
||||
/// Retourne le SharedState et udn par instance_id
|
||||
pub fn get_state_and_udn(&self, instance_id: &str) -> Option<(SharedState, String)> {
|
||||
self.instances
|
||||
.read()
|
||||
.get(instance_id)
|
||||
.map(|i| (i.state.clone(), i.udn.clone()))
|
||||
}
|
||||
|
||||
/// Retourne le SharedState par UDN
|
||||
pub fn get_state_by_udn(&self, udn: &str) -> Option<SharedState> {
|
||||
self.by_udn
|
||||
.read()
|
||||
.get(udn)
|
||||
.map(|i| i.state.clone())
|
||||
}
|
||||
|
||||
/// Retourne le DeviceInstance par UDN
|
||||
pub fn get_device_by_udn(&self, udn: &str) -> Option<Arc<DeviceInstance>> {
|
||||
self.by_udn
|
||||
.read()
|
||||
.get(udn)
|
||||
.map(|i| i.device_instance.clone())
|
||||
}
|
||||
|
||||
/// Met à jour la durée depuis le navigateur.
|
||||
/// La position est gérée par PlayerSource via PlayerEvent::Position.
|
||||
/// On n'utilise duration_sec que si la source ne la connaît pas (flux radio sans durée).
|
||||
pub fn update_duration(&self, instance_id: &str, duration_sec: Option<f64>) {
|
||||
let instances = self.instances.read();
|
||||
if let Some(instance) = instances.get(instance_id) {
|
||||
let mut s = instance.state.write();
|
||||
// N'écraser la durée que si elle n'est pas déjà connue (la source est prioritaire)
|
||||
if s.duration.is_none() {
|
||||
if let Some(dur) = duration_sec {
|
||||
if dur > 0.0 {
|
||||
s.duration = Some(crate::pipeline::seconds_to_upnp_time(dur));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn schedule_unregister(self: &Arc<Self>, instance_id: &str) {
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
// Ne pas détruire immédiatement : attendre 5s au cas où la page se recharge
|
||||
let cancel = CancellationToken::new();
|
||||
self.pending_unregister.write().insert(instance_id.to_string(), cancel.clone());
|
||||
|
||||
let instance_id_owned = instance_id.to_string();
|
||||
let registry = Arc::clone(self);
|
||||
|
||||
tracing::info!(instance_id = %instance_id, "WebRenderer: unregister scheduled (5s grace period)");
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => {
|
||||
tracing::info!(instance_id = %instance_id_owned, "WebRenderer: deferred unregister cancelled (page reload)");
|
||||
}
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {
|
||||
registry.pending_unregister.write().remove(&instance_id_owned);
|
||||
let instance = registry.instances.write().remove(&instance_id_owned);
|
||||
if let Some(instance) = instance {
|
||||
registry.by_udn.write().remove(&instance.udn);
|
||||
instance.pipeline.stop_token.cancel();
|
||||
#[cfg(feature = "pmoserver")]
|
||||
if let Ok(mut reg) = registry.control_point.registry().write() {
|
||||
reg.device_says_byebye(&instance.udn);
|
||||
}
|
||||
tracing::info!(
|
||||
instance_id = %instance_id_owned,
|
||||
udn = %instance.udn,
|
||||
"WebRenderer: instance unregistered"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Met à jour l'état avec les rapports du player
|
||||
pub async fn update_player_state(
|
||||
&self,
|
||||
instance_id: &str,
|
||||
report: crate::register::PlayerStateReport,
|
||||
) {
|
||||
let instances = self.instances.read();
|
||||
if let Some(instance) = instances.get(instance_id) {
|
||||
let mut state = instance.state.write();
|
||||
if let Some(pos) = report.position_sec {
|
||||
state.position = Some(crate::pipeline::seconds_to_upnp_time(pos));
|
||||
}
|
||||
if let Some(dur) = report.duration_sec {
|
||||
state.duration = Some(crate::pipeline::seconds_to_upnp_time(dur));
|
||||
}
|
||||
if let Some(s) = &report.state {
|
||||
state.playback_state = match s.as_str() {
|
||||
"playing" => crate::messages::PlaybackState::Playing,
|
||||
"paused" => crate::messages::PlaybackState::Paused,
|
||||
"stopped" => crate::messages::PlaybackState::Stopped,
|
||||
_ => state.playback_state.clone(),
|
||||
};
|
||||
}
|
||||
tracing::debug!(instance_id = %instance_id, position = ?state.position, "player state updated");
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère et consomme la commande en attente pour le player
|
||||
pub async fn get_pending_command(
|
||||
&self,
|
||||
instance_id: &str,
|
||||
) -> Option<serde_json::Value> {
|
||||
let state = self.instances.read().get(instance_id).map(|i| i.state.clone())?;
|
||||
let cmd = state.write().pop_command()?;
|
||||
serde_json::to_value(cmd).ok()
|
||||
}
|
||||
|
||||
// ── Création d'instance ────────────────────────────────────────────────────
|
||||
|
||||
async fn create_instance(
|
||||
&self,
|
||||
instance_id: &str,
|
||||
user_agent: &str,
|
||||
) -> Result<WebRendererInstance, WebRendererError> {
|
||||
// UDN stable dérivé de l'instance_id
|
||||
let candidate_udn = instance_id.to_ascii_lowercase();
|
||||
let full_udn = format!("uuid:{}", candidate_udn);
|
||||
|
||||
// Persister l'UDN dans la config (pour que device_instance.rs le retrouve)
|
||||
if let Err(e) = pmoconfig::get_config().set_device_udn(
|
||||
"MediaRenderer",
|
||||
instance_id,
|
||||
candidate_udn.clone(),
|
||||
) {
|
||||
tracing::warn!("WebRenderer: failed to persist UDN: {:?}", e);
|
||||
}
|
||||
|
||||
let state: SharedState = Arc::new(parking_lot::RwLock::new(RendererState::default()));
|
||||
|
||||
// Créer l'adapter avant le pipeline (sera passé à run_event_listener)
|
||||
let adapter: Arc<dyn crate::adapter::DeviceAdapter> =
|
||||
Arc::new(crate::adapter::BrowserAdapter::new(state.clone()));
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
let (device_instance, pipeline) = {
|
||||
use pmoupnp::UpnpServerExt;
|
||||
|
||||
let server_arc = pmoserver::get_server()
|
||||
.ok_or(WebRendererError::ServerNotAvailable)?;
|
||||
|
||||
// Créer le pipeline avec l'adapter pour le event listener
|
||||
let ip = InstancePipeline::start(
|
||||
state.clone(),
|
||||
self.control_point.clone(),
|
||||
full_udn.clone(),
|
||||
adapter.clone(),
|
||||
);
|
||||
let pipeline = ip.pipeline_handle.clone();
|
||||
|
||||
// Vérifier si un device avec ce même UDN existe déjà
|
||||
let existing_di = {
|
||||
let server = server_arc.read().await;
|
||||
server.get_device(&candidate_udn)
|
||||
};
|
||||
|
||||
let di = if let Some(di) = existing_di {
|
||||
tracing::info!(udn = %candidate_udn, "WebRenderer: reusing device from registry");
|
||||
di
|
||||
} else {
|
||||
// Créer le device UPnP
|
||||
tracing::info!(udn = %candidate_udn, "WebRenderer: creating new device");
|
||||
let device = WebRendererFactory::create_device_with_pipeline(
|
||||
instance_id,
|
||||
user_agent,
|
||||
pipeline.clone(),
|
||||
state.clone(),
|
||||
)
|
||||
.map_err(|e| WebRendererError::DeviceCreationError(e.to_string()))?;
|
||||
|
||||
let mut server = server_arc.write().await;
|
||||
server
|
||||
.register_device(Arc::new(device), false)
|
||||
.await
|
||||
.map_err(|e| WebRendererError::RegistrationError(e.to_string()))?
|
||||
};
|
||||
|
||||
self.register_with_control_point(&di)?;
|
||||
(di, ip)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "pmoserver"))]
|
||||
let (device_instance, pipeline) = {
|
||||
use pmoupnp::UpnpModel;
|
||||
|
||||
let ip = InstancePipeline::start(
|
||||
state.clone(),
|
||||
full_udn.clone(),
|
||||
adapter.clone(),
|
||||
);
|
||||
let pipeline = ip.pipeline_handle.clone();
|
||||
|
||||
let device = WebRendererFactory::create_device_with_pipeline(
|
||||
instance_id,
|
||||
user_agent,
|
||||
pipeline.clone(),
|
||||
state.clone(),
|
||||
)
|
||||
.map_err(|e| WebRendererError::DeviceCreationError(e.to_string()))?;
|
||||
|
||||
(Arc::new(device).create_instance(), ip)
|
||||
};
|
||||
|
||||
Ok(WebRendererInstance {
|
||||
instance_id: instance_id.to_string(),
|
||||
udn: full_udn,
|
||||
device_instance,
|
||||
state,
|
||||
flac_handle: pipeline.flac_handle.clone(),
|
||||
pipeline: pipeline.pipeline_handle,
|
||||
created_at: SystemTime::now(),
|
||||
adapter,
|
||||
})
|
||||
}
|
||||
|
||||
/// Enregistre le device dans le ControlPoint
|
||||
#[cfg(feature = "pmoserver")]
|
||||
fn register_with_control_point(
|
||||
&self,
|
||||
di: &Arc<DeviceInstance>,
|
||||
) -> Result<(), WebRendererError> {
|
||||
let base_url = di.base_url().to_string();
|
||||
let udn = di.udn().to_ascii_lowercase();
|
||||
let udn_with_prefix = format!("uuid:{}", udn);
|
||||
let device_route = di.route();
|
||||
let model = di.get_model();
|
||||
|
||||
let avtransport_control_url = Some(format!(
|
||||
"{}{}/service/AVTransport/control",
|
||||
base_url, device_route
|
||||
));
|
||||
let rendering_control_url = Some(format!(
|
||||
"{}{}/service/RenderingControl/control",
|
||||
base_url, device_route
|
||||
));
|
||||
let connection_manager_url = Some(format!(
|
||||
"{}{}/service/ConnectionManager/control",
|
||||
base_url, device_route
|
||||
));
|
||||
|
||||
let renderer_info = pmocontrol::RendererInfo::make(
|
||||
DeviceId(udn_with_prefix.clone()),
|
||||
udn_with_prefix.clone(),
|
||||
model.friendly_name().to_string(),
|
||||
model.model_name().to_string(),
|
||||
"PMOMusic".to_string(),
|
||||
RendererProtocol::UpnpAvOnly,
|
||||
RendererCapabilities {
|
||||
has_avtransport: true,
|
||||
has_avtransport_set_next: true,
|
||||
has_rendering_control: true,
|
||||
has_connection_manager: true,
|
||||
..Default::default()
|
||||
},
|
||||
format!("{}{}", base_url, di.description_route()),
|
||||
"PMOMusic WebRenderer/2.0".to_string(),
|
||||
Some("urn:schemas-upnp-org:service:AVTransport:1".to_string()),
|
||||
avtransport_control_url,
|
||||
Some("urn:schemas-upnp-org:service:RenderingControl:1".to_string()),
|
||||
rendering_control_url,
|
||||
Some("urn:schemas-upnp-org:service:ConnectionManager:1".to_string()),
|
||||
connection_manager_url,
|
||||
None, None, None, None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
);
|
||||
|
||||
if let Ok(mut registry) = self.control_point.registry().write() {
|
||||
registry.push_renderer(&renderer_info, 86400);
|
||||
}
|
||||
|
||||
tracing::info!(udn = %udn, "WebRenderer: registered with ControlPoint");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
//! Factory pour créer des instances WebRenderer privées
|
||||
//!
|
||||
//! Construit des Device/Service models UPnP dynamiques avec des action handlers
|
||||
//! qui relaient les commandes SOAP vers le navigateur via WebSocket.
|
||||
|
||||
use pmoupnp::actions::{Action, Argument};
|
||||
use pmoupnp::devices::Device;
|
||||
use pmoupnp::services::Service;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::handlers;
|
||||
use crate::pipeline::PipelineHandle;
|
||||
use crate::state::SharedState;
|
||||
|
||||
// ─── Helper functions pour l'ajout d'arguments UPnP ───────────────────────
|
||||
fn add_arg_in(
|
||||
action: &mut Action,
|
||||
name: &str,
|
||||
var: &Arc<pmoupnp::state_variables::StateVariable>,
|
||||
) -> Result<(), FactoryError> {
|
||||
action
|
||||
.add_argument(Arc::new(Argument::new_in(
|
||||
name.to_string(),
|
||||
Arc::clone(var),
|
||||
)))
|
||||
.map_err(|e| FactoryError::ActionError(format!("{:?}", e)))
|
||||
}
|
||||
|
||||
fn add_arg_out(
|
||||
action: &mut Action,
|
||||
name: &str,
|
||||
var: &Arc<pmoupnp::state_variables::StateVariable>,
|
||||
) -> Result<(), FactoryError> {
|
||||
action
|
||||
.add_argument(Arc::new(Argument::new_out(
|
||||
name.to_string(),
|
||||
Arc::clone(var),
|
||||
)))
|
||||
.map_err(|e| FactoryError::ActionError(format!("{:?}", e)))
|
||||
}
|
||||
|
||||
fn add_var(
|
||||
svc: &mut Service,
|
||||
var: &Arc<pmoupnp::state_variables::StateVariable>,
|
||||
) -> Result<(), FactoryError> {
|
||||
svc.add_variable(Arc::clone(var))
|
||||
.map_err(|e| FactoryError::VariableError(e.to_string()))
|
||||
}
|
||||
|
||||
fn add_action(svc: &mut Service, action: Arc<Action>) -> Result<(), FactoryError> {
|
||||
svc.add_action(action)
|
||||
.map_err(|e| FactoryError::ActionError(e.to_string()))
|
||||
}
|
||||
|
||||
// ─── Réimport des variables statiques de pmomediarenderer ───────────────────
|
||||
// Variables AVTransport
|
||||
use pmomediarenderer::avtransport::variables::{
|
||||
ABSOLUTETIMEPOSITION, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI,
|
||||
AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID as AVT_INSTANCE_ID, A_ARG_TYPE_PLAY_SPEED,
|
||||
A_ARG_TYPE_SEEKMODE, CURRENTMEDIADURATION, CURRENTPLAYMODE, CURRENTTRACK, CURRENTTRACKDURATION,
|
||||
CURRENTTRACKMETADATA, CURRENTTRACKURI, NUMBEROFTRACKS, PLAYBACKSTORAGEMEDIUM,
|
||||
POSSIBLEPLAYBACKSTORAGEMEDIA, RELATIVETIMEPOSITION, SEEKMODE, TRANSPORTPLAYSPEED,
|
||||
TRANSPORTSTATE, TRANSPORTSTATUS,
|
||||
};
|
||||
|
||||
// Variables RenderingControl
|
||||
use pmomediarenderer::renderingcontrol::variables::{
|
||||
A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID as RC_INSTANCE_ID, MUTE, VOLUME,
|
||||
};
|
||||
|
||||
// Variables ConnectionManager
|
||||
use pmomediarenderer::connectionmanager::variables::{
|
||||
A_ARG_TYPE_AVTRANSPORTID, A_ARG_TYPE_CONNECTIONID, A_ARG_TYPE_CONNECTIONSTATUS,
|
||||
A_ARG_TYPE_DIRECTION, A_ARG_TYPE_PROTOCOLINFO, A_ARG_TYPE_RCSID, CURRENTCONNECTIONIDS,
|
||||
SINKPROTOCOLINFO, SOURCEPROTOCOLINFO,
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FactoryError {
|
||||
#[error("Failed to add service to device: {0}")]
|
||||
ServiceError(String),
|
||||
#[error("Failed to add action to service: {0}")]
|
||||
ActionError(String),
|
||||
#[error("Failed to add variable to service: {0}")]
|
||||
VariableError(String),
|
||||
}
|
||||
|
||||
/// Extrait un nom de navigateur court depuis un User-Agent complet.
|
||||
fn extract_browser_name(ua: &str) -> &str {
|
||||
if ua.contains("Edg/") || ua.contains("EdgA/") {
|
||||
"Edge"
|
||||
} else if ua.contains("OPR/") || ua.contains("Opera") {
|
||||
"Opera"
|
||||
} else if ua.contains("Chrome/") {
|
||||
"Chrome"
|
||||
} else if ua.contains("Firefox/") {
|
||||
"Firefox"
|
||||
} else if ua.contains("Safari/") {
|
||||
"Safari"
|
||||
} else {
|
||||
"Browser"
|
||||
}
|
||||
}
|
||||
|
||||
/// Factory pour créer des Device UPnP WebRenderer avec un pipeline audio serveur
|
||||
pub struct WebRendererFactory;
|
||||
|
||||
impl WebRendererFactory {
|
||||
/// Crée un Device model UPnP complet pour un WebRenderer.
|
||||
///
|
||||
/// `device_name` sert de clé pour retrouver l'UDN persistant dans la config.
|
||||
/// `browser_ua` est le User-Agent complet (pour déterminer le nom affiché).
|
||||
pub fn create_device_with_pipeline(
|
||||
device_name: &str,
|
||||
browser_ua: &str,
|
||||
pipeline: PipelineHandle,
|
||||
state: SharedState,
|
||||
) -> Result<Device, FactoryError> {
|
||||
let avtransport = Self::build_avtransport(pipeline.clone(), state.clone(), device_name)?;
|
||||
let renderingcontrol = Self::build_renderingcontrol(state.clone())?;
|
||||
let connectionmanager = Self::build_connectionmanager()?;
|
||||
|
||||
let short_name = extract_browser_name(browser_ua);
|
||||
let mut device = Device::new(
|
||||
device_name.to_string(),
|
||||
"MediaRenderer".to_string(),
|
||||
format!("Web Audio – {}", short_name),
|
||||
);
|
||||
device.set_model_name("WebRenderer".to_string());
|
||||
device
|
||||
.add_service(Arc::new(avtransport))
|
||||
.map_err(|e| FactoryError::ServiceError(format!("{:?}", e)))?;
|
||||
device
|
||||
.add_service(Arc::new(renderingcontrol))
|
||||
.map_err(|e| FactoryError::ServiceError(format!("{:?}", e)))?;
|
||||
device
|
||||
.add_service(Arc::new(connectionmanager))
|
||||
.map_err(|e| FactoryError::ServiceError(format!("{:?}", e)))?;
|
||||
|
||||
Ok(device)
|
||||
}
|
||||
|
||||
/// Construit le service AVTransport avec les handlers pipeline
|
||||
fn build_avtransport(
|
||||
pipeline: PipelineHandle,
|
||||
state: SharedState,
|
||||
instance_id: &str,
|
||||
) -> Result<Service, FactoryError> {
|
||||
let mut svc = Service::new("AVTransport".to_string());
|
||||
|
||||
// Ajouter toutes les variables d'état
|
||||
add_var(&mut svc, &AVT_INSTANCE_ID)?;
|
||||
add_var(&mut svc, &A_ARG_TYPE_PLAY_SPEED)?;
|
||||
add_var(&mut svc, &A_ARG_TYPE_SEEKMODE)?;
|
||||
add_var(&mut svc, &ABSOLUTETIMEPOSITION)?;
|
||||
add_var(&mut svc, &AVTRANSPORTNEXTURI)?;
|
||||
add_var(&mut svc, &AVTRANSPORTNEXTURIMETADATA)?;
|
||||
add_var(&mut svc, &AVTRANSPORTURI)?;
|
||||
add_var(&mut svc, &AVTRANSPORTURIMETADATA)?;
|
||||
add_var(&mut svc, &CURRENTMEDIADURATION)?;
|
||||
add_var(&mut svc, &CURRENTPLAYMODE)?;
|
||||
add_var(&mut svc, &CURRENTTRACK)?;
|
||||
add_var(&mut svc, &CURRENTTRACKDURATION)?;
|
||||
add_var(&mut svc, &CURRENTTRACKMETADATA)?;
|
||||
add_var(&mut svc, &CURRENTTRACKURI)?;
|
||||
add_var(&mut svc, &NUMBEROFTRACKS)?;
|
||||
add_var(&mut svc, &PLAYBACKSTORAGEMEDIUM)?;
|
||||
add_var(&mut svc, &POSSIBLEPLAYBACKSTORAGEMEDIA)?;
|
||||
add_var(&mut svc, &RELATIVETIMEPOSITION)?;
|
||||
add_var(&mut svc, &SEEKMODE)?;
|
||||
add_var(&mut svc, &TRANSPORTPLAYSPEED)?;
|
||||
add_var(&mut svc, &TRANSPORTSTATE)?;
|
||||
add_var(&mut svc, &TRANSPORTSTATUS)?;
|
||||
|
||||
// Play
|
||||
let mut play = Action::new("Play".to_string());
|
||||
add_arg_in(&mut play, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
add_arg_in(&mut play, "Speed", &TRANSPORTPLAYSPEED)?;
|
||||
play.set_handler(handlers::play_handler(pipeline.clone(), state.clone(), instance_id.to_string()));
|
||||
add_action(&mut svc, Arc::new(play))?;
|
||||
|
||||
// Stop
|
||||
let mut stop = Action::new("Stop".to_string());
|
||||
add_arg_in(&mut stop, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
stop.set_handler(handlers::stop_handler(pipeline.clone(), state.clone()));
|
||||
add_action(&mut svc, Arc::new(stop))?;
|
||||
|
||||
// Pause
|
||||
let mut pause = Action::new("Pause".to_string());
|
||||
add_arg_in(&mut pause, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
pause.set_handler(handlers::pause_handler(pipeline.clone(), state.clone()));
|
||||
add_action(&mut svc, Arc::new(pause))?;
|
||||
|
||||
// Next
|
||||
let mut next = Action::new("Next".to_string());
|
||||
add_arg_in(&mut next, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
next.set_handler(handlers::next_handler(pipeline.clone()));
|
||||
add_action(&mut svc, Arc::new(next))?;
|
||||
|
||||
// Previous
|
||||
let mut previous = Action::new("Previous".to_string());
|
||||
add_arg_in(&mut previous, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
previous.set_handler(handlers::previous_handler(pipeline.clone()));
|
||||
add_action(&mut svc, Arc::new(previous))?;
|
||||
|
||||
// Seek
|
||||
let mut seek = Action::new("Seek".to_string());
|
||||
add_arg_in(&mut seek, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
add_arg_in(&mut seek, "Unit", &A_ARG_TYPE_SEEKMODE)?;
|
||||
add_arg_in(&mut seek, "Target", &SEEKMODE)?;
|
||||
seek.set_handler(handlers::seek_handler(pipeline.clone()));
|
||||
add_action(&mut svc, Arc::new(seek))?;
|
||||
|
||||
// SetAVTransportURI
|
||||
let mut set_uri = Action::new("SetAVTransportURI".to_string());
|
||||
add_arg_in(&mut set_uri, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
add_arg_in(&mut set_uri, "CurrentURI", &AVTRANSPORTURI)?;
|
||||
add_arg_in(&mut set_uri, "CurrentURIMetaData", &AVTRANSPORTURIMETADATA)?;
|
||||
set_uri.set_handler(handlers::set_uri_handler(pipeline.clone(), state.clone()));
|
||||
add_action(&mut svc, Arc::new(set_uri))?;
|
||||
|
||||
// SetNextAVTransportURI
|
||||
let mut set_next_uri = Action::new("SetNextAVTransportURI".to_string());
|
||||
add_arg_in(&mut set_next_uri, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
add_arg_in(&mut set_next_uri, "NextURI", &AVTRANSPORTNEXTURI)?;
|
||||
add_arg_in(
|
||||
&mut set_next_uri,
|
||||
"NextURIMetaData",
|
||||
&AVTRANSPORTNEXTURIMETADATA,
|
||||
)?;
|
||||
set_next_uri.set_handler(handlers::set_next_uri_handler(
|
||||
pipeline.clone(),
|
||||
state.clone(),
|
||||
));
|
||||
add_action(&mut svc, Arc::new(set_next_uri))?;
|
||||
|
||||
// GetPositionInfo
|
||||
let mut get_pos = Action::new("GetPositionInfo".to_string());
|
||||
add_arg_in(&mut get_pos, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
add_arg_out(&mut get_pos, "Track", &CURRENTTRACK)?;
|
||||
add_arg_out(&mut get_pos, "TrackDuration", &CURRENTTRACKDURATION)?;
|
||||
add_arg_out(&mut get_pos, "TrackURI", &CURRENTTRACKURI)?;
|
||||
add_arg_out(&mut get_pos, "TrackMetaData", &CURRENTTRACKMETADATA)?;
|
||||
add_arg_out(&mut get_pos, "RelTime", &RELATIVETIMEPOSITION)?;
|
||||
add_arg_out(&mut get_pos, "AbsTime", &ABSOLUTETIMEPOSITION)?;
|
||||
get_pos.set_stateful(false);
|
||||
get_pos.set_handler(handlers::get_position_info_handler(state.clone()));
|
||||
add_action(&mut svc, Arc::new(get_pos))?;
|
||||
|
||||
// GetTransportInfo
|
||||
let mut get_info = Action::new("GetTransportInfo".to_string());
|
||||
add_arg_in(&mut get_info, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
add_arg_out(&mut get_info, "CurrentTransportState", &TRANSPORTSTATE)?;
|
||||
add_arg_out(&mut get_info, "CurrentTransportStatus", &TRANSPORTSTATUS)?;
|
||||
add_arg_out(&mut get_info, "CurrentSpeed", &TRANSPORTPLAYSPEED)?;
|
||||
get_info.set_stateful(false);
|
||||
get_info.set_handler(handlers::get_transport_info_handler(state.clone()));
|
||||
add_action(&mut svc, Arc::new(get_info))?;
|
||||
|
||||
// GetMediaInfo
|
||||
let mut get_media = Action::new("GetMediaInfo".to_string());
|
||||
add_arg_in(&mut get_media, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
add_arg_out(&mut get_media, "NrTracks", &NUMBEROFTRACKS)?;
|
||||
add_arg_out(&mut get_media, "CurrentURI", &AVTRANSPORTURI)?;
|
||||
add_arg_out(
|
||||
&mut get_media,
|
||||
"CurrentURIMetaData",
|
||||
&AVTRANSPORTURIMETADATA,
|
||||
)?;
|
||||
add_arg_out(&mut get_media, "NextURI", &AVTRANSPORTNEXTURI)?;
|
||||
add_arg_out(
|
||||
&mut get_media,
|
||||
"NextURIMetaData",
|
||||
&AVTRANSPORTNEXTURIMETADATA,
|
||||
)?;
|
||||
get_media.set_stateful(false);
|
||||
get_media.set_handler(handlers::get_media_info_handler(state.clone()));
|
||||
add_action(&mut svc, Arc::new(get_media))?;
|
||||
|
||||
// GetTransportSettings (passthrough)
|
||||
let mut get_settings = Action::new("GetTransportSettings".to_string());
|
||||
add_arg_in(&mut get_settings, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
add_action(&mut svc, Arc::new(get_settings))?;
|
||||
|
||||
// GetDeviceCapabilities (passthrough)
|
||||
let mut get_caps = Action::new("GetDeviceCapabilities".to_string());
|
||||
add_arg_in(&mut get_caps, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
add_action(&mut svc, Arc::new(get_caps))?;
|
||||
|
||||
// GetCurrentTransportActions (passthrough)
|
||||
let mut get_actions = Action::new("GetCurrentTransportActions".to_string());
|
||||
add_arg_in(&mut get_actions, "InstanceID", &AVT_INSTANCE_ID)?;
|
||||
add_action(&mut svc, Arc::new(get_actions))?;
|
||||
|
||||
Ok(svc)
|
||||
}
|
||||
|
||||
/// Construit le service RenderingControl
|
||||
fn build_renderingcontrol(state: SharedState) -> Result<Service, FactoryError> {
|
||||
let mut svc = Service::new("RenderingControl".to_string());
|
||||
|
||||
add_var(&mut svc, &RC_INSTANCE_ID)?;
|
||||
add_var(&mut svc, &A_ARG_TYPE_CHANNEL)?;
|
||||
add_var(&mut svc, &VOLUME)?;
|
||||
add_var(&mut svc, &MUTE)?;
|
||||
|
||||
// SetVolume
|
||||
let mut set_vol = Action::new("SetVolume".to_string());
|
||||
add_arg_in(&mut set_vol, "InstanceID", &RC_INSTANCE_ID)?;
|
||||
add_arg_in(&mut set_vol, "Channel", &A_ARG_TYPE_CHANNEL)?;
|
||||
add_arg_in(&mut set_vol, "DesiredVolume", &VOLUME)?;
|
||||
set_vol.set_handler(handlers::set_volume_handler(state.clone()));
|
||||
add_action(&mut svc, Arc::new(set_vol))?;
|
||||
|
||||
// GetVolume
|
||||
let mut get_vol = Action::new("GetVolume".to_string());
|
||||
add_arg_in(&mut get_vol, "InstanceID", &RC_INSTANCE_ID)?;
|
||||
add_arg_in(&mut get_vol, "Channel", &A_ARG_TYPE_CHANNEL)?;
|
||||
add_arg_out(&mut get_vol, "CurrentVolume", &VOLUME)?;
|
||||
get_vol.set_stateful(false);
|
||||
get_vol.set_handler(handlers::get_volume_handler(state.clone()));
|
||||
add_action(&mut svc, Arc::new(get_vol))?;
|
||||
|
||||
// SetMute
|
||||
let mut set_mute = Action::new("SetMute".to_string());
|
||||
add_arg_in(&mut set_mute, "InstanceID", &RC_INSTANCE_ID)?;
|
||||
add_arg_in(&mut set_mute, "Channel", &A_ARG_TYPE_CHANNEL)?;
|
||||
add_arg_in(&mut set_mute, "DesiredMute", &MUTE)?;
|
||||
set_mute.set_handler(handlers::set_mute_handler(state.clone()));
|
||||
add_action(&mut svc, Arc::new(set_mute))?;
|
||||
|
||||
// GetMute
|
||||
let mut get_mute = Action::new("GetMute".to_string());
|
||||
add_arg_in(&mut get_mute, "InstanceID", &RC_INSTANCE_ID)?;
|
||||
add_arg_in(&mut get_mute, "Channel", &A_ARG_TYPE_CHANNEL)?;
|
||||
add_arg_out(&mut get_mute, "CurrentMute", &MUTE)?;
|
||||
get_mute.set_stateful(false);
|
||||
get_mute.set_handler(handlers::get_mute_handler(state.clone()));
|
||||
add_action(&mut svc, Arc::new(get_mute))?;
|
||||
|
||||
Ok(svc)
|
||||
}
|
||||
|
||||
/// Construit le service ConnectionManager
|
||||
fn build_connectionmanager() -> Result<Service, FactoryError> {
|
||||
let mut svc = Service::new("ConnectionManager".to_string());
|
||||
|
||||
add_var(&mut svc, &A_ARG_TYPE_CONNECTIONID)?;
|
||||
add_var(&mut svc, &A_ARG_TYPE_CONNECTIONSTATUS)?;
|
||||
add_var(&mut svc, &A_ARG_TYPE_DIRECTION)?;
|
||||
add_var(&mut svc, &A_ARG_TYPE_PROTOCOLINFO)?;
|
||||
add_var(&mut svc, &A_ARG_TYPE_RCSID)?;
|
||||
add_var(&mut svc, &A_ARG_TYPE_AVTRANSPORTID)?;
|
||||
add_var(&mut svc, &CURRENTCONNECTIONIDS)?;
|
||||
add_var(&mut svc, &SINKPROTOCOLINFO)?;
|
||||
add_var(&mut svc, &SOURCEPROTOCOLINFO)?;
|
||||
|
||||
// GetProtocolInfo
|
||||
let mut get_proto = Action::new("GetProtocolInfo".to_string());
|
||||
add_arg_out(&mut get_proto, "Source", &SOURCEPROTOCOLINFO)?;
|
||||
add_arg_out(&mut get_proto, "Sink", &SINKPROTOCOLINFO)?;
|
||||
get_proto.set_stateful(false);
|
||||
get_proto.set_handler(handlers::get_protocol_info_handler());
|
||||
add_action(&mut svc, Arc::new(get_proto))?;
|
||||
|
||||
// GetCurrentConnectionIDs
|
||||
let mut get_ids = Action::new("GetCurrentConnectionIDs".to_string());
|
||||
add_arg_out(&mut get_ids, "ConnectionIDs", &CURRENTCONNECTIONIDS)?;
|
||||
add_action(&mut svc, Arc::new(get_ids))?;
|
||||
|
||||
// GetCurrentConnectionInfo
|
||||
let mut get_conn = Action::new("GetCurrentConnectionInfo".to_string());
|
||||
add_arg_in(&mut get_conn, "ConnectionID", &A_ARG_TYPE_CONNECTIONID)?;
|
||||
add_arg_out(&mut get_conn, "RcsID", &A_ARG_TYPE_RCSID)?;
|
||||
add_arg_out(&mut get_conn, "AVTransportID", &A_ARG_TYPE_AVTRANSPORTID)?;
|
||||
add_arg_out(&mut get_conn, "ProtocolInfo", &A_ARG_TYPE_PROTOCOLINFO)?;
|
||||
add_arg_out(
|
||||
&mut get_conn,
|
||||
"PeerConnectionManager",
|
||||
&A_ARG_TYPE_PROTOCOLINFO,
|
||||
)?;
|
||||
add_arg_out(&mut get_conn, "PeerConnectionID", &A_ARG_TYPE_CONNECTIONID)?;
|
||||
add_arg_out(&mut get_conn, "Direction", &A_ARG_TYPE_DIRECTION)?;
|
||||
add_arg_out(&mut get_conn, "Status", &A_ARG_TYPE_CONNECTIONSTATUS)?;
|
||||
add_action(&mut svc, Arc::new(get_conn))?;
|
||||
|
||||
Ok(svc)
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
//! État partagé du renderer (backend ↔ pipeline)
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::adapter::DeviceCommand;
|
||||
use crate::messages::PlaybackState;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RendererState {
|
||||
pub playback_state: PlaybackState,
|
||||
pub current_uri: Option<String>,
|
||||
pub current_metadata: Option<String>,
|
||||
pub next_uri: Option<String>,
|
||||
pub next_metadata: Option<String>,
|
||||
pub position: Option<String>,
|
||||
pub duration: Option<String>,
|
||||
pub volume: u16,
|
||||
pub mute: bool,
|
||||
pub pending_commands: VecDeque<DeviceCommand>,
|
||||
}
|
||||
|
||||
impl RendererState {
|
||||
pub fn push_command(&mut self, cmd: DeviceCommand) {
|
||||
self.pending_commands.push_back(cmd);
|
||||
}
|
||||
|
||||
pub fn pop_command(&mut self) -> Option<DeviceCommand> {
|
||||
self.pending_commands.pop_front()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RendererState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
playback_state: PlaybackState::Stopped,
|
||||
current_uri: None,
|
||||
current_metadata: None,
|
||||
next_uri: None,
|
||||
next_metadata: None,
|
||||
position: None,
|
||||
duration: None,
|
||||
volume: 100,
|
||||
mute: false,
|
||||
pending_commands: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SharedState = Arc<RwLock<RendererState>>;
|
||||
@@ -18,11 +18,11 @@ use std::sync::Arc;
|
||||
use tokio_util::io::ReaderStream;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::registry::RendererRegistry;
|
||||
use pmomediarenderer::MediaRendererRegistry;
|
||||
|
||||
/// GET /api/webrenderer/{id}/stream
|
||||
pub async fn stream_handler(
|
||||
State(registry): State<Arc<RendererRegistry>>,
|
||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||
Path(instance_id): Path<String>,
|
||||
headers: HeaderMap,
|
||||
) -> impl IntoResponse {
|
||||
|
||||
Reference in New Issue
Block a user