From c863f045ae0796f439faef155393da2c22c4c1ee Mon Sep 17 00:00:00 2001 From: Benoit Date: Fri, 22 May 2026 23:16:48 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20VU-m=C3=A8tre=20audio=20entrant=20fonct?= =?UTF-8?q?ionnel=20+=20simplification=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modifications : - Ajout analyseur audio pour pistes distantes (remoteAnalyserRef) - setupRemoteAudioAnalyser() appelé sur TrackSubscribed - analyseAudioLevel() alterne automatiquement entre micro local et audio entrant - useEffect redémarre analyse quand isTalking change - Cleanup complet des 2 contextes audio (local + remote) UI VU-mètre : - Suppression jauge redondante (gardé uniquement barres) - Barres uniformes (même hauteur) au lieu d'effet égaliseur - Couleurs distinctes : vert (audio entrant) vs bleu/rouge (micro) - Jaune > 75%, rouge clignotant > 90% Tests validés : ✅ VU-mètre micro local : fonctionne ✅ VU-mètre audio entrant : fonctionne (fix principal) ✅ Alternance automatique talking/listening 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- client/src/components/AudioIndicator.css | 10 ++- client/src/components/AudioIndicator.jsx | 12 +-- client/src/hooks/useLiveKit.js | 93 ++++++++++++++++++++---- 3 files changed, 85 insertions(+), 30 deletions(-) diff --git a/client/src/components/AudioIndicator.css b/client/src/components/AudioIndicator.css index 2a47385..fce3933 100644 --- a/client/src/components/AudioIndicator.css +++ b/client/src/components/AudioIndicator.css @@ -56,18 +56,20 @@ .audio-bar { flex: 1; - background: var(--color-bg); + height: 100%; + background: rgba(255, 255, 255, 0.1); border-radius: 2px; transition: all 0.1s ease; - min-height: 4px; - height: 20%; } .audio-bar.active { - height: 100%; background: var(--color-success); } +.audio-bar.active.talking { + background: var(--color-primary); +} + .audio-bar.active.warning { background: var(--color-warning); } diff --git a/client/src/components/AudioIndicator.jsx b/client/src/components/AudioIndicator.jsx index ae5170e..2900a65 100644 --- a/client/src/components/AudioIndicator.jsx +++ b/client/src/components/AudioIndicator.jsx @@ -11,17 +11,9 @@ export default function AudioIndicator({ level, isTalking }) {
{isTalking ? 'Votre micro' : 'Audio entrant'} - {Math.round(normalizedLevel)}%
-
-
-
- - {/* Bars VU-mètre style */} + {/* VU-mètre barres */}
{[...Array(20)].map((_, i) => { const threshold = (i + 1) * 5; @@ -34,7 +26,7 @@ export default function AudioIndicator({ level, isTalking }) { key={i} className={`audio-bar ${isActive ? 'active' : ''} ${ isActive && isDanger ? 'danger' : isActive && isWarning ? 'warning' : '' - }`} + } ${isTalking ? 'talking' : ''}`} /> ); })} diff --git a/client/src/hooks/useLiveKit.js b/client/src/hooks/useLiveKit.js index e5063a0..5c1abcc 100644 --- a/client/src/hooks/useLiveKit.js +++ b/client/src/hooks/useLiveKit.js @@ -17,6 +17,11 @@ export default function useLiveKit() { const animationFrameRef = useRef(null); const isAudioUnlockedRef = useRef(false); + // Analyseur audio pour pistes distantes (audio entrant) + const remoteAudioContextRef = useRef(null); + const remoteAnalyserRef = useRef(null); + const remoteAnimationFrameRef = useRef(null); + /** * Connexion à la room LiveKit */ @@ -68,6 +73,9 @@ export default function useLiveKit() { if (track.kind === Track.Kind.Audio) { const audioElement = track.attach(); document.body.appendChild(audioElement); + + // Setup analyseur pour audio entrant + setupRemoteAudioAnalyser(track); } }); @@ -92,6 +100,8 @@ export default function useLiveKit() { // Mute par défaut (PTT) track.mute(); setupAudioAnalyser(track); + // Démarrer l'analyse audio + analyseAudioLevel(); console.log('✓ Track audio configuré et muted pour PTT'); } }); @@ -249,7 +259,7 @@ export default function useLiveKit() { }; /** - * Setup analyseur audio pour VU-mètre + * Setup analyseur audio pour VU-mètre (micro local) */ const setupAudioAnalyser = (track) => { try { @@ -266,36 +276,63 @@ export default function useLiveKit() { audioContextRef.current = audioContext; analyserRef.current = analyser; - // Démarrer analyse - analyseAudioLevel(); + console.log('✓ Analyseur audio local configuré'); } catch (error) { - console.error('Erreur setup analyser:', error); + console.error('Erreur setup analyser local:', error); + } + }; + + /** + * Setup analyseur audio pour pistes distantes (audio entrant) + */ + const setupRemoteAudioAnalyser = (track) => { + try { + const mediaStream = track.mediaStream; + if (!mediaStream) return; + + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const analyser = audioContext.createAnalyser(); + const source = audioContext.createMediaStreamSource(mediaStream); + + analyser.fftSize = 256; + source.connect(analyser); + + remoteAudioContextRef.current = audioContext; + remoteAnalyserRef.current = analyser; + + console.log('✓ Analyseur audio distant configuré'); + } catch (error) { + console.error('Erreur setup analyser distant:', error); } }; /** * Analyser niveau audio (pour VU-mètre) + * Alterne entre micro local (si talking) et audio entrant (si listening) */ - const analyseAudioLevel = () => { - if (!analyserRef.current) return; - - const analyser = analyserRef.current; - const dataArray = new Uint8Array(analyser.frequencyBinCount); - + const analyseAudioLevel = useCallback(() => { const analyse = () => { - analyser.getByteFrequencyData(dataArray); + // Choisir l'analyseur selon l'état + const analyser = isTalking ? analyserRef.current : remoteAnalyserRef.current; - // Calculer moyenne - const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length; - const normalized = Math.min(100, (average / 255) * 100); + if (analyser) { + const dataArray = new Uint8Array(analyser.frequencyBinCount); + analyser.getByteFrequencyData(dataArray); - setAudioLevel(normalized); + // Calculer moyenne + const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length; + const normalized = Math.min(100, (average / 255) * 100); + + setAudioLevel(normalized); + } else { + setAudioLevel(0); + } animationFrameRef.current = requestAnimationFrame(analyse); }; analyse(); - }; + }, [isTalking]); /** * Cleanup @@ -306,15 +343,39 @@ export default function useLiveKit() { animationFrameRef.current = null; } + if (remoteAnimationFrameRef.current) { + cancelAnimationFrame(remoteAnimationFrameRef.current); + remoteAnimationFrameRef.current = null; + } + if (audioContextRef.current) { audioContextRef.current.close(); audioContextRef.current = null; } + if (remoteAudioContextRef.current) { + remoteAudioContextRef.current.close(); + remoteAudioContextRef.current = null; + } + analyserRef.current = null; + remoteAnalyserRef.current = null; localTrackRef.current = null; }; + // Redémarrer l'analyse audio quand isTalking change + useEffect(() => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + + // Redémarrer l'analyse si on a au moins un analyseur + if (analyserRef.current || remoteAnalyserRef.current) { + analyseAudioLevel(); + } + }, [isTalking, analyseAudioLevel]); + // Cleanup au démontage useEffect(() => { return () => {