Començons un Mediarenderer

This commit is contained in:
2025-10-01 20:49:50 +02:00
parent 92ef3271d6
commit 4f999c8a3c
32 changed files with 1485 additions and 42 deletions

View File

@@ -5,6 +5,7 @@ edition = "2024"
[dependencies]
pmoconfig = { path = "../pmoconfig" }
pmodidl = { path = "../pmodidl"}
url = "2.5.7"
uuid = "1.18.1"
@@ -34,3 +35,5 @@ anyhow = "1.0"
utoipa = { version = "5.4.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
validator = { version = "0.20.0", features = ["derive"] }
bevy_reflect = "0.17.1"
bevy_reflect_derive = "0.17.1"

View File

@@ -0,0 +1,197 @@
/// Macro pour définir facilement des actions UPnP
///
/// # Syntaxe
///
/// ```ignore
/// define_action! {
/// pub static ACTION_NAME = "ActionName" {
/// in "ParamName" => VARIABLE_REF,
/// in "OtherParam" => OTHER_VAR,
/// out "ResultParam" => RESULT_VAR,
/// }
/// }
/// ```
#[macro_export]
macro_rules! define_action {
// Variante sans arguments
(pub static $name:ident = $action_name:literal) => {
pub static $name: once_cell::sync::Lazy<$crate::actions::Action> =
once_cell::sync::Lazy::new(|| {
$crate::actions::Action::new($action_name.to_string())
});
};
// Variante avec arguments
(pub static $name:ident = $action_name:literal {
$(
$direction:ident $arg_name:literal => $var_ref:expr
),* $(,)?
}) => {
pub static $name: once_cell::sync::Lazy<$crate::actions::Action> =
once_cell::sync::Lazy::new(|| {
let mut ac = $crate::actions::Action::new($action_name.to_string());
$(
ac.add_argument(
define_action!(@arg $direction $arg_name, $var_ref)
);
)*
ac
});
};
// Helpers internes pour créer les arguments
(@arg in $name:literal, $var:expr) => {
$crate::actions::Argument::new_in($name.to_string(), $var.clone())
};
(@arg out $name:literal, $var:expr) => {
$crate::actions::Argument::new_out($name.to_string(), $var.clone())
};
}
// ============= Exemples d'utilisation =============
#[cfg(test)]
mod examples {
use super::*;
// Utilisation originale (pour comparaison)
mod original {
use crate::mediarenderer::avtransport::variables::{
A_ARG_TYPE_INSTANCE_ID,
TRANSPORTPLAYSPEED
};
use crate::actions::{Action, Argument};
use once_cell::sync::Lazy;
pub static PLAY: Lazy<Action> = Lazy::new(|| -> Action {
let mut ac = Action::new("Play".to_string());
ac.add_argument(
Argument::new_in("InstanceID".to_string(),
A_ARG_TYPE_INSTANCE_ID.clone())
);
ac.add_argument(
Argument::new_in("Speed".to_string(),
TRANSPORTPLAYSPEED.clone())
);
ac
});
}
// Avec la macro - Version simple
mod with_macro {
use crate::mediarenderer::avtransport::variables::*;
define_action! {
pub static PLAY = "Play" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
in "Speed" => TRANSPORTPLAYSPEED,
}
}
define_action! {
pub static SETAVTRANSPORTURI = "SetAVTransportURI" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
in "CurrentURI" => AVTRANSPORTURI,
in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA,
}
}
define_action! {
pub static STOP = "Stop" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
}
}
// Action sans arguments
define_action! {
pub static PAUSE = "Pause"
}
}
}
// ============= Macro alternative encore plus concise =============
/// Macro pour définir plusieurs actions en une fois
///
/// # Syntaxe
///
/// ```ignore
/// define_actions! {
/// PLAY = "Play" {
/// in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
/// in "Speed" => TRANSPORTPLAYSPEED,
/// }
///
/// STOP = "Stop" {
/// in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
/// }
/// }
/// ```
#[macro_export]
macro_rules! define_actions {
(
$(
$name:ident = $action_name:literal {
$(
$direction:ident $arg_name:literal => $var_ref:expr
),* $(,)?
}
)*
) => {
$(
define_action! {
pub static $name = $action_name {
$($direction $arg_name => $var_ref),*
}
}
)*
};
// Support pour actions sans arguments
(
$(
$name:ident = $action_name:literal
)*
) => {
$(
define_action! {
pub static $name = $action_name
}
)*
};
}
// ============= Exemple d'utilisation groupée =============
#[cfg(test)]
mod grouped_example {
use crate::mediarenderer::avtransport::variables::*;
define_actions! {
PLAY = "Play" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
in "Speed" => TRANSPORTPLAYSPEED,
}
STOP = "Stop" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
}
PAUSE = "Pause" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
}
SETAVTRANSPORTURI = "SetAVTransportURI" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
in "CurrentURI" => AVTRANSPORTURI,
in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA,
}
}
}

