webrenderer : câblage adapter & nettoyages terminés

- Phase 1.5/2.6 : adapter BrowserAdapter instancié dans WebRendererInstance et câblé aux handlers (Flush/Stop/Pause via deliver())
- Phase 2.4 : Flush envoyé au device sur TrackEnded (Weak<dyn DeviceAdapter> évite les cycles de référence)
- Phase 1.6 : méthodes browser-spécifiques supprimées de RendererRegistry (set_player_command, has_current_uri…), remplacée par get_instance() + accès direct adapter/pipeline
- handlers.rs : flush/stop et pause livrés via l'adapter dans stop_handler/pause_handle
- register.rs : endpoints /play, pause et set_uri mis à jour pour utiliser l'adapter
- pipeline.rs : adapter exposé dans PipelineHandle et passé au event listener via Weak
This commit is contained in:
2026-04-05 20:36:47 +02:00
parent c13de9f46b
commit fe2d755b4a
6 changed files with 81 additions and 155 deletions

View File

@@ -47,6 +47,12 @@ pub fn stop_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHandl
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)
}
@@ -58,6 +64,9 @@ pub fn pause_handler(pipeline: PipelineHandle, state: SharedState) -> ActionHand
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)
}

View File

@@ -31,6 +31,7 @@ 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,
}
@@ -69,6 +70,7 @@ impl InstancePipeline {
#[cfg(feature = "pmoserver")]
control_point: Arc<pmocontrol::ControlPoint>,
udn: String,
adapter: Arc<dyn crate::adapter::DeviceAdapter>,
) -> Self {
let stop_token = CancellationToken::new();
@@ -102,12 +104,14 @@ impl InstancePipeline {
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,
@@ -118,6 +122,7 @@ impl InstancePipeline {
player: player_handle,
stop_token: stop_token.clone(),
flac_handle: flac_handle.clone(),
adapter,
state,
};
@@ -133,12 +138,14 @@ impl InstancePipeline {
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 {
@@ -168,6 +175,10 @@ async fn run_event_listener(
}
PlayerEvent::TrackEnded => {
state.write().playback_state = PlaybackState::Transitioning;
// Vider le buffer du device avant la nouvelle piste.
if let Some(adapter) = adapter.upgrade() {
adapter.deliver(DeviceCommand::Flush);
}
#[cfg(feature = "pmoserver")]
{
let cp = control_point.clone();

View File

@@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::messages::PlaybackState;
use crate::pipeline::PipelineControl;
use crate::registry::RendererRegistry;
#[derive(Debug, Deserialize)]
@@ -109,7 +110,10 @@ pub async fn set_uri_handler(
Json(req): Json<UriRequest>,
) -> impl IntoResponse {
tracing::info!(instance_id = %instance_id, uri = %req.uri, "WebRenderer: set_uri request");
registry.load_uri(&instance_id, req.uri).await;
if let Some(pipeline) = registry.get_pipeline(&instance_id) {
pipeline.send(PipelineControl::LoadUri(req.uri.clone())).await;
pipeline.send(PipelineControl::Play).await;
}
StatusCode::OK
}
@@ -120,7 +124,9 @@ pub async fn pause_handler(
Path(instance_id): Path<String>,
) -> impl IntoResponse {
tracing::info!(instance_id = %instance_id, "WebRenderer: pause request");
registry.send_pause_command(&instance_id).await;
if let Some(instance) = registry.get_instance(instance_id.as_str()) {
instance.adapter.deliver(crate::adapter::DeviceCommand::Pause);
}
StatusCode::OK
}
@@ -168,7 +174,15 @@ pub async fn play_handler(
Path(instance_id): Path<String>,
) -> impl IntoResponse {
// Check if there's a valid URI loaded - if not, ignore the play command
if !registry.has_current_uri(&instance_id) {
let instance = match registry.get_instance(&instance_id) {
Some(i) => i,
None => {
return (StatusCode::NOT_FOUND, "Instance not found").into_response();
}
};
let has_uri = instance.state.read().current_uri.is_some();
if !has_uri {
tracing::warn!(instance_id = %instance_id, "Play command ignored: no URI loaded");
let mut headers = HeaderMap::new();
headers.insert(axum::http::header::CONTENT_TYPE, "text/plain".parse().unwrap());
@@ -177,18 +191,12 @@ pub async fn play_handler(
tracing::info!(instance_id = %instance_id, "WebRenderer: play request");
// Get stream URL and tell player to play it
// Send Stream command to the adapter (via pending_commands queue)
let stream_url = format!("/api/webrenderer/{}/stream", instance_id);
instance.adapter.deliver(crate::adapter::DeviceCommand::Stream { url: stream_url });
// Set command for player to start streaming
let command = serde_json::json!({
"type": "stream",
"url": stream_url
});
registry.set_player_command(&instance_id, command);
// Also tell pipeline to play (if not already) - use existing method
registry.send_play_command(&instance_id).await;
// Also tell pipeline to play (if not already)
instance.pipeline.send(PipelineControl::Play).await;
(StatusCode::OK, "OK").into_response()
}

View File

@@ -33,6 +33,8 @@ pub struct WebRendererInstance {
pub flac_handle: pmoaudio_ext::sinks::OggFlacStreamHandle,
pub pipeline: PipelineHandle,
pub created_at: SystemTime,
/// Adapter device-spécifique pour la livraison des commandes.
pub adapter: Arc<dyn crate::adapter::DeviceAdapter>,
}
/// Registre global des instances WebRenderer
@@ -151,6 +153,11 @@ impl RendererRegistry {
.map(|i| i.pipeline.clone())
}
/// Retourne l'instance par instance_id
pub fn get_instance(&self, instance_id: &str) -> Option<Arc<WebRendererInstance>> {
self.instances.read().get(instance_id).cloned()
}
/// Retourne le SharedState par instance_id
pub fn get_state(&self, instance_id: &str) -> Option<SharedState> {
self.instances
@@ -159,6 +166,11 @@ impl RendererRegistry {
.map(|i| i.state.clone())
}
/// Retourne le PipelineHandle par instance_id
pub fn get_pipeline(&self, instance_id: &str) -> Option<PipelineHandle> {
self.instances.read().get(instance_id).map(|i| i.pipeline.clone())
}
/// Retourne le SharedState et udn par instance_id
pub fn get_state_and_udn(&self, instance_id: &str) -> Option<(SharedState, String)> {
self.instances
@@ -276,56 +288,6 @@ impl RendererRegistry {
serde_json::to_value(cmd).ok()
}
/// Stocke une commande pour le player (consommée via GET /command)
pub fn set_player_command(&self, instance_id: &str, command: serde_json::Value) {
if let Some(instance) = self.instances.read().get(instance_id) {
if let Ok(cmd) = serde_json::from_value(command) {
instance.state.write().push_command(cmd);
}
}
}
/// Consume and send a command to the pipeline
async fn send_pipeline_command(&self, instance_id: &str, cmd: PipelineControl) {
let pipeline = self.get_pipeline(instance_id);
if let Some(pipeline) = pipeline {
pipeline.send(cmd).await;
} else {
tracing::error!(instance_id = %instance_id, "Instance not found for pipeline command");
}
}
fn get_pipeline(&self, instance_id: &str) -> Option<PipelineHandle> {
self.instances.read().get(instance_id).map(|i| i.pipeline.clone())
}
/// Charge une URI dans le pipeline et lance la lecture
pub async fn load_uri(&self, instance_id: &str, uri: String) {
self.send_pipeline_command(instance_id, PipelineControl::LoadUri(uri.clone())).await;
self.send_pipeline_command(instance_id, PipelineControl::Play).await;
tracing::info!(instance_id = %instance_id, uri = %uri, "loaded URI");
}
/// Envoie commande play au pipeline
pub async fn send_play_command(&self, instance_id: &str) {
tracing::info!(instance_id = %instance_id, "send_play_command called");
self.send_pipeline_command(instance_id, PipelineControl::Play).await;
}
/// Envoie commande pause au pipeline
pub async fn send_pause_command(&self, instance_id: &str) {
self.send_pipeline_command(instance_id, PipelineControl::Pause).await;
}
/// Check if the instance has a current URI loaded
pub fn has_current_uri(&self, instance_id: &str) -> bool {
self.instances
.read()
.get(instance_id)
.map(|i| i.state.read().current_uri.is_some())
.unwrap_or(false)
}
// ── Création d'instance ────────────────────────────────────────────────────
async fn create_instance(
@@ -348,6 +310,10 @@ impl RendererRegistry {
let state: SharedState = Arc::new(parking_lot::RwLock::new(RendererState::default()));
// Créer l'adapter avant le pipeline (sera passé à run_event_listener)
let adapter: Arc<dyn crate::adapter::DeviceAdapter> =
Arc::new(crate::adapter::BrowserAdapter::new(state.clone()));
#[cfg(feature = "pmoserver")]
let (device_instance, pipeline) = {
use pmoupnp::UpnpServerExt;
@@ -355,11 +321,12 @@ impl RendererRegistry {
let server_arc = pmoserver::get_server()
.ok_or(WebRendererError::ServerNotAvailable)?;
// Créer le pipeline d'abord pour avoir le PipelineHandle
// Créer le pipeline avec l'adapter pour le event listener
let ip = InstancePipeline::start(
state.clone(),
self.control_point.clone(),
full_udn.clone(),
adapter.clone(),
);
let pipeline = ip.pipeline_handle.clone();
@@ -398,7 +365,11 @@ impl RendererRegistry {
let (device_instance, pipeline) = {
use pmoupnp::UpnpModel;
let ip = InstancePipeline::start(state.clone(), full_udn.clone());
let ip = InstancePipeline::start(
state.clone(),
full_udn.clone(),
adapter.clone(),
);
let pipeline = ip.pipeline_handle.clone();
let device = WebRendererFactory::create_device_with_pipeline(
@@ -420,6 +391,7 @@ impl RendererRegistry {
flac_handle: pipeline.flac_handle.clone(),
pipeline: pipeline.pipeline_handle,
created_at: SystemTime::now(),
adapter,
})
}