♻️ 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:
2026-04-05 21:20:51 +02:00
parent fe2d755b4a
commit e375d447d4
26 changed files with 552 additions and 431 deletions

View File

@@ -1,7 +1,7 @@
[package]
name = "pmomediarenderer"
version = "0.1.0"
edition = "2024"
edition = "2021"
[dependencies]
pmoupnp = { path = "../pmoupnp" }
@@ -11,3 +11,28 @@ once_cell = "1.20"
bevy_reflect = "0.17.1"
htmlescape = "0.3"
quick-xml = { workspace = true }
pmoaudio-ext = { path = "../pmoaudio-ext", features = ["http-stream"] }
pmoaudio = { path = "../pmoaudio" }
pmoflac = { path = "../pmoflac" }
pmoconfig = { path = "../pmoconfig" }
pmometadata = { path = "../pmometadata" }
pmoutils = { version = "0.1.2", registry = "pmo" }
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
uuid = { workspace = true, features = ["v4", "serde"] }
parking_lot = "0.12"
thiserror = { workspace = true }
tracing = { workspace = true }
pmoserver = { path = "../pmoserver", optional = true }
pmocontrol = { path = "../pmocontrol", optional = true }
[features]
default = []
pmoserver = ["dep:pmoserver", "dep:pmocontrol"]

View File

@@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize};
#[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>;
}

View File

@@ -1,6 +1,6 @@
use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI,
AVTRANSPORTURIMETADATA, CURRENTTRACK, NUMBEROFTRACKS,
AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
A_ARG_TYPE_INSTANCE_ID, CURRENTTRACK, NUMBEROFTRACKS,
};
use pmoupnp::define_action;

View File

@@ -1,5 +1,5 @@
use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, ABSOLUTETIMEPOSITION, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
ABSOLUTETIMEPOSITION, AVTRANSPORTURI, AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID,
CURRENTTRACK, CURRENTTRACKDURATION, RELATIVETIMEPOSITION,
};
use pmoupnp::define_action;

View File

@@ -1,5 +1,5 @@
use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA,
AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, A_ARG_TYPE_INSTANCE_ID,
};
use pmoupnp::define_action;

View File

@@ -1,5 +1,5 @@
use crate::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
AVTRANSPORTURI, AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID,
};
use pmoupnp::define_action;

View File

