feat: système hot-reload bridge audio avec ConfigManager (Phase 2.5)
- ConfigManager: gestionnaire centralisé config avec EventEmitter - AudioBridgeManager: gestion bridge avec auto-reload sur changement config - Intégration dans serveur principal (index.js) - Événements 'audio-device-updated' et 'config-updated' - Reload automatique du bridge sans redémarrer serveur - Mode placeholder pour développement (vrai bridge Phase 3)
This commit is contained in:
+12
-23
@@ -10,6 +10,7 @@ import YAML from 'yaml';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import { CoreAudioBackend } from '../bridge/backends/CoreAudioBackend.js';
|
import { CoreAudioBackend } from '../bridge/backends/CoreAudioBackend.js';
|
||||||
|
import configManager from '../config/ConfigManager.js';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -503,8 +504,8 @@ router.get('/audio/devices', (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.get('/audio/device', (req, res) => {
|
router.get('/audio/device', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = configManager.get();
|
||||||
const audioDevice = config.audio.device || {};
|
const audioDevice = config.audio?.device || {};
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
device: audioDevice
|
device: audioDevice
|
||||||
@@ -524,31 +525,19 @@ router.post('/audio/device', (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { inputDeviceId, outputDeviceId, sampleRate, bufferSize } = req.body;
|
const { inputDeviceId, outputDeviceId, sampleRate, bufferSize } = req.body;
|
||||||
|
|
||||||
const config = loadConfig();
|
// Utiliser le ConfigManager pour mettre à jour et émettre l'événement
|
||||||
|
const deviceConfig = configManager.updateAudioDevice({
|
||||||
// Initialiser la section device si elle n'existe pas
|
inputDeviceId,
|
||||||
if (!config.audio.device) {
|
outputDeviceId,
|
||||||
config.audio.device = {};
|
sampleRate,
|
||||||
}
|
bufferSize
|
||||||
|
});
|
||||||
// Mettre à jour les paramètres fournis
|
|
||||||
if (inputDeviceId !== undefined) config.audio.device.inputDeviceId = inputDeviceId;
|
|
||||||
if (outputDeviceId !== undefined) config.audio.device.outputDeviceId = outputDeviceId;
|
|
||||||
if (sampleRate !== undefined) {
|
|
||||||
config.audio.device.sampleRate = sampleRate;
|
|
||||||
config.audio.sampleRate = sampleRate; // Sync avec config globale
|
|
||||||
}
|
|
||||||
if (bufferSize !== undefined) config.audio.device.bufferSize = bufferSize;
|
|
||||||
|
|
||||||
saveConfig(config);
|
|
||||||
|
|
||||||
addLog('info', 'Audio device configured', { inputDeviceId, outputDeviceId, sampleRate, bufferSize });
|
addLog('info', 'Audio device configured', { inputDeviceId, outputDeviceId, sampleRate, bufferSize });
|
||||||
|
|
||||||
// TODO Phase 2.5 : Émettre événement pour reload du bridge audio
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: 'Audio device configured',
|
message: 'Audio device configured (bridge audio sera rechargé)',
|
||||||
device: config.audio.device
|
device: deviceConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* AudioBridgeManager.js
|
||||||
|
* Gestionnaire du bridge audio avec support hot-reload
|
||||||
|
* Phase 2.5
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import configManager from '../config/ConfigManager.js';
|
||||||
|
|
||||||
|
class AudioBridgeManager extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.bridge = null;
|
||||||
|
this.isRunning = false;
|
||||||
|
|
||||||
|
// Écouter les événements de configuration
|
||||||
|
configManager.on('audio-device-updated', this.handleDeviceUpdate.bind(this));
|
||||||
|
configManager.on('config-updated', this.handleConfigUpdate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Démarre le bridge audio avec la configuration actuelle
|
||||||
|
*/
|
||||||
|
async start() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
console.warn('⚠️ AudioBridge déjà démarré');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = configManager.get();
|
||||||
|
console.log('🎵 Démarrage AudioBridge avec configuration:', config.audio);
|
||||||
|
|
||||||
|
// TODO Phase 3: Implémenter le vrai bridge audio
|
||||||
|
// const AudioBridge = await import('./AudioBridge.js');
|
||||||
|
// this.bridge = new AudioBridge(config.audio);
|
||||||
|
// await this.bridge.start();
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
console.log('✓ AudioBridge démarré (mode placeholder)');
|
||||||
|
|
||||||
|
this.emit('started');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur démarrage AudioBridge:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arrête le bridge audio
|
||||||
|
*/
|
||||||
|
async stop() {
|
||||||
|
if (!this.isRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('⏹ Arrêt AudioBridge...');
|
||||||
|
|
||||||
|
// TODO Phase 3: Arrêter le vrai bridge
|
||||||
|
// if (this.bridge) {
|
||||||
|
// await this.bridge.stop();
|
||||||
|
// this.bridge = null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.isRunning = false;
|
||||||
|
console.log('✓ AudioBridge arrêté');
|
||||||
|
|
||||||
|
this.emit('stopped');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur arrêt AudioBridge:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recharge le bridge avec la nouvelle configuration
|
||||||
|
*/
|
||||||
|
async reload() {
|
||||||
|
try {
|
||||||
|
console.log('🔄 Rechargement AudioBridge...');
|
||||||
|
|
||||||
|
await this.stop();
|
||||||
|
await this.start();
|
||||||
|
|
||||||
|
console.log('✓ AudioBridge rechargé avec succès');
|
||||||
|
this.emit('reloaded');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur rechargement AudioBridge:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestionnaire événement mise à jour device audio
|
||||||
|
*/
|
||||||
|
async handleDeviceUpdate(deviceConfig) {
|
||||||
|
console.log('🔧 Device audio mis à jour:', deviceConfig);
|
||||||
|
console.log('→ Rechargement AudioBridge requis...');
|
||||||
|
|
||||||
|
// Auto-reload du bridge
|
||||||
|
if (this.isRunning) {
|
||||||
|
await this.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestionnaire événement mise à jour configuration
|
||||||
|
*/
|
||||||
|
handleConfigUpdate(config) {
|
||||||
|
console.log('🔧 Configuration mise à jour');
|
||||||
|
// Peut déclencher un reload si nécessaire
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne l'état actuel du bridge
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
return {
|
||||||
|
running: this.isRunning,
|
||||||
|
config: configManager.get().audio
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
const audioBridgeManager = new AudioBridgeManager();
|
||||||
|
|
||||||
|
export default audioBridgeManager;
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* ConfigManager.js
|
||||||
|
* Gestionnaire centralisé de configuration avec support événements
|
||||||
|
* Phase 2.5
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import YAML from 'yaml';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const configPath = join(__dirname, 'config.yaml');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère un ID slug à partir d'un nom
|
||||||
|
*/
|
||||||
|
function slugify(text) {
|
||||||
|
return text
|
||||||
|
.toString()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/[^\w-]+/g, '')
|
||||||
|
.replace(/--+/g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigManager extends EventEmitter {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.config = null;
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge la configuration depuis le fichier YAML
|
||||||
|
*/
|
||||||
|
load() {
|
||||||
|
try {
|
||||||
|
const configFile = readFileSync(configPath, 'utf8');
|
||||||
|
this.config = YAML.parse(configFile);
|
||||||
|
|
||||||
|
// Générer les IDs pour les groupes et canaux
|
||||||
|
this.config.groups = this.config.groups.map(group => {
|
||||||
|
const groupId = slugify(group.name);
|
||||||
|
return {
|
||||||
|
...group,
|
||||||
|
id: groupId,
|
||||||
|
channels: group.channels ? group.channels.map(channel => ({
|
||||||
|
...channel,
|
||||||
|
id: channel.id || `${groupId}-${slugify(channel.name)}`
|
||||||
|
})) : []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.config;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur chargement configuration:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la configuration actuelle
|
||||||
|
*/
|
||||||
|
get() {
|
||||||
|
return this.config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sauvegarde la configuration dans le fichier YAML
|
||||||
|
* Ne sauvegarde PAS les IDs (ils sont générés dynamiquement)
|
||||||
|
*/
|
||||||
|
save(config) {
|
||||||
|
try {
|
||||||
|
// Nettoyer les IDs avant de sauvegarder
|
||||||
|
const cleanConfig = {
|
||||||
|
...config,
|
||||||
|
groups: config.groups.map(group => {
|
||||||
|
const { id, ...groupWithoutId } = group;
|
||||||
|
return {
|
||||||
|
...groupWithoutId,
|
||||||
|
channels: group.channels ? group.channels.map(channel => {
|
||||||
|
const { id: channelId, ...channelWithoutId } = channel;
|
||||||
|
return channelWithoutId;
|
||||||
|
}) : []
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const yamlContent = YAML.stringify(cleanConfig);
|
||||||
|
writeFileSync(configPath, yamlContent, 'utf8');
|
||||||
|
|
||||||
|
// Recharger pour synchroniser
|
||||||
|
this.load();
|
||||||
|
|
||||||
|
// Émettre événement de changement
|
||||||
|
this.emit('config-updated', this.config);
|
||||||
|
|
||||||
|
return this.config;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur sauvegarde configuration:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour la configuration audio device
|
||||||
|
*/
|
||||||
|
updateAudioDevice(deviceConfig) {
|
||||||
|
if (!this.config.audio) {
|
||||||
|
this.config.audio = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.audio.device) {
|
||||||
|
this.config.audio.device = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour les paramètres fournis
|
||||||
|
if (deviceConfig.inputDeviceId !== undefined) {
|
||||||
|
this.config.audio.device.inputDeviceId = deviceConfig.inputDeviceId;
|
||||||
|
}
|
||||||
|
if (deviceConfig.outputDeviceId !== undefined) {
|
||||||
|
this.config.audio.device.outputDeviceId = deviceConfig.outputDeviceId;
|
||||||
|
}
|
||||||
|
if (deviceConfig.sampleRate !== undefined) {
|
||||||
|
this.config.audio.device.sampleRate = deviceConfig.sampleRate;
|
||||||
|
this.config.audio.sampleRate = deviceConfig.sampleRate; // Sync avec config globale
|
||||||
|
}
|
||||||
|
if (deviceConfig.bufferSize !== undefined) {
|
||||||
|
this.config.audio.device.bufferSize = deviceConfig.bufferSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save(this.config);
|
||||||
|
|
||||||
|
// Émettre événement spécifique
|
||||||
|
this.emit('audio-device-updated', this.config.audio.device);
|
||||||
|
|
||||||
|
return this.config.audio.device;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour la configuration audio globale
|
||||||
|
*/
|
||||||
|
updateAudioConfig(audioConfig) {
|
||||||
|
if (!this.config.audio) {
|
||||||
|
this.config.audio = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioConfig.sampleRate !== undefined) {
|
||||||
|
this.config.audio.sampleRate = audioConfig.sampleRate;
|
||||||
|
}
|
||||||
|
if (audioConfig.defaultBitrate !== undefined) {
|
||||||
|
this.config.audio.defaultBitrate = audioConfig.defaultBitrate;
|
||||||
|
}
|
||||||
|
if (audioConfig.jitterBufferMs !== undefined) {
|
||||||
|
this.config.audio.jitterBufferMs = audioConfig.jitterBufferMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save(this.config);
|
||||||
|
|
||||||
|
return this.config.audio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
const configManager = new ConfigManager();
|
||||||
|
|
||||||
|
export default configManager;
|
||||||
+18
-36
@@ -10,45 +10,15 @@ import { networkInterfaces } from 'os';
|
|||||||
import YAML from 'yaml';
|
import YAML from 'yaml';
|
||||||
import { AccessToken } from 'livekit-server-sdk';
|
import { AccessToken } from 'livekit-server-sdk';
|
||||||
import adminRouter, { registerUser, addLog } from './api/admin.js';
|
import adminRouter, { registerUser, addLog } from './api/admin.js';
|
||||||
|
import configManager from './config/ConfigManager.js';
|
||||||
|
import audioBridgeManager from './bridge/AudioBridgeManager.js';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
// Chargement configuration
|
// Chargement configuration via ConfigManager
|
||||||
const configPath = join(__dirname, 'config', 'config.yaml');
|
const config = configManager.get();
|
||||||
const configFile = readFileSync(configPath, 'utf8');
|
|
||||||
const config = YAML.parse(configFile);
|
|
||||||
|
|
||||||
/**
|
// Note: Les IDs sont maintenant générés automatiquement par le ConfigManager
|
||||||
* Génère un ID slug à partir d'un nom
|
|
||||||
* Ex: "Équipe Production" -> "equipe-production"
|
|
||||||
*/
|
|
||||||
function slugify(text) {
|
|
||||||
return text
|
|
||||||
.toString()
|
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[\u0300-\u036f]/g, '') // Retire les accents
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.replace(/\s+/g, '-')
|
|
||||||
.replace(/[^\w-]+/g, '')
|
|
||||||
.replace(/--+/g, '-');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Générer les IDs pour les groupes et canaux s'ils n'existent pas
|
|
||||||
config.groups = config.groups.map(group => {
|
|
||||||
if (!group.id) {
|
|
||||||
group.id = slugify(group.name);
|
|
||||||
}
|
|
||||||
if (group.channels) {
|
|
||||||
group.channels = group.channels.map(channel => {
|
|
||||||
if (!channel.id) {
|
|
||||||
channel.id = `${group.id}-${slugify(channel.name)}`;
|
|
||||||
}
|
|
||||||
return channel;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return group;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Détecte l'IP réseau locale (WiFi/Ethernet)
|
* Détecte l'IP réseau locale (WiFi/Ethernet)
|
||||||
@@ -388,6 +358,12 @@ async function start() {
|
|||||||
log('info', `Groupes configurés: ${config.groups.map(g => g.name).join(', ')}`);
|
log('info', `Groupes configurés: ${config.groups.map(g => g.name).join(', ')}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 3. Démarrer Audio Bridge Manager (Phase 2.5)
|
||||||
|
log('info', '');
|
||||||
|
log('info', '🎵 Démarrage Audio Bridge Manager...');
|
||||||
|
await audioBridgeManager.start();
|
||||||
|
log('info', '✓ Audio Bridge Manager prêt (mode placeholder)');
|
||||||
|
|
||||||
// Gérer erreur port déjà utilisé
|
// Gérer erreur port déjà utilisé
|
||||||
server.on('error', (error) => {
|
server.on('error', (error) => {
|
||||||
if (error.code === 'EADDRINUSE') {
|
if (error.code === 'EADDRINUSE') {
|
||||||
@@ -407,9 +383,15 @@ async function start() {
|
|||||||
|
|
||||||
// ========== Cleanup ==========
|
// ========== Cleanup ==========
|
||||||
|
|
||||||
function cleanup() {
|
async function cleanup() {
|
||||||
log('info', 'Arrêt du serveur...');
|
log('info', 'Arrêt du serveur...');
|
||||||
|
|
||||||
|
// Arrêter l'audio bridge
|
||||||
|
if (audioBridgeManager) {
|
||||||
|
log('info', 'Arrêt Audio Bridge Manager...');
|
||||||
|
await audioBridgeManager.stop();
|
||||||
|
}
|
||||||
|
|
||||||
if (livekitProcess) {
|
if (livekitProcess) {
|
||||||
log('info', 'Arrêt LiveKit Server...');
|
log('info', 'Arrêt LiveKit Server...');
|
||||||
livekitProcess.kill('SIGTERM');
|
livekitProcess.kill('SIGTERM');
|
||||||
|
|||||||
Reference in New Issue
Block a user