View File

@@ -7,13 +7,15 @@ mod action_instance_set;
mod argument;
mod arg_set_methods;
mod macros;
use std::collections::HashMap;
use crate::{state_variables::StateVariable, UpnpObjectType};
#[derive(Debug, Clone)]
struct Action {
pub struct Action {
object: UpnpObjectType,
arguments: ArgumentSet,
}

View File

@@ -6,6 +6,8 @@ pub mod value_ranges;
pub mod variable_types;
pub mod actions;
pub mod mediarenderer;
pub use crate::object_trait::{UpnpXml,UpnpObject};
#[derive(Debug,Clone)]

View File

@@ -0,0 +1,8 @@
mod play;
mod stop;
mod setavtransporturi;
pub use play::PLAY;
pub use stop::STOP;
pub use setavtransporturi::SETAVTRANSPORTURI;

View File

@@ -0,0 +1,19 @@
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, TRANSPORTPLAYSPEED};
use crate::actions::{Action, Argument};
use once_cell::sync::Lazy;
pub static PLAY: Lazy<Action> = Lazy::new(|| -> Action {
let mut ac = Action::new("Play".to_string());
ac.add_argument(
Argument::new_in("InstanceID".to_string(),
A_ARG_TYPE_INSTANCE_ID.clone())
);
ac.add_argument(
Argument::new_in("Speed".to_string(),
TRANSPORTPLAYSPEED.clone())
);
ac
});

View File

@@ -0,0 +1,10 @@
use crate::mediarenderer::avtransport::variables::{A_ARG_TYPE_INSTANCE_ID, AVTRANSPORTURI, AVTRANSPORTURIMETADATA};
use crate::define_action;
define_action! {
pub static SETAVTRANSPORTURI = "SetAVTransportURI" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
in "CurrentURI" => AVTRANSPORTURI,
in "CurrentURIMetaData" => AVTRANSPORTURIMETADATA,
}
}

View File

@@ -0,0 +1,8 @@
use crate::mediarenderer::avtransport::variables::A_ARG_TYPE_INSTANCE_ID;
use crate::define_action;
define_action! {
pub static STOP = "Stop" {
in "InstanceID" => A_ARG_TYPE_INSTANCE_ID,
}
}

View File

@@ -0,0 +1,3 @@
pub mod variables;
pub mod actions;

View File

@@ -0,0 +1,7 @@
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static A_ARG_TYPE_INSTANCE_ID: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
StateVariable::new(StateVarType::UI4, "A_ARG_TYPE_InstanceID".to_string())
});

View File

@@ -0,0 +1,7 @@
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static A_ARG_TYPE_PLAY_SPEED: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
StateVariable::new(StateVarType::String, "A_ARG_TYPE_PlaySpeed".to_string())
});

View File

@@ -0,0 +1,7 @@
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static AVTRANSPORTURI: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
StateVariable::new(StateVarType::String, "AVTransportURI".to_string())
});

View File

@@ -0,0 +1,33 @@
use std::sync::Arc;
use crate::state_variables::{StateVariable, StateVariableError};
use crate::variable_types::StateVarType;
use bevy_reflect::Reflect;
use once_cell::sync::Lazy;
use pmodidl::{DIDLLite, MediaMetadataParser};
// func _AVTransportURIMetaDataParser(value string) (interface{}, error) {
// log.Debug("[avtransport] Parsing AVTransport)")
// didl, err := pmodidl.Parse(value)
// if err != nil {
// return value, err
// }
// return didl, nil
// }
fn avtransporturimetadataparser(value: &str) -> Result<Box<dyn Reflect>, StateVariableError> {
// Parse DIDL-Lite
let didl = DIDLLite::parse(value)
.map_err(|e| StateVariableError::ParseError(format!("Failed to parse DIDL-Lite: {}", e)))?;
// Retourne le résultat sous forme de Box<dyn Reflect>
Ok(Box::new(didl) as Box<dyn Reflect>)
}
pub static AVTRANSPORTURIMETADATA: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
let mut sv = StateVariable::new(StateVarType::String, "AVTransportURIMetaData".to_string());
sv.set_value_parser(Arc::new(avtransporturimetadataparser)).expect("Failed to set parser");
sv
});