@@ -100,8 +100,8 @@ use actions::{
SETNEXTAVTRANSPORTURI, STOP,
};
use variables::{
A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_PLAY_SPEED, A_ARG_TYPE_SEEKMODE, ABSOLUTETIMEPOSITION,
AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI, AVTRANSPORTURIMETADATA,
ABSOLUTETIMEPOSITION, AVTRANSPORTNEXTURI, AVTRANSPORTNEXTURIMETADATA, AVTRANSPORTURI,
AVTRANSPORTURIMETADATA, A_ARG_TYPE_INSTANCE_ID, A_ARG_TYPE_PLAY_SPEED, A_ARG_TYPE_SEEKMODE,
CURRENTMEDIADURATION, CURRENTPLAYMODE, CURRENTTRACK, CURRENTTRACKDURATION,
CURRENTTRACKMETADATA, CURRENTTRACKURI, NUMBEROFTRACKS, PLAYBACKSTORAGEMEDIUM,
POSSIBLEPLAYBACKSTORAGEMEDIA, RELATIVETIMEPOSITION, SEEKMODE, TRANSPORTPLAYSPEED,

View File

@@ -1,66 +0,0 @@
//! Définition du device MediaRenderer.
use once_cell::sync::Lazy;
use std::sync::Arc;
use crate::{
avtransport::AVTTRANSPORT, connectionmanager::CONNECTIONMANAGER,
renderingcontrol::RENDERINGCONTROL,
};
use pmoupnp::devices::Device;
/// Device MediaRenderer UPnP.
///
/// MediaRenderer audio-only conforme UPnP AV Architecture 1.0.
///
/// # Services inclus
///
/// - **AVTransport:1** : Contrôle de la lecture
/// - **RenderingControl:1** : Contrôle du volume et du mute
/// - **ConnectionManager:1** : Gestion des connexions
///
/// # Spécifications
///
/// - Device Type : `urn:schemas-upnp-org:device:MediaRenderer:1`
/// - Version : 1
/// - Manufacturer : PMOMusic
/// - Model : PMOMusic Audio Renderer
///
/// # Exemple
///
/// ```ignore
/// use pmomediarenderer::MEDIA_RENDERER;
/// use pmoupnp::UpnpModel;
///
/// // Créer une instance du renderer
/// let renderer_instance = MEDIA_RENDERER.create_instance();
///
/// // Accéder aux services
/// if let Some(avtransport) = renderer_instance.get_service("AVTransport") {
/// // Contrôler la lecture...
/// }
/// ```
pub static MEDIA_RENDERER: Lazy<Arc<Device>> = Lazy::new(|| {
let mut device = Device::new_from_config(
"PMO_MediaRenderer".to_string(),
"MediaRenderer".to_string(),
"Audio Renderer".to_string(),
);
device.set_model_description("UPnP AV MediaRenderer for audio streaming".to_string());
// Ajouter les trois services obligatoires
device
.add_service(Arc::clone(&AVTTRANSPORT))
.expect("Failed to add AVTransport service");
device
.add_service(Arc::clone(&RENDERINGCONTROL))
.expect("Failed to add RenderingControl service");
device
.add_service(Arc::clone(&CONNECTIONMANAGER))
.expect("Failed to add ConnectionManager service");
Arc::new(device)
});

View File

@@ -0,0 +1,18 @@
//! Erreurs liées au MediaRenderer
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MediaRendererError {
#[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,
}

View File

@@ -0,0 +1,232 @@
//! Action handlers SOAP → Pipeline pour le MediaRenderer
//!
//! 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!("[MediaRenderer] UPnP Play action invoked");
let has_uri = state.read().current_uri.is_some();
if !has_uri {
tracing::warn!("[MediaRenderer] 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),
});
}
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!("[MediaRenderer] 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();
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)
})
}

View File

@@ -11,25 +11,23 @@
//! - **AVTransport** : Contrôle de la lecture (play, pause, stop, seek, etc.)
//! - **RenderingControl** : Contrôle du volume et du mute
//! - **ConnectionManager** : Gestion des connexions et des protocoles supportés
//!
//! # Device UPnP
//!
//! - Type : `urn:schemas-upnp-org:device:MediaRenderer:1`
//! - Services : AVTransport:1, RenderingControl:1, ConnectionManager:1
//!
//! # Utilisation
//!
//! ```ignore
//! use pmomediarenderer::MEDIA_RENDERER;
//!
//! // Le device est déjà configuré avec tous ses services
//! let renderer = MEDIA_RENDERER.clone();
//! let instance = renderer.create_instance();
//! ```
pub mod adapter;
pub mod avtransport;
pub mod connectionmanager;
pub mod device;
pub mod error;
pub mod handlers;
pub mod messages;
pub mod pipeline;
pub mod registry;
pub mod renderingcontrol;
pub mod renderer;
pub mod state;
pub use device::MEDIA_RENDERER;
pub use error::MediaRendererError;
pub use handlers::*;
pub use messages::{PlaybackState, PlayerStateReport};
pub use pipeline::{PipelineControl, PipelineHandle, seconds_to_upnp_time, upnp_time_to_seconds, InstancePipeline};
pub use registry::{MediaRendererInstance, MediaRendererRegistry};
pub use state::{RendererState, SharedState};
pub use adapter::{DeviceAdapter, DeviceCommand, DevicePlaybackState, DeviceStateReport};

View File

@@ -0,0 +1,21 @@
//! Types de messages pour le MediaRenderer
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PlaybackState {
Stopped,
Playing,
Paused,
Transitioning,
}
#[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>,
}

View File

