Corriger les informations de position pour les flux continus

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.
This commit is contained in:
2026-01-31 13:11:55 +01:00
parent d98bc357b9
commit 5440e72afb
6 changed files with 143 additions and 30 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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;
});

View File

@@ -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":

View File

@@ -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<SystemTime>,
/// 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<String>,
}
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<u32> {
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<TrackMetadata>) {
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;
}
}

View File

@@ -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,
&current_item.uri,
&current_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(&current_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(&current_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 {