View File

@@ -0,0 +1,8 @@
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static CURRENTTRACKDURATION: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
StateVariable::new(StateVarType::String, "CurrentTrackDuration".to_string())
});

View File

@@ -0,0 +1,22 @@
mod a_arg_type_instanceid;
mod a_arg_type_playspeed;
mod avtransporturi;
mod avtransporturimetadata;
mod currenttrackduration;
mod seekmode;
mod transportplayspeed;
mod transportstate;
mod transportstatus;
pub use a_arg_type_instanceid::A_ARG_TYPE_INSTANCE_ID;
pub use a_arg_type_playspeed::A_ARG_TYPE_PLAY_SPEED;
pub use avtransporturi::AVTRANSPORTURI;
pub use avtransporturimetadata::AVTRANSPORTURIMETADATA;
pub use currenttrackduration::CURRENTTRACKDURATION;
pub use seekmode::SEEKMODE;
pub use transportplayspeed::TRANSPORTPLAYSPEED;
pub use transportstate::TRANSPORTSTATE;
pub use transportstatus::TRANSPORTSTATUS;

View File

@@ -0,0 +1,8 @@
use crate::state_variables::StateVariable;
use crate::variable_types::StateVarType;
use once_cell::sync::Lazy;
pub static SEEKMODE: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
StateVariable::new(StateVarType::String, "SeekMode".to_string())
});

View File

@@ -0,0 +1,13 @@
use crate::state_variables::StateVariable;
use crate::variable_types::{StateValue, StateVarType};
use once_cell::sync::Lazy;
pub static TRANSPORTPLAYSPEED: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
let mut sv = StateVariable::new(StateVarType::String, "TransportPlaySpeed".to_string());
sv.push_allowed_value(&StateValue::String("1".to_string())).expect("Cannot add allowed value");
sv.set_default(&StateValue::String("1".to_string())).expect("Cannt set default value");
sv
});

View File

@@ -0,0 +1,21 @@
use crate::state_variables::StateVariable;
use crate::variable_types::{StateValue, StateVarType};
use once_cell::sync::Lazy;
pub static TRANSPORTSTATE: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
let mut sv = StateVariable::new(StateVarType::String, "TransportState".to_string());
sv.push_allowed_value(&StateValue::String("NO_MEDIA_PRESENT".to_string())).expect("Cannot add allowed value");
sv.extend_allowed_values(&[
StateValue::String("STOPPED".to_string()),
StateValue::String("PLAYING".to_string()),
StateValue::String("RECORDING".to_string()),
StateValue::String("TRANSITIONING".to_string()),
StateValue::String("PAUSED_PLAYBACK".to_string()),
StateValue::String("PAUSED_RECORDING".to_string()),
StateValue::String("NO_MEDIA_PRESENT".to_string()),
]).expect("Cannt set default value");
sv
});

View File

@@ -0,0 +1,17 @@
use crate::state_variables::StateVariable;
use crate::variable_types::{StateValue, StateVarType};
use once_cell::sync::Lazy;
pub static TRANSPORTSTATUS: Lazy<StateVariable> = Lazy::new(|| -> StateVariable {
let mut sv = StateVariable::new(StateVarType::String, "TransportStatus".to_string());
sv.push_allowed_value(&StateValue::String("OK".to_string()))
.expect("Cannot add allowed value");
sv.extend_allowed_values(&[
StateValue::String("OK".to_string()),
StateValue::String("ERROR_OCCURRED".to_string()),
])
.expect("Cannt set default value");
sv
});

View File

@@ -0,0 +1 @@
pub mod avtransport;

View File

