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 { dirname } from 'path';
|
||||
import { CoreAudioBackend } from '../bridge/backends/CoreAudioBackend.js';
|
||||
import configManager from '../config/ConfigManager.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const router = Router();
|
||||
@@ -503,8 +504,8 @@ router.get('/audio/devices', (req, res) => {
|
||||
*/
|
||||
router.get('/audio/device', (req, res) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const audioDevice = config.audio.device || {};
|
||||
const config = configManager.get();
|
||||
const audioDevice = config.audio?.device || {};
|
||||
|
||||
res.json({
|
||||
device: audioDevice
|
||||
@@ -524,31 +525,19 @@ router.post('/audio/device', (req, res) => {
|
||||
try {
|
||||
const { inputDeviceId, outputDeviceId, sampleRate, bufferSize } = req.body;
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
// Initialiser la section device si elle n'existe pas
|
||||
if (!config.audio.device) {
|
||||
config.audio.device = {};
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Utiliser le ConfigManager pour mettre à jour et émettre l'événement
|
||||
const deviceConfig = configManager.updateAudioDevice({
|
||||
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({
|
||||
message: 'Audio device configured',
|
||||
device: config.audio.device
|
||||
message: 'Audio device configured (bridge audio sera rechargé)',
|
||||
device: deviceConfig
|
||||
});
|
||||
|
||||
} 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 { AccessToken } from 'livekit-server-sdk';
|
||||
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));
|
||||
|
||||
// Chargement configuration
|
||||
const configPath = join(__dirname, 'config', 'config.yaml');
|
||||
const configFile = readFileSync(configPath, 'utf8');
|
||||
const config = YAML.parse(configFile);
|
||||
// Chargement configuration via ConfigManager
|
||||
const config = configManager.get();
|
||||
|
||||
/**
|
||||
* 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;
|
||||
});
|
||||
// Note: Les IDs sont maintenant générés automatiquement par le ConfigManager
|
||||
|
||||
/**
|
||||
* 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(', ')}`);
|
||||
});
|
||||
|
||||
// 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é
|
||||
server.on('error', (error) => {
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
@@ -407,9 +383,15 @@ async function start() {
|
||||
|
||||
// ========== Cleanup ==========
|
||||
|
||||
function cleanup() {
|
||||
async function cleanup() {
|
||||
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) {
|
||||
log('info', 'Arrêt LiveKit Server...');
|
||||
livekitProcess.kill('SIGTERM');
|
||||
|
||||
Reference in New Issue
Block a user