@@ -0,0 +1,203 @@
//! Pipeline audio serveur par instance MediaRenderer
//!
//! Chaque instance MediaRenderer possède un pipeline独立的音频处理
//! - 一个 `PlayerSource` 管理 AVTransport 生命周期Play/Pause/Stop/Seek/LoadUri
//! - 一个 `StreamingOggFlacSink` 编码并向 HTTP 客户端传输 OGG-FLAC 流
//! - 规范化节点(重采样 → 96 kHz转换 → 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 ────────────────────────────────
pub use pmoaudio_ext::PlayerCommand as PipelineControl;
// ─── Handle vers le 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 {
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é ──────────────────────────────────────────────────────
pub struct InstancePipeline {
pub flac_handle: OggFlacStreamHandle,
pub pipeline_handle: PipelineHandle,
}
impl InstancePipeline {
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;
let (sink, flac_handle) = StreamingOggFlacSink::new(EncoderOptions::default(), 24);
let mut to_i24 = ToI24Node::new();
to_i24.register(sink.boxed());
let mut resampler = ResamplingNode::new(96_000);
resampler.register(to_i24.boxed());
let (mut player_source, player_handle) = PlayerSource::new();
player_source.register(resampler.boxed());
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");
});
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;
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;
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,
}
}

View File

@@ -0,0 +1,417 @@
//! Registre des instances MediaRenderer actives.
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::SystemTime;
use pmoupnp::devices::DeviceInstance;
use crate::error::MediaRendererError;
use crate::pipeline::{InstancePipeline, PipelineHandle};
use crate::renderer::MediaRendererFactory;
use crate::state::{RendererState, SharedState};
use super::adapter::DeviceAdapter;
#[cfg(feature = "pmoserver")]
use pmocontrol::{ControlPoint, DeviceId};
#[cfg(feature = "pmoserver")]
use pmocontrol::model::{RendererCapabilities, RendererProtocol};
#[cfg(feature = "pmoserver")]
use pmoupnp::UpnpTypedInstance;
pub struct MediaRendererInstance {
pub instance_id: String,
pub udn: String,
pub device_instance: Arc<DeviceInstance>,
pub state: SharedState,
pub flac_handle: pmoaudio_ext::sinks::OggFlacStreamHandle,
pub pipeline: PipelineHandle,
pub created_at: SystemTime,
pub adapter: Arc<dyn DeviceAdapter>,
}
pub struct MediaRendererRegistry {
instances: RwLock<HashMap<String, Arc<MediaRendererInstance>>>,
by_udn: RwLock<HashMap<String, Arc<MediaRendererInstance>>>,
pending_unregister: RwLock<HashMap<String, tokio_util::sync::CancellationToken>>,
#[cfg(feature = "pmoserver")]
control_point: Arc<ControlPoint>,
}
impl MediaRendererRegistry {
#[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()),
}
}
pub async fn register_or_reconnect(
&self,
instance_id: &str,
user_agent: &str,
adapter: Arc<dyn DeviceAdapter>,
) -> Result<(String, String, bool), MediaRendererError> {
if let Some(cancel) = self.pending_unregister.write().remove(instance_id) {
tracing::info!(instance_id = %instance_id, "MediaRenderer: cancelled pending unregister (page reload)");
cancel.cancel();
}
{
let instances = self.instances.read();
if let Some(existing) = instances.get(instance_id) {
tracing::info!(instance_id = %instance_id, "MediaRenderer: 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));
}
}
let instance = self.create_instance_with_adapter(instance_id, user_agent, adapter).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,
"MediaRenderer: new instance registered"
);
Ok((stream_url, udn, false))
}
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
}
}
}
pub fn get_pipeline_by_udn(&self, udn: &str) -> Option<PipelineHandle> {
self.by_udn
.read()
.get(udn)
.map(|i| i.pipeline.clone())
}
pub fn get_instance(&self, instance_id: &str) -> Option<Arc<MediaRendererInstance>> {
self.instances.read().get(instance_id).cloned()
}
pub fn get_state(&self, instance_id: &str) -> Option<SharedState> {
self.instances
.read()
.get(instance_id)
.map(|i| i.state.clone())
}
pub fn get_pipeline(&self, instance_id: &str) -> Option<PipelineHandle> {
self.instances.read().get(instance_id).map(|i| i.pipeline.clone())
}
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()))
}
pub fn get_state_by_udn(&self, udn: &str) -> Option<SharedState> {
self.by_udn
.read()
.get(udn)
.map(|i| i.state.clone())
}
pub fn get_device_by_udn(&self, udn: &str) -> Option<Arc<DeviceInstance>> {
self.by_udn
.read()
.get(udn)
.map(|i| i.device_instance.clone())
}
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();
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;
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, "MediaRenderer: unregister scheduled (5s grace period)");
tokio::spawn(async move {
tokio::select! {
_ = cancel.cancelled() => {
tracing::info!(instance_id = %instance_id_owned, "MediaRenderer: 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,
"MediaRenderer: instance unregistered"
);
}
}
}
});
}
pub async fn update_player_state(
&self,
instance_id: &str,
report: crate::messages::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");
}
}
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éer une nouvelle instance avec un adapter fourni (permet à l'appelant de créer BrowserAdapter)
pub async fn create_instance_with_adapter(
&self,
instance_id: &str,
user_agent: &str,
adapter: Arc<dyn DeviceAdapter>,
) -> Result<MediaRendererInstance, MediaRendererError> {
let candidate_udn = instance_id.to_ascii_lowercase();
let full_udn = format!("uuid:{}", candidate_udn);
if let Err(e) = pmoconfig::get_config().set_device_udn(
"MediaRenderer",
instance_id,
candidate_udn.clone(),
) {
tracing::warn!("MediaRenderer: failed to persist UDN: {:?}", e);
}
let state: SharedState = Arc::new(parking_lot::RwLock::new(RendererState::default()));
#[cfg(feature = "pmoserver")]
let (device_instance, pipeline) = {
use pmoupnp::UpnpServerExt;
let server_arc = pmoserver::get_server()
.ok_or(MediaRendererError::ServerNotAvailable)?;
let ip = InstancePipeline::start(
state.clone(),
self.control_point.clone(),
full_udn.clone(),
adapter.clone(),
);
let pipeline = ip.pipeline_handle.clone();
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, "MediaRenderer: reusing device from registry");
di
} else {
tracing::info!(udn = %candidate_udn, "MediaRenderer: creating new device");
let device = MediaRendererFactory::create_device_with_pipeline(
instance_id,
"MediaRenderer",
user_agent,
pipeline.clone(),
state.clone(),
)
.map_err(|e| MediaRendererError::DeviceCreationError(e.to_string()))?;
let mut server = server_arc.write().await;
server
.register_device(Arc::new(device), false)
.await
.map_err(|e| MediaRendererError::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 = MediaRendererFactory::create_device_with_pipeline(
instance_id,
"MediaRenderer",
user_agent,
pipeline.clone(),
state.clone(),
)
.map_err(|e| MediaRendererError::DeviceCreationError(e.to_string()))?;
(Arc::new(device).create_instance(), ip)
};
Ok(MediaRendererInstance {
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,
})
}
#[cfg(feature = "pmoserver")]
fn register_with_control_point(
&self,
di: &Arc<DeviceInstance>,
) -> Result<(), MediaRendererError> {
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, "MediaRenderer: registered with ControlPoint");
Ok(())
}
}

