feat: implémentation du WebRenderer UPnP privé par navigateur
Ajout de la fonctionnalité WebRenderer permettant à chaque navigateur connecté de devenir un MediaRenderer UPnP privé. - Création du crate pmowebrenderer avec l'architecture complète - Implémentation des handlers SOAP → WebSocket pour les services AVTransport, RenderingControl et ConnectionManager - Intégration avec le ControlPoint pour l'enregistrement dynamique des renderers - Gestion des sessions avec timeout et cleanup automatique - Support des commandes de transport (play, pause, stop, seek) et du contrôle du volume - Mise à jour des dépendances dans Cargo.toml et Cargo.lock - Documentation de l'architecture dans Blackboard/Architecture/webrenderer.md
This commit is contained in:
@@ -22,7 +22,7 @@ base64 = "0.22.1"
|
||||
thiserror = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
xmltree = "0.11.0"
|
||||
axum = "0.8.4"
|
||||
axum = { version = "0.8.4", features = ["ws"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -57,7 +57,6 @@ pub enum SsdpEvent {
|
||||
#[derive(Clone)]
|
||||
pub struct SsdpClient {
|
||||
socket: Arc<UdpSocket>,
|
||||
loopback_socket: Arc<UdpSocket>,
|
||||
}
|
||||
|
||||
impl SsdpClient {
|
||||
@@ -76,63 +75,32 @@ impl SsdpClient {
|
||||
let bind_addr: SocketAddr = "0.0.0.0:0".parse().unwrap();
|
||||
socket2.bind(&bind_addr.into())?;
|
||||
|
||||
// Le client SSDP n'a pas besoin de join_multicast_v4 :
|
||||
// il envoie des M-SEARCH et reçoit des réponses unicast.
|
||||
// Le join est seulement utile pour recevoir les NOTIFY multicast,
|
||||
// ce dont le SsdpServer s'occupe.
|
||||
//
|
||||
// On configure uniquement l'interface de sortie multicast sur
|
||||
// l'IP principale pour que send_to vers 239.255.255.250 fonctionne.
|
||||
// Sur macOS, join_multicast_v4 sur des interfaces bridge/VM écrase
|
||||
// IP_MULTICAST_IF et provoque EHOSTUNREACH (errno 65).
|
||||
let local_ip: std::net::Ipv4Addr = pmoutils::guess_local_ip()
|
||||
.parse()
|
||||
.unwrap_or("127.0.0.1".parse().unwrap());
|
||||
socket2.set_multicast_if_v4(&local_ip)?;
|
||||
debug!(
|
||||
"SSDP client: multicast outgoing interface set to {}",
|
||||
local_ip
|
||||
);
|
||||
|
||||
let socket: UdpSocket = socket2.into();
|
||||
socket.set_read_timeout(Some(Duration::from_secs(1)))?;
|
||||
socket.set_multicast_loop_v4(true)?; // utile en dev local
|
||||
|
||||
for iface in get_if_addrs::get_if_addrs()? {
|
||||
if let std::net::IpAddr::V4(ipv4) = iface.ip() {
|
||||
// Join multicast on ALL interfaces, including loopback for local development
|
||||
match socket.join_multicast_v4(&SSDP_MULTICAST_ADDR.parse().unwrap(), &ipv4) {
|
||||
Ok(()) => {
|
||||
if ipv4.is_loopback() {
|
||||
debug!(
|
||||
"SSDP: joined {} on {} (localhost - dev mode)",
|
||||
SSDP_MULTICAST_ADDR, ipv4
|
||||
);
|
||||
} else {
|
||||
debug!("SSDP: joined {} on {}", SSDP_MULTICAST_ADDR, ipv4);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"SSDP: failed to join {} on {}: {}",
|
||||
SSDP_MULTICAST_ADDR, ipv4, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a second socket for loopback multicast (when network blocks multicast)
|
||||
let loopback_socket2 = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
|
||||
loopback_socket2.set_reuse_address(true)?;
|
||||
|
||||
// Bind on any address with ephemeral port (like the main socket)
|
||||
let loopback_bind: SocketAddr = "0.0.0.0:0".parse().unwrap();
|
||||
loopback_socket2.bind(&loopback_bind.into())?;
|
||||
|
||||
let loopback_socket: UdpSocket = loopback_socket2.into();
|
||||
loopback_socket.set_multicast_loop_v4(true)?;
|
||||
|
||||
// Join multicast specifically on loopback interface (127.0.0.1)
|
||||
let loopback_addr: std::net::Ipv4Addr = "127.0.0.1".parse().unwrap();
|
||||
if let Err(e) =
|
||||
loopback_socket.join_multicast_v4(&SSDP_MULTICAST_ADDR.parse().unwrap(), &loopback_addr)
|
||||
{
|
||||
warn!("Failed to join multicast on loopback socket: {}", e);
|
||||
} else {
|
||||
debug!(
|
||||
"SSDP loopback socket: joined {} on 127.0.0.1",
|
||||
SSDP_MULTICAST_ADDR
|
||||
);
|
||||
}
|
||||
|
||||
info!("✅ SSDP client ready on {}", addr);
|
||||
|
||||
Ok(Self {
|
||||
socket: Arc::new(socket),
|
||||
loopback_socket: Arc::new(loopback_socket),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -150,50 +118,19 @@ impl SsdpClient {
|
||||
SSDP_MULTICAST_ADDR, SSDP_PORT, mx, st
|
||||
);
|
||||
|
||||
let multicast_addr: SocketAddr = format!("{}:{}", SSDP_MULTICAST_ADDR, SSDP_PORT)
|
||||
let addr: SocketAddr = format!("{}:{}", SSDP_MULTICAST_ADDR, SSDP_PORT)
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
// Try multicast first
|
||||
match self.socket.send_to(msg.as_bytes(), multicast_addr) {
|
||||
match self.socket.send_to(msg.as_bytes(), addr) {
|
||||
Ok(_) => {
|
||||
info!("📤 M-SEARCH sent to multicast (ST={}, MX={})", st, mx);
|
||||
info!("📤 M-SEARCH sent (ST={}, MX={})", st, mx);
|
||||
debug!(
|
||||
"📨 M-SEARCH payload\n<details>\n\n```\n{}\n```\n</details>\n",
|
||||
msg
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) if e.raw_os_error() == Some(65) || e.raw_os_error() == Some(101) => {
|
||||
// Error 65 (EHOSTUNREACH) on macOS, 101 (ENETUNREACH) on Linux
|
||||
// Network blocks multicast (e.g., eduroam) → send to localhost unicast
|
||||
warn!(
|
||||
"⚠️ Multicast blocked on network ({}), sending to localhost for local devices",
|
||||
e
|
||||
);
|
||||
|
||||
// Send to localhost unicast (not multicast) - servers on 127.0.0.1:1900 will receive
|
||||
let localhost_addr: SocketAddr =
|
||||
format!("127.0.0.1:{}", SSDP_PORT).parse().unwrap();
|
||||
|
||||
match self.loopback_socket.send_to(msg.as_bytes(), localhost_addr) {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"📤 M-SEARCH sent to localhost unicast (ST={}, MX={})",
|
||||
st, mx
|
||||
);
|
||||
debug!(
|
||||
"📨 M-SEARCH localhost payload\n<details>\n\n```\n{}\n```\n</details>\n",
|
||||
msg
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(loopback_err) => {
|
||||
warn!("❌ Failed to send M-SEARCH to localhost: {}", loopback_err);
|
||||
Err(loopback_err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("❌ Failed to send M-SEARCH: {}", e);
|
||||
Err(e)
|
||||
|
||||
@@ -15,9 +15,6 @@ pub struct SsdpServer {
|
||||
|
||||
/// Socket UDP pour SSDP
|
||||
socket: Option<Arc<UdpSocket>>,
|
||||
|
||||
/// Socket dédié pour loopback (quand le réseau bloque le multicast)
|
||||
loopback_socket: Option<Arc<UdpSocket>>,
|
||||
}
|
||||
|
||||
impl SsdpServer {
|
||||
@@ -26,7 +23,6 @@ impl SsdpServer {
|
||||
Self {
|
||||
devices: Arc::new(RwLock::new(HashMap::new())),
|
||||
socket: None,
|
||||
loopback_socket: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,93 +74,38 @@ impl SsdpServer {
|
||||
let bind_addr: SocketAddr = format!("0.0.0.0:{}", SSDP_PORT).parse().unwrap();
|
||||
socket2.bind(&bind_addr.into())?;
|
||||
|
||||
// Configurer l'interface de sortie multicast sur l'IP principale.
|
||||
// Sur macOS, sans cela le kernel peut router les paquets multicast
|
||||
// via une interface bridge/VM qui n'a pas de route, causant
|
||||
// EHOSTUNREACH (errno 65).
|
||||
let local_ip: std::net::Ipv4Addr = pmoutils::guess_local_ip()
|
||||
.parse()
|
||||
.unwrap_or("127.0.0.1".parse().unwrap());
|
||||
socket2.set_multicast_if_v4(&local_ip)?;
|
||||
debug!(
|
||||
"SSDP server: multicast outgoing interface set to {}",
|
||||
local_ip
|
||||
);
|
||||
|
||||
// Convertir en UdpSocket standard
|
||||
let socket: UdpSocket = socket2.into();
|
||||
|
||||
// Rejoindre le groupe multicast sur toutes les interfaces (y compris loopback)
|
||||
// Ceci est essentiel pour le développement local avec plusieurs instances
|
||||
for iface in get_if_addrs::get_if_addrs()? {
|
||||
if let std::net::IpAddr::V4(ipv4) = iface.ip() {
|
||||
match socket.join_multicast_v4(&SSDP_MULTICAST_ADDR.parse().unwrap(), &ipv4) {
|
||||
Ok(()) => {
|
||||
if ipv4.is_loopback() {
|
||||
debug!(
|
||||
"SSDP server: joined {} on {} (localhost - dev mode)",
|
||||
SSDP_MULTICAST_ADDR, ipv4
|
||||
);
|
||||
} else {
|
||||
debug!("SSDP server: joined {} on {}", SSDP_MULTICAST_ADDR, ipv4);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"SSDP server: failed to join {} on {}: {}",
|
||||
SSDP_MULTICAST_ADDR, ipv4, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rejoindre le groupe multicast via INADDR_ANY (l'OS choisit l'interface)
|
||||
socket.join_multicast_v4(
|
||||
&SSDP_MULTICAST_ADDR.parse().unwrap(),
|
||||
&"0.0.0.0".parse().unwrap(),
|
||||
)?;
|
||||
|
||||
socket.set_read_timeout(Some(Duration::from_secs(1)))?;
|
||||
socket.set_multicast_loop_v4(true)?; // Important pour dev local
|
||||
socket.set_multicast_loop_v4(true)?; // utile en dev local
|
||||
|
||||
let socket = Arc::new(socket);
|
||||
self.socket = Some(socket.clone());
|
||||
|
||||
// Create a second socket for loopback multicast (when network blocks multicast)
|
||||
let loopback_socket2 = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
|
||||
loopback_socket2.set_reuse_address(true)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::io::AsRawFd;
|
||||
let fd = loopback_socket2.as_raw_fd();
|
||||
let optval: libc::c_int = 1;
|
||||
unsafe {
|
||||
let result = libc::setsockopt(
|
||||
fd,
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_REUSEPORT,
|
||||
&optval as *const _ as *const libc::c_void,
|
||||
std::mem::size_of_val(&optval) as libc::socklen_t,
|
||||
);
|
||||
if result != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bind on any address with ephemeral port (not on port 1900 to avoid conflicts)
|
||||
let loopback_bind: SocketAddr = "0.0.0.0:0".parse().unwrap();
|
||||
loopback_socket2.bind(&loopback_bind.into())?;
|
||||
|
||||
let loopback_socket: UdpSocket = loopback_socket2.into();
|
||||
loopback_socket.set_multicast_loop_v4(true)?;
|
||||
|
||||
// Join multicast specifically on loopback interface (127.0.0.1)
|
||||
let loopback_addr: std::net::Ipv4Addr = "127.0.0.1".parse().unwrap();
|
||||
if let Err(e) =
|
||||
loopback_socket.join_multicast_v4(&SSDP_MULTICAST_ADDR.parse().unwrap(), &loopback_addr)
|
||||
{
|
||||
warn!(
|
||||
"SSDP server: failed to join multicast on loopback socket: {}",
|
||||
e
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"SSDP loopback server socket: joined {} on 127.0.0.1",
|
||||
SSDP_MULTICAST_ADDR
|
||||
);
|
||||
}
|
||||
|
||||
let loopback_socket = Arc::new(loopback_socket);
|
||||
self.loopback_socket = Some(loopback_socket.clone());
|
||||
|
||||
info!("✅ SSDP server started on {}", addr);
|
||||
|
||||
// Lancer les goroutines d'annonces périodiques et d'écoute M-SEARCH
|
||||
self.start_periodic_announcements(socket.clone(), loopback_socket.clone());
|
||||
self.start_periodic_announcements(socket.clone());
|
||||
self.start_msearch_listener(socket.clone());
|
||||
|
||||
Ok(())
|
||||
@@ -190,10 +131,9 @@ impl SsdpServer {
|
||||
|
||||
// Envoyer alive pour tous les NTs
|
||||
if let Some(ref socket) = self.socket {
|
||||
let loopback_socket = self.loopback_socket.as_ref();
|
||||
let nts = device.get_notification_types();
|
||||
for nt in nts.iter() {
|
||||
Self::send_alive(socket, loopback_socket, &device, nt, false);
|
||||
Self::send_alive(socket, &device, nt, false);
|
||||
// Petit délai pour éviter de saturer le buffer UDP sur macOS
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
@@ -222,13 +162,7 @@ impl SsdpServer {
|
||||
}
|
||||
|
||||
/// Envoie un NOTIFY alive
|
||||
fn send_alive(
|
||||
socket: &UdpSocket,
|
||||
loopback_socket: Option<&Arc<UdpSocket>>,
|
||||
device: &SsdpDevice,
|
||||
nt: &str,
|
||||
is_periodic: bool,
|
||||
) {
|
||||
fn send_alive(socket: &UdpSocket, device: &SsdpDevice, nt: &str, is_periodic: bool) {
|
||||
let usn = if nt.starts_with("uuid:") {
|
||||
format!("{}", nt)
|
||||
} else {
|
||||
@@ -248,11 +182,11 @@ impl SsdpServer {
|
||||
SSDP_MULTICAST_ADDR, SSDP_PORT, MAX_AGE, device.location, nt, device.server, usn
|
||||
);
|
||||
|
||||
let multicast_addr: SocketAddr = format!("{}:{}", SSDP_MULTICAST_ADDR, SSDP_PORT)
|
||||
let addr: SocketAddr = format!("{}:{}", SSDP_MULTICAST_ADDR, SSDP_PORT)
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
match socket.send_to(msg.as_bytes(), multicast_addr) {
|
||||
match socket.send_to(msg.as_bytes(), addr) {
|
||||
Ok(_) => {
|
||||
let label = if is_periodic { " (periodic)" } else { "" };
|
||||
info!("✅ NOTIFY alive{}: {} (NT={})", label, usn, nt);
|
||||
@@ -261,34 +195,7 @@ impl SsdpServer {
|
||||
label, msg
|
||||
);
|
||||
}
|
||||
Err(e) if e.raw_os_error() == Some(65) || e.raw_os_error() == Some(101) => {
|
||||
// Multicast bloqué sur le réseau, envoyer en unicast sur localhost
|
||||
if let Some(loopback_sock) = loopback_socket {
|
||||
// Send to localhost unicast - local clients will receive
|
||||
let localhost_addr: SocketAddr =
|
||||
format!("127.0.0.1:{}", SSDP_PORT).parse().unwrap();
|
||||
|
||||
match loopback_sock.send_to(msg.as_bytes(), localhost_addr) {
|
||||
Ok(_) => {
|
||||
let label = if is_periodic { " (periodic)" } else { "" };
|
||||
info!("✅ NOTIFY alive{} to localhost: {} (NT={})", label, usn, nt);
|
||||
}
|
||||
Err(loopback_err) => {
|
||||
let label = if is_periodic { "periodic " } else { "" };
|
||||
warn!(
|
||||
"❌ Failed to send {}NOTIFY alive to localhost for {}: {}",
|
||||
label, usn, loopback_err
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let label = if is_periodic { "periodic " } else { "" };
|
||||
warn!(
|
||||
"❌ Failed to send {}NOTIFY alive for {} (no loopback socket available): {}",
|
||||
label, usn, e
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let label = if is_periodic { "periodic " } else { "" };
|
||||
warn!("❌ Failed to send {}NOTIFY alive for {}: {}", label, usn, e);
|
||||
@@ -331,11 +238,7 @@ impl SsdpServer {
|
||||
}
|
||||
|
||||
/// Démarre les annonces périodiques (toutes les MAX_AGE/2 secondes)
|
||||
fn start_periodic_announcements(
|
||||
&self,
|
||||
socket: Arc<UdpSocket>,
|
||||
loopback_socket: Arc<UdpSocket>,
|
||||
) {
|
||||
fn start_periodic_announcements(&self, socket: Arc<UdpSocket>) {
|
||||
let devices = Arc::clone(&self.devices);
|
||||
let period = Duration::from_secs((MAX_AGE / 2) as u64);
|
||||
|
||||
@@ -351,7 +254,7 @@ impl SsdpServer {
|
||||
};
|
||||
for device in &devices_snapshot {
|
||||
for nt in device.get_notification_types() {
|
||||
Self::send_alive(&socket, Some(&loopback_socket), device, nt, true);
|
||||
Self::send_alive(&socket, device, nt, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user