feat: VU-mètre audio entrant fonctionnel + simplification UI

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 23:16:48 +02:00
parent ed22e6d878
commit c863f045ae
3 changed files with 85 additions and 30 deletions
+6 -4
View File
@@ -56,18 +56,20 @@
.audio-bar { .audio-bar {
flex: 1; flex: 1;
background: var(--color-bg); height: 100%;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px; border-radius: 2px;
transition: all 0.1s ease; transition: all 0.1s ease;
min-height: 4px;
height: 20%;
} }
.audio-bar.active { .audio-bar.active {
height: 100%;
background: var(--color-success); background: var(--color-success);
} }
.audio-bar.active.talking {
background: var(--color-primary);
}
.audio-bar.active.warning { .audio-bar.active.warning {
background: var(--color-warning); background: var(--color-warning);
} }
+2 -10
View File
@@ -11,17 +11,9 @@ export default function AudioIndicator({ level, isTalking }) {
<div className="audio-indicator-container"> <div className="audio-indicator-container">
<div className="audio-indicator-label"> <div className="audio-indicator-label">
<span>{isTalking ? 'Votre micro' : 'Audio entrant'}</span> <span>{isTalking ? 'Votre micro' : 'Audio entrant'}</span>
<span className="audio-level-value">{Math.round(normalizedLevel)}%</span>
</div> </div>
<div className="audio-indicator-bar"> {/* VU-mètre barres */}
<div
className={`audio-indicator-fill ${isTalking ? 'talking' : ''}`}
style={{ width: `${normalizedLevel}%` }}
/>
</div>
{/* Bars VU-mètre style */}
<div className="audio-bars"> <div className="audio-bars">
{[...Array(20)].map((_, i) => { {[...Array(20)].map((_, i) => {
const threshold = (i + 1) * 5; const threshold = (i + 1) * 5;
@@ -34,7 +26,7 @@ export default function AudioIndicator({ level, isTalking }) {
key={i} key={i}
className={`audio-bar ${isActive ? 'active' : ''} ${ className={`audio-bar ${isActive ? 'active' : ''} ${
isActive && isDanger ? 'danger' : isActive && isWarning ? 'warning' : '' isActive && isDanger ? 'danger' : isActive && isWarning ? 'warning' : ''
}`} } ${isTalking ? 'talking' : ''}`}
/> />
); );
})} })}
+77 -16
View File
@@ -17,6 +17,11 @@ export default function useLiveKit() {
const animationFrameRef = useRef(null); const animationFrameRef = useRef(null);
const isAudioUnlockedRef = useRef(false); 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 * Connexion à la room LiveKit
*/ */
@@ -68,6 +73,9 @@ export default function useLiveKit() {
if (track.kind === Track.Kind.Audio) { if (track.kind === Track.Kind.Audio) {
const audioElement = track.attach(); const audioElement = track.attach();
document.body.appendChild(audioElement); document.body.appendChild(audioElement);
// Setup analyseur pour audio entrant
setupRemoteAudioAnalyser(track);
} }
}); });
@@ -92,6 +100,8 @@ export default function useLiveKit() {
// Mute par défaut (PTT) // Mute par défaut (PTT)
track.mute(); track.mute();
setupAudioAnalyser(track); setupAudioAnalyser(track);
// Démarrer l'analyse audio
analyseAudioLevel();
console.log('✓ Track audio configuré et muted pour PTT'); 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) => { const setupAudioAnalyser = (track) => {
try { try {
@@ -266,36 +276,63 @@ export default function useLiveKit() {
audioContextRef.current = audioContext; audioContextRef.current = audioContext;
analyserRef.current = analyser; analyserRef.current = analyser;
// Démarrer analyse console.log('✓ Analyseur audio local configuré');
analyseAudioLevel();
} catch (error) { } 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) * Analyser niveau audio (pour VU-mètre)
* Alterne entre micro local (si talking) et audio entrant (si listening)
*/ */
const analyseAudioLevel = () => { const analyseAudioLevel = useCallback(() => {
if (!analyserRef.current) return;
const analyser = analyserRef.current;
const dataArray = new Uint8Array(analyser.frequencyBinCount);
const analyse = () => { const analyse = () => {
analyser.getByteFrequencyData(dataArray); // Choisir l'analyseur selon l'état
const analyser = isTalking ? analyserRef.current : remoteAnalyserRef.current;
// Calculer moyenne if (analyser) {
const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length; const dataArray = new Uint8Array(analyser.frequencyBinCount);
const normalized = Math.min(100, (average / 255) * 100); 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); animationFrameRef.current = requestAnimationFrame(analyse);
}; };
analyse(); analyse();
}; }, [isTalking]);
/** /**
* Cleanup * Cleanup
@@ -306,15 +343,39 @@ export default function useLiveKit() {
animationFrameRef.current = null; animationFrameRef.current = null;
} }
if (remoteAnimationFrameRef.current) {
cancelAnimationFrame(remoteAnimationFrameRef.current);
remoteAnimationFrameRef.current = null;
}
if (audioContextRef.current) { if (audioContextRef.current) {
audioContextRef.current.close(); audioContextRef.current.close();
audioContextRef.current = null; audioContextRef.current = null;
} }
if (remoteAudioContextRef.current) {
remoteAudioContextRef.current.close();
remoteAudioContextRef.current = null;
}
analyserRef.current = null; analyserRef.current = null;
remoteAnalyserRef.current = null;
localTrackRef.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 // Cleanup au démontage
useEffect(() => { useEffect(() => {
return () => { return () => {