View File

@@ -0,0 +1,342 @@
//! Factory pour créer des instances MediaRenderer privées
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 ───────────────────
use crate::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,
};
use crate::renderingcontrol::variables::{
A_ARG_TYPE_CHANNEL, A_ARG_TYPE_INSTANCE_ID as RC_INSTANCE_ID, MUTE, VOLUME,
};
use crate::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),
}
/// Factory pour créer des Device UPnP MediaRenderer avec un pipeline audio serveur
pub struct MediaRendererFactory;
impl MediaRendererFactory {
pub fn create_device_with_pipeline(
device_name: &str,
device_type: &str,
device_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 mut device = Device::new(
device_name.to_string(),
device_type.to_string(),
device_ua.to_string(),
);
device.set_model_name("MediaRenderer".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)
}
fn build_avtransport(
pipeline: PipelineHandle,
state: SharedState,
instance_id: &str,
) -> Result<Service, FactoryError> {
let mut svc = Service::new("AVTransport".to_string());
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)?;
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))?;
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))?;
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))?;
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))?;
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))?;
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))?;
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))?;
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))?;
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))?;
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))?;
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))?;
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))?;
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))?;
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)
}
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)?;
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))?;
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))?;
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))?;
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)
}
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)?;
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))?;
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))?;
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)
}
}

View File

@@ -0,0 +1,51 @@
//! É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>>;