@@ -24,8 +24,72 @@ pub trait UpnpXml {
xml_string
}
fn to_markdown(&self) -> String {
let elem = self.to_xml_element();
let mut md = String::new();
fn is_url(s: &str) -> bool {
s.starts_with("http://") || s.starts_with("https://") || s.starts_with("urn:")
}
fn is_image_url(s: &str) -> bool {
let s = s.to_lowercase();
s.ends_with(".png")
|| s.ends_with(".jpg")
|| s.ends_with(".jpeg")
|| s.ends_with(".gif")
|| s.ends_with(".svg")
|| s.ends_with(".webp")
}
fn format_value(v: &str) -> String {
let v = v.trim().to_string(); // <-- clone du texte nettoyé
if is_url(&v) {
if is_image_url(&v) {
format!("[{}]({})<br>![]({})", v, v, v)
} else {
format!("[{}]({})", v, v)
}
} else {
format!("`{}`", v)
}
}
fn recurse(elem: &xmltree::Element, md: &mut String, depth: usize) {
let indent = " ".repeat(depth);
md.push_str(&format!("{}- **{}**", indent, elem.name));
// Attributs
if !elem.attributes.is_empty() {
let attrs: Vec<String> = elem
.attributes
.iter()
.map(|(k, v)| format!("{}={}", k, format_value(v)))
.collect();
md.push_str(&format!(" ({})", attrs.join(", ")));
}
// Texte (utilise get_text() maintenant)
if let Some(text) = elem.get_text().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) {
md.push_str(&format!(": {}", format_value(&text)));
}
md.push('\n');
// Enfants
for child in &elem.children {
if let xmltree::XMLNode::Element(child_elem) = child {
recurse(child_elem, md, depth + 1);
}
}
}
md.push_str("# UPnP XML (Markdown view)\n\n");
recurse(&elem, &mut md, 0);
md
}
}
pub trait UpnpObject : UpnpXml {
pub trait UpnpObject: UpnpXml {
fn as_upnp_object_type(&self) -> &UpnpObjectType;
fn get_name(&self) -> &String {
@@ -35,6 +99,4 @@ pub trait UpnpObject : UpnpXml {
fn get_object_type(&self) -> &String {
&self.as_upnp_object_type().object_type
}
}

View File

@@ -25,7 +25,7 @@ use rust_embed::RustEmbed;
use serde::Serialize;
use std::{net::SocketAddr, sync::Arc};
use tokio::{signal, sync::RwLock, task::JoinHandle};
use tracing::{debug, error, info, warn};
use tracing::{info, warn, debug, error};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
@@ -150,16 +150,17 @@ impl Server {
///
/// # Exemple
///
/// ```rust,no_run
/// # use pmoupnp::server::Server;
/// # use rust_embed::RustEmbed;
/// ```ignore
/// use pmoupnp::server::Server;
/// use rust_embed::RustEmbed;
///
/// #[derive(RustEmbed, Clone)]
/// #[folder = "static/"]
/// struct Assets;
///
/// # #[tokio::main]
/// # async fn main() {
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
/// let mut server = Server::new("Test", "http://localhost:3000", 3000);
/// server.add_dir::<Assets>("/assets").await;
/// // Les fichiers de static/ sont accessibles via /assets/*
/// # }
@@ -169,9 +170,9 @@ impl Server {
E: RustEmbed + Clone + Send + Sync + 'static,
{
let serve = ServeEmbed::<E>::new();
let mut r = self.router.write().await;
if path == "/" {
*r = std::mem::take(&mut *r).fallback_service(serve);
} else {
@@ -229,9 +230,9 @@ impl Server {
axum_embed::FallbackBehavior::Ok,
Some("index.html".to_string()),
);
let mut r = self.router.write().await;
if path == "/" {
*r = std::mem::take(&mut *r).fallback_service(serve);
} else {
@@ -288,17 +289,23 @@ impl Server {
///
/// # Exemple avec SSE
///
/// ```rust,no_run
/// # use pmoupnp::server::Server;
/// # use axum::extract::State;
/// # use axum::response::sse::{Event, Sse, KeepAlive};
/// # use tokio::sync::broadcast;
/// # #[derive(Clone)]
/// # struct LogState { tx: broadcast::Sender<String> }
/// # impl LogState { fn subscribe(&self) -> broadcast::Receiver<String> { self.tx.subscribe() } }
/// # #[tokio::main]
/// # async fn main() {
/// # let mut server = Server::new("Test", "http://localhost:3000", 3000);
/// ```ignore
/// use pmoupnp::server::Server;
/// use axum::extract::State;
/// use axum::response::sse::{Event, Sse, KeepAlive};
/// use tokio::sync::broadcast;
///
/// #[derive(Clone)]
/// struct LogState {
/// tx: broadcast::Sender<String>
/// }
///
/// impl LogState {
/// fn subscribe(&self) -> broadcast::Receiver<String> {
/// self.tx.subscribe()
/// }
/// }
///
/// async fn log_sse(State(state): State<LogState>) -> Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>> {
/// let mut rx = state.subscribe();
/// let stream = async_stream::stream! {
@@ -311,7 +318,6 @@ impl Server {
///
/// let log_state = LogState { tx: broadcast::channel(100).0 };
/// server.add_handler_with_state("/logs", log_sse, log_state).await;
/// # }
/// ```
pub async fn add_handler_with_state<H, T, S>(&mut self, path: &str, handler: H, state: S)
where
@@ -319,7 +325,9 @@ impl Server {
T: 'static,
S: Clone + Send + Sync + 'static,
{
let route = Router::new().route("/", get(handler)).with_state(state);
let route = Router::new()
.route("/", get(handler))
.with_state(state);
let mut r = self.router.write().await;
*r = std::mem::take(&mut *r).nest(path, route);
@@ -398,9 +406,16 @@ impl Server {
///
/// # Exemple
///
/// ```rust,no_run
/// ```ignore
/// use utoipa::OpenApi;
/// use axum::{Router, Json, routing::get};
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize, utoipa::ToSchema)]
/// struct User {
/// id: u64,
/// name: String,
/// }
///
/// #[derive(utoipa::OpenApi)]
/// #[openapi(
@@ -429,7 +444,8 @@ impl Server {
*api_r = Some(api_router);
// Ajouter Swagger UI
let swagger = SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", openapi);
let swagger = SwaggerUi::new("/swagger-ui")
.url("/api-docs/openapi.json", openapi);
let mut r = self.router.write().await;
*r = std::mem::take(&mut *r).merge(swagger);
@@ -453,10 +469,7 @@ impl Server {
/// ```
pub async fn start(&mut self) {
let addr = SocketAddr::from(([0, 0, 0, 0], self.http_port));
info!(
"Server {} running at [http://{}:{}](http://{}:{})",
self.name, self.base_url, self.http_port, self.base_url, self.http_port
);
info!("Server {} running at [http://{}:{}](http://{}:{})", self.name, self.base_url, self.http_port, self.base_url, self.http_port);
// Merger le routeur API si présent
let api_router = self.api_router.read().await;
@@ -532,7 +545,7 @@ impl ServerBuilder {
Self {
name: "PMO-Music-Server".to_string(),
base_url: config.get_base_url(),
http_port: config.get_http_port(),
http_port: config.get_http_port()
}
}
@@ -550,4 +563,4 @@ impl ServerBuilder {
pub fn build(self) -> Server {
Server::new(self.name, self.base_url, self.http_port)
}
}
}

View File

@@ -10,6 +10,7 @@ use std::{
};
pub use crate::state_variables::variable_trait::UpnpVariable;
use bevy_reflect::Reflect;
use chrono::{DateTime, Utc};
pub use errors::StateVariableError;
@@ -24,7 +25,7 @@ pub type StateConditionFunc = Arc<dyn Fn(&StateVarInstance) -> bool + Send + Syn
/// Type pour les fonctions de parsing de valeurs depuis des chaînes
pub type StringValueParser =
Arc<dyn Fn(&str) -> Result<StateValue, StateVariableError> + Send + Sync>;
Arc<dyn Fn(&str) -> Result<Box<dyn Reflect>, StateVariableError> + Send + Sync>;
/// Type pour les fonctions de sérialisation de valeurs vers des chaînes
pub type ValueSerializer =

View File

@@ -295,7 +295,7 @@ impl StateVariable {
self.description = description;
}
pub fn set_default(&mut self, value: StateValue) -> Result<(), StateValueError> {
pub fn set_default(&mut self, value: &StateValue) -> Result<(), StateValueError> {
if self.as_state_var_type() != value.as_state_var_type() {
return Err(StateValueError::TypeError(
"value does not have the right type".to_string(),
@@ -353,7 +353,7 @@ impl StateVariable {
self.send_events = false;
}
pub fn set_value_parseur(&mut self, parser: StringValueParser) -> Result<(), StateValueError> {
pub fn set_value_parser(&mut self, parser: StringValueParser) -> Result<(), StateValueError> {
if self.as_state_var_type() == StateVarType::String {
self.parse = Some(parser);
return Ok(());

View File

@@ -30,6 +30,8 @@ mod values_from_uri;
mod values_from_uuid;
mod values_from_vec_u8;
mod values_from_str;
use std::fmt::Debug;
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime};

View File

@@ -0,0 +1,21 @@
use std::convert::TryFrom;
use url::Url;
use crate::variable_types::{StateValue, StateValueError};
impl TryFrom<&str> for StateValue {
type Error = StateValueError;
fn try_from(s: &str) -> Result<Self, StateValueError> {
Ok(StateValue::String(s.to_string()))
}
}
// Conversion depuis String
impl TryFrom<String> for StateValue {
type Error = StateValueError;
fn try_from(s: String) -> Result<Self, StateValueError> {
Ok(StateValue::String(s))
}
}

View File

@@ -400,6 +400,7 @@ button.active {
.message {
color: #d4d4d4;
word-break: break-word;
text-align: left;
}
@media (max-width: 768px) {