⬆️ version bump to v0.3.35 & refactor MediaRenderer stream URL handling
- Bump version to v0.3.35 in Cargo.toml, lockfile and `version.txt` - Refactor MediaRenderer to accept dynamic stream URL base instead of hardcoded `/api/webrenderer` path - Remove `PlayerStateReport`, moving it to pmoweb/renderer as local type for better encapsulation - Update `register_or_reconnect` and device creation to accept renderer name, stream URL base via closure - Replace `update_player_state` and `get_pending_command`, delegating state/command access directly via instance lookup - Simplify device creation by using `Device::new_from_config` and removing manual model_name setting
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -4,7 +4,7 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "PMOMusic"
|
name = "PMOMusic"
|
||||||
version = "0.3.33"
|
version = "0.3.35"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum 0.8.7",
|
"axum 0.8.7",
|
||||||
"console-subscriber",
|
"console-subscriber",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "PMOMusic"
|
name = "PMOMusic"
|
||||||
version = "0.3.33"
|
version = "0.3.35"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ pub fn play_handler(
|
|||||||
pipeline: PipelineHandle,
|
pipeline: PipelineHandle,
|
||||||
state: SharedState,
|
state: SharedState,
|
||||||
instance_id: String,
|
instance_id: String,
|
||||||
|
stream_url_base: String,
|
||||||
) -> ActionHandler {
|
) -> ActionHandler {
|
||||||
action_handler!(
|
action_handler!(
|
||||||
captures(pipeline, state, instance_id) | data | {
|
captures(pipeline, state, instance_id, stream_url_base) | data | {
|
||||||
tracing::info!("[MediaRenderer] UPnP Play action invoked");
|
tracing::info!("[MediaRenderer] UPnP Play action invoked");
|
||||||
let has_uri = state.read().current_uri.is_some();
|
let has_uri = state.read().current_uri.is_some();
|
||||||
if !has_uri {
|
if !has_uri {
|
||||||
@@ -31,7 +32,7 @@ pub fn play_handler(
|
|||||||
let mut s = state.write();
|
let mut s = state.write();
|
||||||
s.playback_state = PlaybackState::Transitioning;
|
s.playback_state = PlaybackState::Transitioning;
|
||||||
s.push_command(crate::adapter::DeviceCommand::Stream {
|
s.push_command(crate::adapter::DeviceCommand::Stream {
|
||||||
url: format!("/api/webrenderer/{}/stream", instance_id),
|
url: format!("{}/{}/stream", stream_url_base, instance_id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
pipeline.flac_handle.resume();
|
pipeline.flac_handle.resume();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ pub mod state;
|
|||||||
|
|
||||||
pub use error::MediaRendererError;
|
pub use error::MediaRendererError;
|
||||||
pub use handlers::*;
|
pub use handlers::*;
|
||||||
pub use messages::{PlaybackState, PlayerStateReport};
|
pub use messages::PlaybackState;
|
||||||
pub use pipeline::{PipelineControl, PipelineHandle, seconds_to_upnp_time, upnp_time_to_seconds, InstancePipeline};
|
pub use pipeline::{PipelineControl, PipelineHandle, seconds_to_upnp_time, upnp_time_to_seconds, InstancePipeline};
|
||||||
pub use registry::{MediaRendererInstance, MediaRendererRegistry};
|
pub use registry::{MediaRendererInstance, MediaRendererRegistry};
|
||||||
pub use state::{RendererState, SharedState};
|
pub use state::{RendererState, SharedState};
|
||||||
|
|||||||
@@ -11,11 +11,3 @@ pub enum PlaybackState {
|
|||||||
Transitioning,
|
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>,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -63,8 +63,9 @@ impl MediaRendererRegistry {
|
|||||||
pub async fn register_or_reconnect(
|
pub async fn register_or_reconnect(
|
||||||
&self,
|
&self,
|
||||||
instance_id: &str,
|
instance_id: &str,
|
||||||
user_agent: &str,
|
stream_url_base: &str,
|
||||||
adapter: Arc<dyn DeviceAdapter>,
|
renderer_name: &str,
|
||||||
|
adapter_fn: impl FnOnce(SharedState) -> Arc<dyn DeviceAdapter>,
|
||||||
) -> Result<(String, String, bool), MediaRendererError> {
|
) -> Result<(String, String, bool), MediaRendererError> {
|
||||||
if let Some(cancel) = self.pending_unregister.write().remove(instance_id) {
|
if let Some(cancel) = self.pending_unregister.write().remove(instance_id) {
|
||||||
tracing::info!(instance_id = %instance_id, "MediaRenderer: cancelled pending unregister (page reload)");
|
tracing::info!(instance_id = %instance_id, "MediaRenderer: cancelled pending unregister (page reload)");
|
||||||
@@ -76,8 +77,8 @@ impl MediaRendererRegistry {
|
|||||||
if let Some(existing) = instances.get(instance_id) {
|
if let Some(existing) = instances.get(instance_id) {
|
||||||
tracing::info!(instance_id = %instance_id, "MediaRenderer: reconnecting existing instance");
|
tracing::info!(instance_id = %instance_id, "MediaRenderer: reconnecting existing instance");
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
self.register_with_control_point(&existing.device_instance)?;
|
self.register_with_control_point(&existing.device_instance, renderer_name)?;
|
||||||
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
|
let stream_url = format!("{}/{}/stream", stream_url_base, instance_id);
|
||||||
let should_play = {
|
let should_play = {
|
||||||
let s = existing.state.read();
|
let s = existing.state.read();
|
||||||
s.current_uri.is_some() && matches!(
|
s.current_uri.is_some() && matches!(
|
||||||
@@ -89,9 +90,9 @@ impl MediaRendererRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let instance = self.create_instance_with_adapter(instance_id, user_agent, adapter).await?;
|
let instance = self.create_instance_with_adapter(instance_id, stream_url_base, renderer_name, adapter_fn).await?;
|
||||||
let instance = Arc::new(instance);
|
let instance = Arc::new(instance);
|
||||||
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
|
let stream_url = format!("{}/{}/stream", stream_url_base, instance_id);
|
||||||
let udn = instance.udn.clone();
|
let udn = instance.udn.clone();
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -223,47 +224,14 @@ impl MediaRendererRegistry {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_player_state(
|
/// Créer une nouvelle instance avec un adapter fourni via une factory closure.
|
||||||
&self,
|
/// La closure reçoit le SharedState de l'instance afin que l'adapter partage le même état.
|
||||||
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(
|
pub async fn create_instance_with_adapter(
|
||||||
&self,
|
&self,
|
||||||
instance_id: &str,
|
instance_id: &str,
|
||||||
user_agent: &str,
|
stream_url_base: &str,
|
||||||
adapter: Arc<dyn DeviceAdapter>,
|
renderer_name: &str,
|
||||||
|
adapter_fn: impl FnOnce(SharedState) -> Arc<dyn DeviceAdapter>,
|
||||||
) -> Result<MediaRendererInstance, MediaRendererError> {
|
) -> Result<MediaRendererInstance, MediaRendererError> {
|
||||||
let candidate_udn = instance_id.to_ascii_lowercase();
|
let candidate_udn = instance_id.to_ascii_lowercase();
|
||||||
let full_udn = format!("uuid:{}", candidate_udn);
|
let full_udn = format!("uuid:{}", candidate_udn);
|
||||||
@@ -277,6 +245,7 @@ impl MediaRendererRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let state: SharedState = Arc::new(parking_lot::RwLock::new(RendererState::default()));
|
let state: SharedState = Arc::new(parking_lot::RwLock::new(RendererState::default()));
|
||||||
|
let adapter = adapter_fn(state.clone());
|
||||||
|
|
||||||
#[cfg(feature = "pmoserver")]
|
#[cfg(feature = "pmoserver")]
|
||||||
let (device_instance, pipeline) = {
|
let (device_instance, pipeline) = {
|
||||||
@@ -306,9 +275,10 @@ impl MediaRendererRegistry {
|
|||||||
let device = MediaRendererFactory::create_device_with_pipeline(
|
let device = MediaRendererFactory::create_device_with_pipeline(
|
||||||
instance_id,
|
instance_id,
|
||||||
"MediaRenderer",
|
"MediaRenderer",
|
||||||
user_agent,
|
"WebRenderer",
|
||||||
pipeline.clone(),
|
pipeline.clone(),
|
||||||
state.clone(),
|
state.clone(),
|
||||||
|
stream_url_base,
|
||||||
)
|
)
|
||||||
.map_err(|e| MediaRendererError::DeviceCreationError(e.to_string()))?;
|
.map_err(|e| MediaRendererError::DeviceCreationError(e.to_string()))?;
|
||||||
|
|
||||||
@@ -319,7 +289,7 @@ impl MediaRendererRegistry {
|
|||||||
.map_err(|e| MediaRendererError::RegistrationError(e.to_string()))?
|
.map_err(|e| MediaRendererError::RegistrationError(e.to_string()))?
|
||||||
};
|
};
|
||||||
|
|
||||||
self.register_with_control_point(&di)?;
|
self.register_with_control_point(&di, renderer_name)?;
|
||||||
(di, ip)
|
(di, ip)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -337,9 +307,10 @@ impl MediaRendererRegistry {
|
|||||||
let device = MediaRendererFactory::create_device_with_pipeline(
|
let device = MediaRendererFactory::create_device_with_pipeline(
|
||||||
instance_id,
|
instance_id,
|
||||||
"MediaRenderer",
|
"MediaRenderer",
|
||||||
user_agent,
|
"WebRenderer",
|
||||||
pipeline.clone(),
|
pipeline.clone(),
|
||||||
state.clone(),
|
state.clone(),
|
||||||
|
stream_url_base,
|
||||||
)
|
)
|
||||||
.map_err(|e| MediaRendererError::DeviceCreationError(e.to_string()))?;
|
.map_err(|e| MediaRendererError::DeviceCreationError(e.to_string()))?;
|
||||||
|
|
||||||
@@ -362,6 +333,7 @@ impl MediaRendererRegistry {
|
|||||||
fn register_with_control_point(
|
fn register_with_control_point(
|
||||||
&self,
|
&self,
|
||||||
di: &Arc<DeviceInstance>,
|
di: &Arc<DeviceInstance>,
|
||||||
|
renderer_name: &str,
|
||||||
) -> Result<(), MediaRendererError> {
|
) -> Result<(), MediaRendererError> {
|
||||||
let base_url = di.base_url().to_string();
|
let base_url = di.base_url().to_string();
|
||||||
let udn = di.udn().to_ascii_lowercase();
|
let udn = di.udn().to_ascii_lowercase();
|
||||||
@@ -397,7 +369,7 @@ impl MediaRendererRegistry {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
format!("{}{}", base_url, di.description_route()),
|
format!("{}{}", base_url, di.description_route()),
|
||||||
"PMOMusic WebRenderer/2.0".to_string(),
|
renderer_name.to_string(),
|
||||||
Some("urn:schemas-upnp-org:service:AVTransport:1".to_string()),
|
Some("urn:schemas-upnp-org:service:AVTransport:1".to_string()),
|
||||||
avtransport_control_url,
|
avtransport_control_url,
|
||||||
Some("urn:schemas-upnp-org:service:RenderingControl:1".to_string()),
|
Some("urn:schemas-upnp-org:service:RenderingControl:1".to_string()),
|
||||||
|
|||||||
@@ -87,20 +87,20 @@ impl MediaRendererFactory {
|
|||||||
pub fn create_device_with_pipeline(
|
pub fn create_device_with_pipeline(
|
||||||
device_name: &str,
|
device_name: &str,
|
||||||
device_type: &str,
|
device_type: &str,
|
||||||
device_ua: &str,
|
friendly_name_suffix: &str,
|
||||||
pipeline: PipelineHandle,
|
pipeline: PipelineHandle,
|
||||||
state: SharedState,
|
state: SharedState,
|
||||||
|
stream_url_base: &str,
|
||||||
) -> Result<Device, FactoryError> {
|
) -> Result<Device, FactoryError> {
|
||||||
let avtransport = Self::build_avtransport(pipeline.clone(), state.clone(), device_name)?;
|
let avtransport = Self::build_avtransport(pipeline.clone(), state.clone(), device_name, stream_url_base)?;
|
||||||
let renderingcontrol = Self::build_renderingcontrol(state.clone())?;
|
let renderingcontrol = Self::build_renderingcontrol(state.clone())?;
|
||||||
let connectionmanager = Self::build_connectionmanager()?;
|
let connectionmanager = Self::build_connectionmanager()?;
|
||||||
|
|
||||||
let mut device = Device::new(
|
let device = Device::new_from_config(
|
||||||
device_name.to_string(),
|
device_name.to_string(),
|
||||||
device_type.to_string(),
|
device_type.to_string(),
|
||||||
device_ua.to_string(),
|
friendly_name_suffix.to_string(),
|
||||||
);
|
);
|
||||||
device.set_model_name("MediaRenderer".to_string());
|
|
||||||
device
|
device
|
||||||
.add_service(Arc::new(avtransport))
|
.add_service(Arc::new(avtransport))
|
||||||
.map_err(|e| FactoryError::ServiceError(format!("{:?}", e)))?;
|
.map_err(|e| FactoryError::ServiceError(format!("{:?}", e)))?;
|
||||||
@@ -118,6 +118,7 @@ impl MediaRendererFactory {
|
|||||||
pipeline: PipelineHandle,
|
pipeline: PipelineHandle,
|
||||||
state: SharedState,
|
state: SharedState,
|
||||||
instance_id: &str,
|
instance_id: &str,
|
||||||
|
stream_url_base: &str,
|
||||||
) -> Result<Service, FactoryError> {
|
) -> Result<Service, FactoryError> {
|
||||||
let mut svc = Service::new("AVTransport".to_string());
|
let mut svc = Service::new("AVTransport".to_string());
|
||||||
|
|
||||||
@@ -151,6 +152,7 @@ impl MediaRendererFactory {
|
|||||||
pipeline.clone(),
|
pipeline.clone(),
|
||||||
state.clone(),
|
state.clone(),
|
||||||
instance_id.to_string(),
|
instance_id.to_string(),
|
||||||
|
stream_url_base.to_string(),
|
||||||
));
|
));
|
||||||
add_action(&mut svc, Arc::new(play))?;
|
add_action(&mut svc, Arc::new(play))?;
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,19 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use pmomediarenderer::PlaybackState;
|
use pmomediarenderer::PlaybackState;
|
||||||
use pmomediarenderer::PipelineControl;
|
use pmomediarenderer::PipelineControl;
|
||||||
use pmomediarenderer::{DeviceAdapter, DeviceCommand, MediaRendererRegistry, PlayerStateReport, SharedState};
|
use pmomediarenderer::{DeviceCommand, MediaRendererRegistry};
|
||||||
|
|
||||||
use crate::adapter::BrowserAdapter;
|
use crate::adapter::BrowserAdapter;
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct PlayerStateReport {
|
||||||
|
pub position_sec: Option<f64>,
|
||||||
|
pub duration_sec: Option<f64>,
|
||||||
|
pub state: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub ready_state: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct RegisterRequest {
|
pub struct RegisterRequest {
|
||||||
pub instance_id: String,
|
pub instance_id: String,
|
||||||
@@ -42,13 +51,13 @@ pub async fn register_handler(
|
|||||||
"WebRenderer: register request"
|
"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
|
match registry
|
||||||
.register_or_reconnect(&req.instance_id, &req.user_agent, adapter)
|
.register_or_reconnect(
|
||||||
|
&req.instance_id,
|
||||||
|
"/api/webrenderer",
|
||||||
|
"PMOMusic WebRenderer/2.0",
|
||||||
|
|state| Arc::new(BrowserAdapter::new(state)),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok((stream_url, udn, should_play)) => {
|
Ok((stream_url, udn, should_play)) => {
|
||||||
@@ -135,8 +144,27 @@ pub async fn report_handler(
|
|||||||
Path(instance_id): Path<String>,
|
Path(instance_id): Path<String>,
|
||||||
Json(report): Json<PlayerStateReport>,
|
Json(report): Json<PlayerStateReport>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
registry.update_player_state(&instance_id, report).await;
|
let instance = match registry.get_instance(&instance_id) {
|
||||||
StatusCode::OK
|
Some(i) => i,
|
||||||
|
None => return StatusCode::NOT_FOUND.into_response(),
|
||||||
|
};
|
||||||
|
let mut state = instance.state.write();
|
||||||
|
if let Some(pos) = report.position_sec {
|
||||||
|
state.position = Some(pmomediarenderer::seconds_to_upnp_time(pos));
|
||||||
|
}
|
||||||
|
if let Some(dur) = report.duration_sec {
|
||||||
|
state.duration = Some(pmomediarenderer::seconds_to_upnp_time(dur));
|
||||||
|
}
|
||||||
|
if let Some(s) = &report.state {
|
||||||
|
state.playback_state = match s.as_str() {
|
||||||
|
"playing" => PlaybackState::Playing,
|
||||||
|
"paused" => PlaybackState::Paused,
|
||||||
|
"stopped" => PlaybackState::Stopped,
|
||||||
|
_ => state.playback_state.clone(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
tracing::debug!(instance_id = %instance_id, position = ?state.position, "player state updated");
|
||||||
|
StatusCode::OK.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[axum::debug_handler]
|
#[axum::debug_handler]
|
||||||
@@ -144,8 +172,12 @@ pub async fn command_handler(
|
|||||||
State(registry): State<Arc<MediaRendererRegistry>>,
|
State(registry): State<Arc<MediaRendererRegistry>>,
|
||||||
Path(instance_id): Path<String>,
|
Path(instance_id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match registry.get_pending_command(&instance_id).await {
|
let Some(instance) = registry.get_instance(&instance_id) else {
|
||||||
Some(cmd) => (StatusCode::OK, Json(cmd)).into_response(),
|
return StatusCode::NO_CONTENT.into_response();
|
||||||
|
};
|
||||||
|
let cmd = instance.state.write().pop_command();
|
||||||
|
match cmd.and_then(|c| serde_json::to_value(c).ok()) {
|
||||||
|
Some(v) => (StatusCode::OK, Json(v)).into_response(),
|
||||||
None => StatusCode::NO_CONTENT.into_response(),
|
None => StatusCode::NO_CONTENT.into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.3.33
|
0.3.35
|
||||||
|
|||||||
Reference in New Issue
Block a user