fix: sécuriser startRecording, stopRecording et utiliser la contrainte de micro ideal

This commit is contained in:
Michael committed 2026-06-12 16:32:39 +02:00
1 parent dd31090d3f
commit e59bdd1feb
2 files changed
+108 -63

No files matched your search

+106 -63
View File
@@ -66,61 +66,127 @@ async function populateMicList() {
async function startRecording() {
const audioConstraints = { echoCancellation: true, noiseSuppression: true, autoGainControl: true };
const selectedMic = $('mic-select').value;
if (selectedMic) audioConstraints.deviceId = { exact: selectedMic };
const selectedMic = $('mic-select') ? $('mic-select').value : '';
if (selectedMic) audioConstraints.deviceId = { ideal: selectedMic };
try {
// 1. Obtenir le flux média (microphone)
state.mediaStream = await navigator.mediaDevices.getUserMedia({ audio: audioConstraints });
// Rafraîchir les étiquettes des micros une fois la permission accordée
await populateMicList();
// 2. Établir la connexion WebSocket
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
state.ws = new WebSocket(`${proto}://${location.host}/ws`);
state.ws.onopen = () => {
const titleVal = $('title') ? $('title').value : '';
const diarizationCb = $('diarization-cb') ? $('diarization-cb').checked : false;
state.ws.send(JSON.stringify({
type: 'start',
title: titleVal,
diarization: diarizationCb,
}));
};
state.ws.onmessage = onServerMessage;
state.ws.onerror = (err) => {
console.error('Erreur WebSocket :', err);
setStatus('error', 'Erreur Connexion');
};
state.ws.onclose = (e) => {
console.log(`WebSocket fermé : code=${e.code}, raison=${e.reason}`);
if (state.recording) {
stopRecording(true);
if (e.code !== 1000 && e.code !== 1001) {
setStatus('error', `Déconnexion (${e.code})`);
}
}
};
// 3. Initialiser l'audio
state.audioContext = new AudioContext();
await state.audioContext.audioWorklet.addModule('worklet.js');
const source = state.audioContext.createMediaStreamSource(state.mediaStream);
state.workletNode = new AudioWorkletNode(state.audioContext, 'pcm-downsampler');
state.workletNode.port.onmessage = (e) => queuePcm(new Int16Array(e.data));
source.connect(state.workletNode);
// 4. Mettre à jour l'interface
state.recording = true;
state.startedAt = Date.now();
state.timerInterval = setInterval(updateTimer, 500);
if ($('record-btn')) {
$('record-btn').textContent = '■ Arrêter';
$('record-btn').classList.add('recording');
}
if ($('title')) $('title').disabled = true;
setStatus('rec', 'Enregistrement…');
clearTranscript();
} catch (err) {
console.error("Impossible de démarrer l'enregistrement :", err);
// Nettoyage en cas d'échec
if (state.ws) {
try { state.ws.close(); } catch (e) {}
state.ws = null;
}
if (state.mediaStream) {
try { state.mediaStream.getTracks().forEach((t) => t.stop()); } catch (e) {}
state.mediaStream = null;
}
if (state.audioContext) {
try { state.audioContext.close(); } catch (e) {}
state.audioContext = null;
}
const causes = {
NotAllowedError: "Permission refusée. Autorisez le micro pour ce site (icône à gauche de l'adresse), et vérifiez que la page est servie en HTTPS.",
NotReadableError: "Le micro est inaccessible au niveau du système : fermez les applications qui l'utilisent (Teams, Discord...), vérifiez les paramètres de confidentialité micro de l'OS, et le périphérique d'entrée choisi par le navigateur.",
NotFoundError: "Aucun micro détecté. Branchez un micro ou choisissez le bon périphérique d'entrée.",
OverconstrainedError: "Le micro ne supporte pas les réglages demandés.",
};
alert("Impossible de démarrer le micro.\n\n" + (causes[err.name] || "") + "\n\n(" + err + ")");
return;
alert("Impossible de démarrer l'enregistrement.\n\n" + (causes[err.name] || err.message || err) + "\n\n(" + err + ")");
setStatus('idle', 'Prêt');
}
}
function stopRecording(abrupt = false) {
state.recording = false;
clearInterval(state.timerInterval);
if (state.workletNode) {
try { state.workletNode.disconnect(); } catch (e) {}
state.workletNode = null;
}
if (state.mediaStream) {
try { state.mediaStream.getTracks().forEach((t) => t.stop()); } catch (e) {}
state.mediaStream = null;
}
if (state.audioContext) {
try { state.audioContext.close(); } catch (e) {}
state.audioContext = null;
}
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
state.ws = new WebSocket(`${proto}://${location.host}/ws`);
state.ws.onopen = () => state.ws.send(JSON.stringify({
type: 'start',
title: $('title').value,
diarization: $('diarization-cb').checked,
}));
state.ws.onmessage = onServerMessage;
state.ws.onerror = (err) => {
console.error('Erreur WebSocket :', err);
setStatus('error', 'Erreur Connexion');
};
state.ws.onclose = (e) => {
console.log(`WebSocket fermé : code=${e.code}, raison=${e.reason}`);
if (state.recording) {
stopRecording(true);
if (e.code !== 1000 && e.code !== 1001) {
setStatus('error', `Déconnexion (${e.code})`);
}
}
};
if (!abrupt && state.ws && state.ws.readyState === WebSocket.OPEN) {
flushPcm();
try { state.ws.send(JSON.stringify({ type: 'stop' })); } catch (e) {}
setStatus('busy', 'Finalisation…');
} else {
setStatus('idle', 'Prêt');
}
state.audioContext = new AudioContext();
await state.audioContext.audioWorklet.addModule('worklet.js');
const source = state.audioContext.createMediaStreamSource(state.mediaStream);
state.workletNode = new AudioWorkletNode(state.audioContext, 'pcm-downsampler');
state.workletNode.port.onmessage = (e) => queuePcm(new Int16Array(e.data));
source.connect(state.workletNode);
state.recording = true;
state.startedAt = Date.now();
state.timerInterval = setInterval(updateTimer, 500);
$('record-btn').textContent = '■ Arrêter';
$('record-btn').classList.add('recording');
$('title').disabled = true;
setStatus('rec', 'Enregistrement…');
clearTranscript();
if ($('record-btn')) {
$('record-btn').textContent = '● Démarrer';
$('record-btn').classList.remove('recording');
}
if ($('title')) $('title').disabled = false;
// Toujours recharger les réunions pour synchroniser la liste (cas d'arrêt abrupt ou échec)
loadMeetings();
}
function queuePcm(samples) {
@@ -140,29 +206,6 @@ function flushPcm() {
if (state.ws && state.ws.readyState === WebSocket.OPEN) state.ws.send(out.buffer);
}
function stopRecording(abrupt = false) {
state.recording = false;
clearInterval(state.timerInterval);
if (state.workletNode) state.workletNode.disconnect();
if (state.mediaStream) state.mediaStream.getTracks().forEach((t) => t.stop());
if (state.audioContext) state.audioContext.close();
if (!abrupt && state.ws && state.ws.readyState === WebSocket.OPEN) {
flushPcm();
state.ws.send(JSON.stringify({ type: 'stop' }));
setStatus('busy', 'Finalisation…');
} else {
setStatus('idle', 'Prêt');
}
$('record-btn').textContent = '● Démarrer';
$('record-btn').classList.remove('recording');
$('title').disabled = false;
// Toujours recharger les réunions pour synchroniser la liste (cas d'arrêt abrupt ou échec)
loadMeetings();
}
function onServerMessage(event) {
const msg = JSON.parse(event.data);
if (msg.type === 'ready') {
+2
View File
@@ -32,4 +32,6 @@ Vérification de la cohérence de tous les fichiers (backend et frontend) et tes
- Correction de l'affichage de la liste des micros : demande initiale d'autorisation au chargement de la page et filtrage des périphériques sans identifiant.
- Ajout de la variable d'environnement DIARIZATION_MAX_SPEAKERS (défaut 8) et intégration dans l'instanciation de SpeakerDiarizer.
- Résolution des problèmes de synchronisation de la liste des réunions (rechargement systématique dans stopRecording), détection des contextes non sécurisés (affichage d'un bandeau d'erreur si HTTP) et gestion des erreurs de connexion WebSocket.
- Sécurisation globale de startRecording() et stopRecording() (bloc try/catch global, nettoyage complet et guards pour éléments null) et changement de contrainte de micro de 'exact' à 'ideal' pour éviter de bloquer la captation.