From 5440e72afb0fb7109a7f20b2a813ab3655f8f352 Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 31 Jan 2026 13:11:55 +0100 Subject: [PATCH] Corriger les informations de position pour les flux continus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cette modification corrige le calcul de la position et de la durée pour les flux continus (webradio) en ajustant la méthode `poll_and_emit_changes()` dans `pmocontrol`. Elle détecte maintenant les flux via `is_playing_a_stream()`, extrait la durée depuis les métadonnées DIDL, et calcule la position relative basée sur `track_start_time`. Les valeurs par défaut (zéro pour la position, none pour la durée) sont utilisées si aucune durée n'est disponible. Les fichiers modifiés incluent `musicrenderer.rs`, `CurrentTrack.vue` et `useRenderers.ts` pour assurer une gestion cohérente des données de position et de durée. --- .../Report/patch_position_info_for_stream.md | 15 +++++ .../Todo/patch_position_info_for_stream.md | 20 +++++++ .../components/pmocontrol/CurrentTrack.vue | 4 +- pmoapp/webapp/src/composables/useRenderers.ts | 15 +++++ .../src/music_renderer/musicrenderer.rs | 60 ++++++++++++++++++- .../src/music_renderer/openhome_renderer.rs | 59 +++++++++--------- 6 files changed, 143 insertions(+), 30 deletions(-) create mode 100644 Blackboard/Report/patch_position_info_for_stream.md create mode 100644 Blackboard/Todo/patch_position_info_for_stream.md diff --git a/Blackboard/Report/patch_position_info_for_stream.md b/Blackboard/Report/patch_position_info_for_stream.md new file mode 100644 index 00000000..947e9198 --- /dev/null +++ b/Blackboard/Report/patch_position_info_for_stream.md @@ -0,0 +1,15 @@ +# Rapport : Patch des informations de position pour les flux continus + +## Résumé +Modification de la méthode `poll_and_emit_changes()` dans la crate `pmocontrol` pour corriger les données de position et durée lorsqu'un renderer diffuse un flux continu (webradio). La méthode détecte maintenant si un flux est en cours via `is_playing_a_stream()` et applique un traitement spécifique : extraction de la durée depuis les métadonnées DIDL, et calcul de la position relative depuis `track_start_time` (qui est déjà maintenu à jour lors des changements de métadonnées). Si aucune durée n'est disponible, la position et la durée sont mises à zéro/none. + +## Fichiers modifiés + +1. `pmocontrol/src/music_renderer/musicrenderer.rs` + - Modification de la méthode `poll_and_emit_changes()` pour patcher les informations de position lors de la détection d'un flux continu + - Ajout du logging au niveau info lors de la détection d'un flux continu + - Extraction conditionnelle de la durée depuis les métadonnées DIDL pour les streams + - Calcul de la position relative basé sur `track_start_time` (différence entre now et track_start_time) + - Retour de valeurs par défaut (zéro pour position, none pour duration) si aucune durée n'est disponible dans les métadonnées + - Préservation de la logique existante pour les médias réguliers (non-streams) + - Note : `track_start_time` est déjà maintenu à jour par la logique existante lors des changements de métadonnées diff --git a/Blackboard/Todo/patch_position_info_for_stream.md b/Blackboard/Todo/patch_position_info_for_stream.md new file mode 100644 index 00000000..63f99890 --- /dev/null +++ b/Blackboard/Todo/patch_position_info_for_stream.md @@ -0,0 +1,20 @@ +** Ce travail devra être réalisé en suivant scrupuleusement les consignes listées dans le fichier [@Rules_optimal.md](file:///Users/coissac/Sync/maison/Petite_maisons/src/pmomusic/Blackboard/Rules_optimal.md) ** + +Cette modification cible la cratte pmocontrol uniquement + +Les musiques Renderer, Disposent maintenant d'une méthode leur permettant de savoir s'ils sont en train de diffuser une webradio via leur méthode is_playing_a_stream. + +Il faut donc que dans la méthode poll_and_emit_changes On fait ce qui est nécessaire pour envoyer des données de position et de durée de track corrigée si l'on a is_playing_a_stream à vrai. + +Si is_playing_a_stream à vrai: + - Maintenir à jour la valeur `track_start_time` de la classe + MusicRenderer En la mettant égale à now de metadata. + - Loguer cet événement au niveau info. + - Extraire la durée du morceau depuis les métadonnées + fournies par la structure de position. + - Si la durée est disponible: + - Utilisez cette donnée pour la pousser sur le bus des événements. + - calculer la position dans le flux comme la différence + entre now et track_start_time. + - Sinon: Envoyer zéro pour la position et none pour la duration. +- Sinon, transmettre les données fournies comme actuellement. diff --git a/pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue b/pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue index 45cf56c1..fd0248bb 100644 --- a/pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue +++ b/pmoapp/webapp/src/components/pmocontrol/CurrentTrack.vue @@ -83,7 +83,9 @@ const progressPercent = computed(() => { const position = state.value?.position_ms; const duration = state.value?.duration_ms; if (position && duration && duration > 0) { - return (position / duration) * 100; + // Plafonner à 100% pour éviter que le curseur dépasse en cas de délai de mise à jour + const percent = (position / duration) * 100; + return Math.min(percent, 100); } return 0; }); diff --git a/pmoapp/webapp/src/composables/useRenderers.ts b/pmoapp/webapp/src/composables/useRenderers.ts index f29486ef..b653bee7 100644 --- a/pmoapp/webapp/src/composables/useRenderers.ts +++ b/pmoapp/webapp/src/composables/useRenderers.ts @@ -123,6 +123,9 @@ function ensureSSEConnected() { break; case "position_changed": + // Mettre à jour position et durée de manière atomique pour garantir la cohérence + // Le backend envoie TOUJOURS les deux valeurs (même si null) + // Convertir rel_time (HH:MM:SS) en millisecondes if (event.rel_time) { const parts = event.rel_time.split(":").map(Number); @@ -133,7 +136,11 @@ function ensureSSEConnected() { (parts[2] ?? 0)) * 1000; } + } else { + // Si rel_time est null/undefined, mettre position à 0 + snapshot.state.position_ms = 0; } + // Convertir track_duration (HH:MM:SS) en millisecondes if (event.track_duration) { const parts = event.track_duration.split(":").map(Number); @@ -144,7 +151,15 @@ function ensureSSEConnected() { (parts[2] ?? 0)) * 1000; } + } else { + // Si track_duration est null/undefined (flux continu sans durée), + // mettre duration_ms à null pour afficher "--:--" + snapshot.state.duration_ms = null; } + + // Important: Trigger reactivity en réassignant l'objet complet + // Cela garantit que position_ms et duration_ms sont mis à jour atomiquement + snapshotState.snapshots.set(rendererId, { ...snapshot }); break; case "volume_changed": diff --git a/pmocontrol/src/music_renderer/musicrenderer.rs b/pmocontrol/src/music_renderer/musicrenderer.rs index 98b43a17..9baf701b 100644 --- a/pmocontrol/src/music_renderer/musicrenderer.rs +++ b/pmocontrol/src/music_renderer/musicrenderer.rs @@ -99,6 +99,10 @@ struct MusicRendererState { /// Timestamp when the current track started playing. /// Used to calculate elapsed time when renderer returns unreliable position info. track_start_time: Option, + /// Current track duration (HH:MM:SS format). + /// For continuous streams, this is kept stable and only updated when it increases + /// (to avoid decreasing duration updates from radio metadata). + current_track_duration: Option, } impl Default for MusicRendererState { @@ -110,6 +114,7 @@ impl Default for MusicRendererState { sleep_timer: SleepTimer::default(), has_played_since_track_start: false, track_start_time: None, + current_track_duration: None, } } } @@ -405,7 +410,56 @@ impl MusicRenderer { } // Now get the fully patched position (with correct track_start_time and rel_time) - if let Ok(position) = self.playback_position() { + if let Ok(mut position) = self.playback_position() { + // For continuous streams, manage duration to prevent it from decreasing + let is_stream = self.is_playing_a_stream(); + if is_stream { + if let Some(ref new_duration) = position.track_duration { + let mut state = self.state.lock().unwrap(); + + // Parse durations to compare (HH:MM:SS format) + let parse_duration = |dur_str: &str| -> Option { + let parts: Vec<&str> = dur_str.split(':').collect(); + if parts.len() == 3 { + let h: u32 = parts[0].parse().ok()?; + let m: u32 = parts[1].parse().ok()?; + let s: u32 = parts[2].parse().ok()?; + Some(h * 3600 + m * 60 + s) + } else { + None + } + }; + + match &state.current_track_duration { + Some(stored_duration) => { + // Compare new duration with stored one + if let (Some(stored_secs), Some(new_secs)) = ( + parse_duration(stored_duration), + parse_duration(new_duration), + ) { + if new_secs > stored_secs { + // Duration increased: update stored value and use new one + tracing::debug!( + "MusicRenderer [{}]: Stream duration increased: {} -> {}", + self.info.friendly_name(), + stored_duration, + new_duration + ); + state.current_track_duration = Some(new_duration.clone()); + } else { + // Duration decreased or equal: keep stored value + position.track_duration = Some(stored_duration.clone()); + } + } + } + None => { + // First time: store the duration + state.current_track_duration = Some(new_duration.clone()); + } + } + } + } + let changed = watched .position .as_ref() @@ -1209,13 +1263,15 @@ impl MusicRenderer { } /// Sets the last known track metadata. - /// Updates track_start_time only if the metadata actually changes. + /// Updates track_start_time and resets current_track_duration only if the metadata actually changes. pub fn set_last_metadata(&self, metadata: Option) { let mut state = self.state.lock().unwrap(); let metadata_changed = state.last_metadata != metadata; state.last_metadata = metadata; if metadata_changed { state.track_start_time = Some(SystemTime::now()); + // Reset duration cache when track changes (for streams) + state.current_track_duration = None; } } diff --git a/pmocontrol/src/music_renderer/openhome_renderer.rs b/pmocontrol/src/music_renderer/openhome_renderer.rs index f4f6e37b..4ddf3eac 100644 --- a/pmocontrol/src/music_renderer/openhome_renderer.rs +++ b/pmocontrol/src/music_renderer/openhome_renderer.rs @@ -313,49 +313,54 @@ impl PlaybackPosition for OpenHomeRenderer { let mut track_uri = None; let mut track_metadata_xml = None; + // Get track ID from playlist if let Some(playlist_client) = &self.playlist { match playlist_client.id() { Ok(id) => track_id = Some(id), Err(err) => debug!( - // renderer = self.info.id.0.as_str(), error = %err, "Failed to read OpenHome track id" ), } } - if let Some(info_client) = &self.info_client { - match info_client.track() { - Ok(track) => { - track_uri = Some(track.uri.clone()); - track_metadata_xml = track.metadata_xml; + // Use queue API to get current item with cached metadata + let mut queue_guard = self.queue.lock().unwrap(); + if let Ok(Some((current_item, _))) = queue_guard.peek_current() { + // Use metadata from queue cache (updated via OpenHome events) + track_uri = Some(current_item.uri.clone()); - // Check if the URI has changed to detect track changes - let mut cached_uri = self.current_track_uri.lock().unwrap(); - let uri_changed = cached_uri.as_ref() != Some(&track.uri); + // Build DIDL metadata XML from cached TrackMetadata + if let Some(ref metadata) = current_item.metadata { + track_metadata_xml = Some( + crate::music_renderer::musicrenderer::build_didl_lite_metadata( + metadata, + ¤t_item.uri, + ¤t_item.protocol_info, + ), + ); + } - if uri_changed { - tracing::debug!( - "OpenHome track URI changed: {:?} -> {:?}", - cached_uri, - track.uri - ); + // Check if the URI has changed to detect track changes + let mut cached_uri = self.current_track_uri.lock().unwrap(); + let uri_changed = cached_uri.as_ref() != Some(¤t_item.uri); - // Détecte si la nouvelle URL est un flux continu - let is_stream = crate::music_renderer::is_continuous_stream_url(&track.uri); - *self.continuous_stream.lock().unwrap() = is_stream; - tracing::debug!("OpenHome URI changed, continuous_stream={}", is_stream); + if uri_changed { + tracing::debug!( + "OpenHome track URI changed: {:?} -> {:?}", + cached_uri, + current_item.uri + ); - *cached_uri = Some(track.uri); - } - } - Err(err) => debug!( - // renderer = self.info.id.0.as_str(), - error = %err, - "Failed to read OpenHome track metadata" - ), + // Détecte si la nouvelle URL est un flux continu + let is_stream = crate::music_renderer::is_continuous_stream_url(¤t_item.uri); + *self.continuous_stream.lock().unwrap() = is_stream; + tracing::debug!("OpenHome URI changed, continuous_stream={}", is_stream); + + *cached_uri = Some(current_item.uri.clone()); } } + drop(queue_guard); // Get duration from Time service - duration_secs=0 means stream (no duration) let track_duration = if time_info.duration_secs == 0 {