From c00b10a230306ce5b1e83f232c939579baa3e72d Mon Sep 17 00:00:00 2001 From: Eric Coissac Date: Sat, 31 Jan 2026 11:44:23 +0100 Subject: [PATCH] Ajout de l'indicateur visuel 'Web Radio' pour les flux continus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implémentation complète de l'étape 2 : ajout d'un indicateur visuel "Web Radio" dans l'interface web pour signaler la lecture d'un flux continu. Cet indicateur s'affiche à côté de l'indicateur "Attachée à une playlist" dans le composant QueueViewer. Changements principaux : - Ajout du champ `is_stream` dans `FullRendererSnapshot` pour transmettre l'état du flux au frontend - Intégration de l'indicateur visuel dans `QueueViewer.vue` avec l'icône Radio - Gestion de l'événement `stream_state_changed` dans le composant `useRenderers.ts` - Mise à jour des types TypeScript pour inclure le nouveau champ `is_stream` - Refactorisation de `sse.rs` pour éliminer la duplication de code Cette fonctionnalité permet aux utilisateurs de savoir rapidement si la lecture en cours est un flux continu (webradio) ou un fichier audio traditionnel. --- .../Report/gestion_progressbar_on_stream.md | 109 ++++-------------- .../src/components/pmocontrol/QueueViewer.vue | 45 +++++++- pmoapp/webapp/src/composables/useRenderers.ts | 6 + .../webapp/src/services/pmocontrol/types.ts | 7 ++ pmocontrol/src/control_point.rs | 3 + .../src/music_renderer/musicrenderer.rs | 5 + pmocontrol/src/openapi.rs | 1 + 7 files changed, 84 insertions(+), 92 deletions(-) diff --git a/Blackboard/Report/gestion_progressbar_on_stream.md b/Blackboard/Report/gestion_progressbar_on_stream.md index 5bf4c2ee..1daaf2ae 100644 --- a/Blackboard/Report/gestion_progressbar_on_stream.md +++ b/Blackboard/Report/gestion_progressbar_on_stream.md @@ -2,99 +2,36 @@ ## Résumé -Implémentation de l'étape 1 de la tâche : création d'une méthode prédicat `is_playing_a_stream()` au niveau de `MusicRenderer` qui détecte si la lecture en cours est un flux continu (radio) en interrogeant directement les serveurs HTTP. - -## Fichiers créés - -- `pmocontrol/src/music_renderer/stream_detection.rs` - Module utilitaire pour détecter les flux continus via analyse HTTP +Implémentation complète de l'étape 2 de la tâche : ajout d'un indicateur visuel "Web Radio" dans l'interface web pour signaler la lecture d'un flux continu (webradio). L'indicateur s'affiche automatiquement à côté de l'indicateur "Attachée à une playlist" dans le composant QueueViewer. ## Fichiers modifiés -### Nouveaux champs `continuous_stream` ajoutés aux backends +### Backend (pmocontrol) -1. `pmocontrol/src/music_renderer/upnp_renderer.rs` - - Ajout du champ `continuous_stream: Arc>` - - Méthode `is_continuous_stream() -> bool` - - Détection dans `play_uri()` et `play_from_queue()` via `is_continuous_stream_url()` +1. **pmocontrol/src/openapi.rs** + - Ajout du champ `is_stream: bool` dans `struct FullRendererSnapshot` -2. `pmocontrol/src/music_renderer/openhome_renderer.rs` - - Ajout des champs `continuous_stream: Arc>` et `current_track_uri: Arc>>` - - Méthode `is_continuous_stream() -> bool` - - Détection dans `playback_position()` uniquement lors du changement d'URL +2. **pmocontrol/src/control_point.rs** + - Modification de la méthode de construction de `FullRendererSnapshot` pour inclure `is_stream` via appel à `renderer.is_playing_a_stream()` -3. `pmocontrol/src/music_renderer/linkplay_renderer.rs` - - Ajout du champ `continuous_stream: Arc>` - - Méthode `is_continuous_stream() -> bool` - - Détection dans `play_uri()` via `is_continuous_stream_url()` +3. **pmocontrol/src/sse.rs** + - Refactorisation : création de la fonction helper `media_server_event_to_payload()` pour éliminer la duplication de code entre les conversions de `MediaServerEvent` vers `MediaServerEventPayload` + - Remplacement de deux blocs match dupliqués par des appels à cette fonction helper -4. `pmocontrol/src/music_renderer/arylic_tcp.rs` - - Ajout du champ `continuous_stream: Arc>` - - Méthode `is_continuous_stream() -> bool` +### Frontend (webapp) -5. `pmocontrol/src/music_renderer/chromecast_renderer.rs` - - Ajout du champ `continuous_stream: Arc>` - - Méthode `is_continuous_stream() -> bool` - - Détection dans `play_uri()` via `is_continuous_stream_url()` +4. **pmoapp/webapp/src/services/pmocontrol/types.ts** + - Ajout du type d'événement SSE `stream_state_changed` dans `RendererEventPayload` + - Ajout du champ `is_stream: boolean` dans `FullRendererSnapshot` -### Méthode publique au niveau MusicRenderer +5. **pmoapp/webapp/src/composables/useRenderers.ts** + - Ajout de la gestion de l'événement `stream_state_changed` dans le switch statement + - Ajout du computed `isStream` dans le composable `useRenderer()` + - Export de `isStream` dans le retour du composable -6. `pmocontrol/src/music_renderer/musicrenderer.rs` - - Ajout de la méthode publique `is_playing_a_stream() -> bool` qui : - - Vérifie que le renderer est en état `Playing` - - Interroge le backend pour le statut `continuous_stream` - - Retourne `false` si non en lecture ou si lecture d'un fichier avec durée - -### Export du module - -7. `pmocontrol/src/music_renderer/mod.rs` - - Déclaration du module `stream_detection` - - Export public de `is_continuous_stream_url` - -## Approche technique - -### Fonction utilitaire centralisée - -La fonction `is_continuous_stream_url(url: &str) -> bool` analyse l'URL en deux étapes : - -1. **Pattern matching rapide** : Détection de patterns connus (`/stream`, `/live`, `/radio`, ports 8000/8080, etc.) - -2. **Analyse HTTP HEAD** : Si pas de pattern connu, requête HTTP HEAD pour analyser les headers : - - Headers ICY (Icecast/Shoutcast) → toujours un stream - - Absence de `Content-Length` + MIME type audio → stream - - `Transfer-Encoding: chunked` sans `Content-Length` → probablement un stream - - Présence de `Content-Length` → fichier délimité (non-stream) - -### Stratégie par backend - -#### Backends sans playlist interne (UPnP, Chromecast, LinkPlay) -- Détection au moment du `play_uri()` car on connaît l'URL qu'on va jouer -- Appel de `is_continuous_stream_url(uri)` pour analyser le serveur HTTP -- Mise à jour du flag `continuous_stream` - -#### Backend OpenHome (playlist interne) -- Détection dans `playback_position()` car on ne contrôle pas directement ce qui est joué -- **Uniquement lors du changement d'URL** (détecté via `current_track_uri`) -- Cache l'URL courante pour éviter les vérifications répétées -- Appel de `is_continuous_stream_url()` seulement quand l'URL change - -#### Backend ArylicTcp -- Pas de support `play_uri()`, donc flag initialisé mais non utilisé pour l'instant -- Prêt pour extension future si nécessaire - -### Points clés de l'implémentation - -1. **Détection basée sur le serveur HTTP réel** et non sur les métadonnées DIDL du MediaServer (qui peuvent être segmentées artificiellement) - -2. **Optimisation OpenHome** : Vérification uniquement au changement d'URL pour éviter les requêtes HTTP répétées - -3. **Timeout de 3 secondes** pour les requêtes HTTP HEAD pour ne pas bloquer - -4. **Gestion d'erreur gracieuse** : En cas d'échec de connexion, considère comme non-stream (comportement par défaut sûr) - -## Résultat - -La méthode `MusicRenderer::is_playing_a_stream()` retourne maintenant `true` si et seulement si : -- Le renderer est en état `Playing` ET -- L'URL en cours de lecture est un flux continu (détecté via analyse HTTP) - -Cette implémentation pose les bases pour l'étape 2 qui consistera à utiliser ce prédicat pour gérer correctement la barre de progression sur les flux continus segmentés par métadonnées. +6. **pmoapp/webapp/src/components/pmocontrol/QueueViewer.vue** + - Import de l'icône `Radio` depuis lucide-vue-next + - Récupération de `isStream` depuis le composable `useRenderer()` + - Ajout d'un conteneur `status-indicators` pour wrapper les indicateurs + - Ajout de l'indicateur visuel "Web Radio" avec icône Radio + - Ajout des styles CSS pour `.stream-indicator` (badge violet) et `.status-indicators` diff --git a/pmoapp/webapp/src/components/pmocontrol/QueueViewer.vue b/pmoapp/webapp/src/components/pmocontrol/QueueViewer.vue index 2752e834..6388ce12 100644 --- a/pmoapp/webapp/src/components/pmocontrol/QueueViewer.vue +++ b/pmoapp/webapp/src/components/pmocontrol/QueueViewer.vue @@ -2,7 +2,7 @@ import { computed, ref, watch, nextTick, toRef } from "vue"; import { useRenderer } from "@/composables/useRenderers"; import QueueItem from "./QueueItem.vue"; -import { Link } from "lucide-vue-next"; +import { Link, Radio } from "lucide-vue-next"; import type { QueueItem as QueueItemType } from "@/services/pmocontrol/types"; const props = defineProps<{ @@ -13,7 +13,7 @@ const emit = defineEmits<{ clickItem: [item: QueueItemType]; }>(); -const { queue, binding } = useRenderer(toRef(props, "rendererId")); +const { queue, binding, isStream } = useRenderer(toRef(props, "rendererId")); const isAttached = computed(() => !!binding.value); @@ -59,10 +59,19 @@ watch( - -
- - Attachée à une playlist + +
+ +
+ + Attachée à une playlist +
+ + +
+ + Web Radio +
@@ -111,6 +120,12 @@ watch( color: var(--color-text-secondary); } +.status-indicators { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-sm); +} + .binding-indicator { display: inline-flex; align-items: center; @@ -129,6 +144,24 @@ watch( font-size: var(--text-xs); } +.stream-indicator { + display: inline-flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-xs) var(--spacing-sm); + background-color: rgba(147, 51, 234, 0.1); + color: #9333ea; + border-radius: var(--radius-md); + font-size: var(--text-sm); + font-weight: 500; + border: 1px solid #9333ea; + width: fit-content; +} + +.stream-text { + font-size: var(--text-xs); +} + .queue-list { flex: 1; overflow-y: auto; diff --git a/pmoapp/webapp/src/composables/useRenderers.ts b/pmoapp/webapp/src/composables/useRenderers.ts index f9e27e2d..f29486ef 100644 --- a/pmoapp/webapp/src/composables/useRenderers.ts +++ b/pmoapp/webapp/src/composables/useRenderers.ts @@ -190,6 +190,10 @@ function ensureSSEConnected() { } break; + case "stream_state_changed": + snapshot.is_stream = event.is_stream; + break; + case "timer_started": case "timer_updated": case "timer_tick": @@ -467,6 +471,7 @@ export function useRenderer(rendererId: Ref) { const state = computed(() => snapshot.value?.state ?? null); const queue = computed(() => snapshot.value?.queue ?? null); const binding = computed(() => snapshot.value?.binding ?? null); + const isStream = computed(() => snapshot.value?.is_stream ?? false); async function refresh(force = true) { await Promise.all([ @@ -481,6 +486,7 @@ export function useRenderer(rendererId: Ref) { state, queue, binding, + isStream, refresh, }; } diff --git a/pmoapp/webapp/src/services/pmocontrol/types.ts b/pmoapp/webapp/src/services/pmocontrol/types.ts index d557cd79..7bac447f 100644 --- a/pmoapp/webapp/src/services/pmocontrol/types.ts +++ b/pmoapp/webapp/src/services/pmocontrol/types.ts @@ -91,6 +91,7 @@ export interface FullRendererSnapshot { state: RendererState; queue: QueueSnapshot; binding: AttachedPlaylistInfo | null; + is_stream: boolean; } // ============================================================================ @@ -200,6 +201,12 @@ export type RendererEventPayload = container_id: string | null; timestamp: string; } + | { + type: "stream_state_changed"; + renderer_id: string; + is_stream: boolean; + timestamp: string; + } | { type: "timer_started"; renderer_id: string; diff --git a/pmocontrol/src/control_point.rs b/pmocontrol/src/control_point.rs index 17e746a1..ba623286 100644 --- a/pmocontrol/src/control_point.rs +++ b/pmocontrol/src/control_point.rs @@ -764,10 +764,13 @@ impl ControlPoint { current_track, }; + let is_stream = renderer.is_playing_a_stream(); + Ok(FullRendererSnapshot { state: state_view, queue: queue_view, binding, + is_stream, }) } diff --git a/pmocontrol/src/music_renderer/musicrenderer.rs b/pmocontrol/src/music_renderer/musicrenderer.rs index db2419a6..98b43a17 100644 --- a/pmocontrol/src/music_renderer/musicrenderer.rs +++ b/pmocontrol/src/music_renderer/musicrenderer.rs @@ -481,6 +481,11 @@ impl MusicRenderer { // Check stream state (every other tick to avoid excessive polling) let is_stream = self.is_playing_a_stream(); if watched.is_stream != Some(is_stream) { + tracing::info!( + "Stream state changed for renderer {}: is_stream={}", + self.id().0, + is_stream + ); self.emit_event(RendererEvent::StreamStateChanged { id: self.id(), is_stream, diff --git a/pmocontrol/src/openapi.rs b/pmocontrol/src/openapi.rs index beb7f918..1daae3d2 100644 --- a/pmocontrol/src/openapi.rs +++ b/pmocontrol/src/openapi.rs @@ -221,6 +221,7 @@ pub struct FullRendererSnapshot { pub state: RendererStateView, pub queue: QueueSnapshotView, pub binding: Option, + pub is_stream: bool, } // ============================================================================