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 {
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);
}
+2 -10
View File
@@ -11,17 +11,9 @@ export default function AudioIndicator({ level, isTalking }) {
<div className="audio-indicator-container">
<div className="audio-indicator-label">
<span>{isTalking ? 'Votre micro' : 'Audio entrant'}</span>
<span className="audio-level-value">{Math.round(normalizedLevel)}%</span>
</div>
<div className="audio-indicator-bar">
<div
className={`audio-indicator-fill ${isTalking ? 'talking' : ''}`}
style={{ width: `${normalizedLevel}%` }}
/>
</div>
{/* Bars VU-mètre style */}
{/* VU-mètre barres */}
<div className="audio-bars">
{[...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' : ''}`}
/>
);
})}
+72 -11
View File
@@ -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,23 +276,47 @@ 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 = () => {
// Choisir l'analyseur selon l'état
const analyser = isTalking ? analyserRef.current : remoteAnalyserRef.current;
if (analyser) {
const dataArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(dataArray);
// Calculer moyenne
@@ -290,12 +324,15 @@ export default function useLiveKit() {
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 () => {