bf960f49bb
Le paradigme devient : pour brancher un canal physique sur un groupe, créer un server audio user. Les matrices sont retirées de l'UI Electron, de l'admin PWA, de l'API REST et du backend (GroupAudioRouter supprimé). AudioBridgeManager ne génère plus de tokens per-group. Option "aucune sortie" ajoutée pour les server audio users.
711 lines
20 KiB
JavaScript
711 lines
20 KiB
JavaScript
/**
|
|
* API Admin - Gestion groupes, utilisateurs, monitoring
|
|
* Phase 2.3
|
|
*/
|
|
|
|
import { Router } from 'express';
|
|
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();
|
|
|
|
/**
|
|
* 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, '-');
|
|
}
|
|
|
|
// État en mémoire des utilisateurs connectés
|
|
const connectedUsers = new Map(); // identity -> { username, groupId, roomName, connectedAt, lastActivity }
|
|
|
|
// Stats monitoring
|
|
const stats = {
|
|
totalConnections: 0,
|
|
activeConnections: 0,
|
|
audioStats: [],
|
|
logs: []
|
|
};
|
|
|
|
/**
|
|
* Ajoute un log au système
|
|
*/
|
|
export function addLog(level, message, meta = {}) {
|
|
const log = {
|
|
timestamp: new Date().toISOString(),
|
|
level,
|
|
message,
|
|
meta
|
|
};
|
|
|
|
stats.logs.unshift(log);
|
|
|
|
// Garder max 1000 logs en mémoire
|
|
if (stats.logs.length > 1000) {
|
|
stats.logs = stats.logs.slice(0, 1000);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enregistre une connexion utilisateur
|
|
*/
|
|
export function registerUser(identity, username, groupId, roomName) {
|
|
connectedUsers.set(identity, {
|
|
username,
|
|
groupId,
|
|
roomName,
|
|
connectedAt: new Date().toISOString(),
|
|
lastActivity: new Date().toISOString()
|
|
});
|
|
|
|
stats.totalConnections++;
|
|
stats.activeConnections = connectedUsers.size;
|
|
|
|
addLog('info', `User connected: ${username}`, { groupId, identity });
|
|
}
|
|
|
|
/**
|
|
* Déconnecte un utilisateur
|
|
*/
|
|
export function unregisterUser(identity) {
|
|
const user = connectedUsers.get(identity);
|
|
if (user) {
|
|
connectedUsers.delete(identity);
|
|
stats.activeConnections = connectedUsers.size;
|
|
addLog('info', `User disconnected: ${user.username}`, { groupId: user.groupId, identity });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Met à jour l'activité d'un utilisateur
|
|
*/
|
|
export function updateUserActivity(identity) {
|
|
const user = connectedUsers.get(identity);
|
|
if (user) {
|
|
user.lastActivity = new Date().toISOString();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ajoute des statistiques audio
|
|
*/
|
|
export function addAudioStats(data) {
|
|
const stat = {
|
|
timestamp: new Date().toISOString(),
|
|
...data
|
|
};
|
|
|
|
stats.audioStats.unshift(stat);
|
|
|
|
// Garder max 100 stats
|
|
if (stats.audioStats.length > 100) {
|
|
stats.audioStats = stats.audioStats.slice(0, 100);
|
|
}
|
|
}
|
|
|
|
// ========== Routes Admin ==========
|
|
|
|
/**
|
|
* GET /admin/groups
|
|
* Liste tous les groupes avec détails
|
|
*/
|
|
router.get('/groups', (req, res) => {
|
|
try {
|
|
const config = configManager.get();
|
|
res.json({
|
|
groups: config.groups
|
|
});
|
|
} catch (error) {
|
|
console.error('Erreur GET /admin/groups:', error);
|
|
res.status(500).json({ error: 'Failed to load groups' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /admin/groups
|
|
* Crée un nouveau groupe
|
|
* Body: { name, audioBitrate? }
|
|
* L'ID est généré automatiquement à partir du nom
|
|
*/
|
|
router.post('/groups', (req, res) => {
|
|
try {
|
|
const { name, audioBitrate } = req.body;
|
|
|
|
if (!name) {
|
|
return res.status(400).json({
|
|
error: 'Missing required field: name'
|
|
});
|
|
}
|
|
|
|
const config = configManager.get();
|
|
|
|
// Générer l'ID à partir du nom
|
|
const id = slugify(name);
|
|
|
|
// Vérifier que l'ID n'existe pas déjà
|
|
if (config.groups.find(g => g.id === id)) {
|
|
return res.status(409).json({
|
|
error: `Group "${name}" already exists (ID: ${id})`
|
|
});
|
|
}
|
|
|
|
// Créer le nouveau groupe
|
|
const newGroup = {
|
|
name,
|
|
...(audioBitrate && { audioBitrate })
|
|
};
|
|
|
|
config.groups.push(newGroup);
|
|
configManager.save(config);
|
|
|
|
addLog('info', `Group created: ${name}`, { id });
|
|
|
|
res.status(201).json({
|
|
message: 'Group created',
|
|
group: { ...newGroup, id }
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Erreur POST /admin/groups:', error);
|
|
res.status(500).json({ error: 'Failed to create group' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /admin/groups/:id
|
|
* Modifie un groupe existant
|
|
* Body: { name?, audioBitrate? }
|
|
* Note: l'ID est un slug généré, on cherche le groupe par nom dans le YAML
|
|
*/
|
|
router.put('/groups/:id', (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { name, audioBitrate } = req.body;
|
|
|
|
const config = configManager.get();
|
|
|
|
// Chercher le groupe par son nom (qui correspond à l'ID slugifié)
|
|
const groupIndex = config.groups.findIndex(g => slugify(g.name) === id);
|
|
|
|
if (groupIndex === -1) {
|
|
return res.status(404).json({
|
|
error: `Group ${id} not found`
|
|
});
|
|
}
|
|
|
|
// Mettre à jour les champs fournis
|
|
if (name !== undefined) config.groups[groupIndex].name = name;
|
|
if (audioBitrate !== undefined) config.groups[groupIndex].audioBitrate = audioBitrate;
|
|
|
|
configManager.save(config);
|
|
|
|
addLog('info', `Group updated: ${config.groups[groupIndex].name}`, { id });
|
|
|
|
// Récupérer la config à jour avec les IDs générés
|
|
const updatedConfig = configManager.get();
|
|
const updatedGroupIndex = updatedConfig.groups.findIndex(g => slugify(g.name) === id || slugify(g.name) === slugify(name));
|
|
const updatedGroup = updatedGroupIndex !== -1 ? updatedConfig.groups[updatedGroupIndex] : null;
|
|
|
|
res.json({
|
|
message: 'Group updated',
|
|
group: updatedGroup
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Erreur PUT /admin/groups:', error);
|
|
res.status(500).json({ error: 'Failed to update group' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /admin/groups/:id
|
|
* Supprime un groupe
|
|
* Note: l'ID est un slug généré, on cherche le groupe par nom dans le YAML
|
|
*/
|
|
router.delete('/groups/:id', (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const config = configManager.get();
|
|
const groupIndex = config.groups.findIndex(g => slugify(g.name) === id);
|
|
|
|
if (groupIndex === -1) {
|
|
return res.status(404).json({
|
|
error: `Group ${id} not found`
|
|
});
|
|
}
|
|
|
|
const groupName = config.groups[groupIndex].name;
|
|
config.groups.splice(groupIndex, 1);
|
|
configManager.save(config);
|
|
|
|
addLog('info', `Group deleted: ${groupName}`, { id });
|
|
|
|
res.json({
|
|
message: 'Group deleted',
|
|
id
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Erreur DELETE /admin/groups:', error);
|
|
res.status(500).json({ error: 'Failed to delete group' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /admin/users
|
|
* Liste tous les utilisateurs connectés
|
|
*/
|
|
router.get('/users', (req, res) => {
|
|
try {
|
|
const users = Array.from(connectedUsers.entries()).map(([identity, data]) => ({
|
|
identity,
|
|
...data
|
|
}));
|
|
|
|
res.json({
|
|
users,
|
|
count: users.length
|
|
});
|
|
} catch (error) {
|
|
console.error('Erreur GET /admin/users:', error);
|
|
res.status(500).json({ error: 'Failed to load users' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* DELETE /admin/users/:identity
|
|
* Déconnecte un utilisateur (force disconnect)
|
|
*/
|
|
router.delete('/users/:identity', (req, res) => {
|
|
try {
|
|
const { identity } = req.params;
|
|
|
|
const user = connectedUsers.get(identity);
|
|
if (!user) {
|
|
return res.status(404).json({
|
|
error: `User ${identity} not found`
|
|
});
|
|
}
|
|
|
|
unregisterUser(identity);
|
|
addLog('warn', `User force disconnected: ${user.username}`, { identity });
|
|
|
|
res.json({
|
|
message: 'User disconnected',
|
|
identity
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Erreur DELETE /admin/users:', error);
|
|
res.status(500).json({ error: 'Failed to disconnect user' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /admin/stats
|
|
* Statistiques temps réel
|
|
*/
|
|
router.get('/stats', (req, res) => {
|
|
try {
|
|
res.json({
|
|
totalConnections: stats.totalConnections,
|
|
activeConnections: stats.activeConnections,
|
|
audioStats: stats.audioStats.slice(0, 20), // 20 dernières stats
|
|
uptime: process.uptime(),
|
|
memory: process.memoryUsage()
|
|
});
|
|
} catch (error) {
|
|
console.error('Erreur GET /admin/stats:', error);
|
|
res.status(500).json({ error: 'Failed to load stats' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /admin/logs
|
|
* Logs serveur
|
|
* Query params: ?limit=100&level=info
|
|
*/
|
|
router.get('/logs', (req, res) => {
|
|
try {
|
|
const limit = parseInt(req.query.limit) || 100;
|
|
const level = req.query.level;
|
|
|
|
let logs = stats.logs;
|
|
|
|
// Filtrer par niveau si spécifié
|
|
if (level) {
|
|
logs = logs.filter(log => log.level === level);
|
|
}
|
|
|
|
// Limiter le nombre
|
|
logs = logs.slice(0, limit);
|
|
|
|
res.json({
|
|
logs,
|
|
total: stats.logs.length
|
|
});
|
|
} catch (error) {
|
|
console.error('Erreur GET /admin/logs:', error);
|
|
res.status(500).json({ error: 'Failed to load logs' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /admin/config
|
|
* Configuration serveur complète
|
|
*/
|
|
router.get('/config', (req, res) => {
|
|
try {
|
|
const config = configManager.get();
|
|
res.json(config);
|
|
} catch (error) {
|
|
console.error('Erreur GET /admin/config:', error);
|
|
res.status(500).json({ error: 'Failed to load config' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /admin/config/audio
|
|
* Met à jour la configuration audio globale
|
|
* Body: { sampleRate?, defaultBitrate?, jitterBufferMs? }
|
|
*/
|
|
router.put('/config/audio', (req, res) => {
|
|
try {
|
|
const { sampleRate, defaultBitrate, jitterBufferMs } = req.body;
|
|
|
|
const config = configManager.get();
|
|
|
|
if (sampleRate !== undefined) config.audio.sampleRate = sampleRate;
|
|
if (defaultBitrate !== undefined) config.audio.defaultBitrate = defaultBitrate;
|
|
if (jitterBufferMs !== undefined) config.audio.jitterBufferMs = jitterBufferMs;
|
|
|
|
configManager.save(config);
|
|
|
|
addLog('info', 'Audio config updated', { sampleRate, defaultBitrate, jitterBufferMs });
|
|
|
|
res.json({
|
|
message: 'Audio config updated',
|
|
audio: config.audio
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Erreur PUT /admin/config/audio:', error);
|
|
res.status(500).json({ error: 'Failed to update audio config' });
|
|
}
|
|
});
|
|
|
|
// ========== Routes Audio Devices (Phase 2.5) ==========
|
|
|
|
/**
|
|
* GET /admin/audio/devices
|
|
* Énumération de toutes les cartes son disponibles
|
|
*/
|
|
router.get('/audio/devices', (req, res) => {
|
|
try {
|
|
const devices = CoreAudioBackend.getDevices();
|
|
const defaultInput = CoreAudioBackend.getDefaultInputDevice();
|
|
const defaultOutput = CoreAudioBackend.getDefaultOutputDevice();
|
|
|
|
res.json({
|
|
devices,
|
|
defaultInput,
|
|
defaultOutput
|
|
});
|
|
} catch (error) {
|
|
console.error('Erreur GET /admin/audio/devices:', error);
|
|
res.status(500).json({ error: 'Failed to enumerate audio devices' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /admin/audio/device
|
|
* Récupère la configuration actuelle de la carte son sélectionnée
|
|
*/
|
|
router.get('/audio/device', (req, res) => {
|
|
try {
|
|
const config = configManager.get();
|
|
const audioDevice = config.audio?.device || {};
|
|
|
|
// Enrichir avec les infos réelles de la carte si configurée
|
|
const devices = CoreAudioBackend.getDevices();
|
|
let deviceInfo = { ...audioDevice };
|
|
|
|
if (audioDevice.inputDeviceId) {
|
|
const inputDev = devices.find(d => d.id === audioDevice.inputDeviceId);
|
|
if (inputDev) {
|
|
deviceInfo.inputChannels = inputDev.maxInputChannels;
|
|
deviceInfo.inputDeviceName = inputDev.name;
|
|
}
|
|
}
|
|
|
|
if (audioDevice.outputDeviceId) {
|
|
const outputDev = devices.find(d => d.id === audioDevice.outputDeviceId);
|
|
if (outputDev) {
|
|
deviceInfo.outputChannels = outputDev.maxOutputChannels;
|
|
deviceInfo.outputDeviceName = outputDev.name;
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
device: deviceInfo
|
|
});
|
|
} catch (error) {
|
|
console.error('Erreur GET /admin/audio/device:', error);
|
|
res.status(500).json({ error: 'Failed to load audio device config' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /admin/audio/channels/names
|
|
* Récupère les noms personnalisés des canaux physiques
|
|
*/
|
|
router.get('/audio/channels/names', (req, res) => {
|
|
try {
|
|
const config = configManager.get();
|
|
const channelNames = config.audio?.channelNames || { inputs: {}, outputs: {} };
|
|
|
|
res.json({
|
|
channelNames
|
|
});
|
|
} catch (error) {
|
|
console.error('Erreur GET /admin/audio/channels/names:', error);
|
|
res.status(500).json({ error: 'Failed to load channel names' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PUT /admin/audio/channels/names
|
|
* Sauvegarde les noms personnalisés des canaux physiques
|
|
* Body: { inputs: { "0": "Micro Principal", ... }, outputs: { "0": "Retour Scène", ... } }
|
|
*/
|
|
router.put('/audio/channels/names', (req, res) => {
|
|
try {
|
|
const { inputs, outputs } = req.body;
|
|
|
|
if (!inputs && !outputs) {
|
|
return res.status(400).json({
|
|
error: 'Missing required fields: inputs or outputs'
|
|
});
|
|
}
|
|
|
|
const config = configManager.get();
|
|
|
|
if (!config.audio.channelNames) {
|
|
config.audio.channelNames = { inputs: {}, outputs: {} };
|
|
}
|
|
|
|
if (inputs) {
|
|
config.audio.channelNames.inputs = inputs;
|
|
}
|
|
|
|
if (outputs) {
|
|
config.audio.channelNames.outputs = outputs;
|
|
}
|
|
|
|
configManager.save(config);
|
|
|
|
addLog('info', 'Channel names updated', { inputCount: Object.keys(inputs || {}).length, outputCount: Object.keys(outputs || {}).length });
|
|
|
|
res.json({
|
|
message: 'Channel names updated',
|
|
channelNames: config.audio.channelNames
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Erreur PUT /admin/audio/channels/names:', error);
|
|
res.status(500).json({ error: 'Failed to update channel names' });
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* POST /admin/audio/device
|
|
* Sélectionne et configure une carte son
|
|
* Body: { inputDeviceId?, outputDeviceId?, sampleRate?, bufferSize? }
|
|
*/
|
|
router.post('/audio/device', (req, res) => {
|
|
try {
|
|
const { inputDeviceId, outputDeviceId, sampleRate, bufferSize } = req.body;
|
|
|
|
// 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 });
|
|
|
|
res.json({
|
|
message: 'Audio device configured (bridge audio sera rechargé)',
|
|
device: deviceConfig
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Erreur POST /admin/audio/device:', error);
|
|
res.status(500).json({ error: 'Failed to configure audio device' });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /admin/devices/list
|
|
* Liste tous les devices audio disponibles (auto-détection)
|
|
* Supporte macOS (CoreAudio), Linux (JACK/PipeWire), Windows (WASAPI)
|
|
*/
|
|
router.get('/devices/list', async (req, res) => {
|
|
try {
|
|
const devices = {
|
|
inputs: [],
|
|
outputs: [],
|
|
platform: process.platform
|
|
};
|
|
|
|
// Détection selon la plateforme
|
|
if (process.platform === 'darwin') {
|
|
// macOS : utiliser CoreAudioBackend.getDevices()
|
|
try {
|
|
const coreAudioDevices = CoreAudioBackend.getDevices();
|
|
|
|
// Séparer inputs et outputs
|
|
coreAudioDevices.forEach(device => {
|
|
if (device.maxInputChannels > 0) {
|
|
devices.inputs.push({
|
|
id: device.name, // Utiliser le nom comme ID (compatible avec inputDeviceName)
|
|
name: device.name,
|
|
channels: device.maxInputChannels,
|
|
sampleRate: device.defaultSampleRate,
|
|
isDefault: device.isDefault?.input || false
|
|
});
|
|
}
|
|
|
|
if (device.maxOutputChannels > 0) {
|
|
devices.outputs.push({
|
|
id: device.name, // Utiliser le nom comme ID (compatible avec outputDeviceName)
|
|
name: device.name,
|
|
channels: device.maxOutputChannels,
|
|
sampleRate: device.defaultSampleRate,
|
|
isDefault: device.isDefault?.output || false
|
|
});
|
|
}
|
|
});
|
|
|
|
// Fallback si aucun device trouvé
|
|
if (devices.inputs.length === 0) {
|
|
devices.inputs.push({ id: 'builtin-mic', name: 'Built-in Microphone', isDefault: true });
|
|
}
|
|
if (devices.outputs.length === 0) {
|
|
devices.outputs.push({ id: 'builtin-output', name: 'Built-in Output', isDefault: true });
|
|
}
|
|
} catch (error) {
|
|
console.warn('⚠️ Détection CoreAudio échouée:', error.message);
|
|
|
|
// Fallback : devices par défaut macOS
|
|
devices.inputs.push({ id: 'builtin-mic', name: 'Built-in Microphone', isDefault: true });
|
|
devices.outputs.push({ id: 'builtin-output', name: 'Built-in Output', isDefault: true });
|
|
}
|
|
|
|
} else if (process.platform === 'linux') {
|
|
// Linux : JACK ou PipeWire
|
|
const { exec } = await import('child_process');
|
|
const { promisify } = await import('util');
|
|
const execPromise = promisify(exec);
|
|
|
|
try {
|
|
// Essayer JACK d'abord
|
|
const { stdout: jackPorts } = await execPromise('jack_lsp 2>/dev/null || echo ""');
|
|
|
|
if (jackPorts.trim()) {
|
|
// Parser les ports JACK
|
|
const ports = jackPorts.split('\n').filter(Boolean);
|
|
|
|
ports.forEach(port => {
|
|
if (port.includes('capture')) {
|
|
devices.inputs.push({ id: port, name: port });
|
|
} else if (port.includes('playback')) {
|
|
devices.outputs.push({ id: port, name: port });
|
|
}
|
|
});
|
|
} else {
|
|
// Fallback : PipeWire/PulseAudio via pactl
|
|
const { stdout: paDevices } = await execPromise('pactl list short sources 2>/dev/null || echo ""');
|
|
const { stdout: paSinks } = await execPromise('pactl list short sinks 2>/dev/null || echo ""');
|
|
|
|
// Helper pour obtenir une description lisible
|
|
const getDeviceDescription = (deviceId) => {
|
|
// Extraire une description plus lisible du nom technique
|
|
if (deviceId.includes('alsa_input')) return deviceId.replace('alsa_input.', 'Input: ');
|
|
if (deviceId.includes('alsa_output')) return deviceId.replace('alsa_output.', 'Output: ');
|
|
return deviceId;
|
|
};
|
|
|
|
if (paDevices.trim()) {
|
|
paDevices.split('\n').filter(Boolean).forEach((line) => {
|
|
const parts = line.split('\t');
|
|
const deviceId = parts[1]; // Nom du device (ex: alsa_input.pci-...)
|
|
if (deviceId && !deviceId.includes('.monitor')) { // Ignorer les monitors
|
|
devices.inputs.push({
|
|
id: deviceId,
|
|
name: getDeviceDescription(deviceId)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
if (paSinks.trim()) {
|
|
paSinks.split('\n').filter(Boolean).forEach((line) => {
|
|
const parts = line.split('\t');
|
|
const deviceId = parts[1]; // Nom du device (ex: alsa_output.pci-...)
|
|
if (deviceId) {
|
|
devices.outputs.push({
|
|
id: deviceId,
|
|
name: getDeviceDescription(deviceId)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} catch (linuxError) {
|
|
console.warn('⚠️ Détection devices Linux échouée:', linuxError.message);
|
|
devices.inputs.push({ id: 0, name: 'Default Input', isDefault: true });
|
|
devices.outputs.push({ id: 0, name: 'Default Output', isDefault: true });
|
|
}
|
|
|
|
} else if (process.platform === 'win32') {
|
|
// Windows : WASAPI (Phase 3)
|
|
// TODO: implémenter détection WASAPI
|
|
devices.inputs.push({ id: 0, name: 'Default Input (Windows)', isDefault: true });
|
|
devices.outputs.push({ id: 0, name: 'Default Output (Windows)', isDefault: true });
|
|
}
|
|
|
|
addLog('info', 'Audio devices listed', {
|
|
inputsCount: devices.inputs.length,
|
|
outputsCount: devices.outputs.length
|
|
});
|
|
|
|
res.json(devices);
|
|
|
|
} catch (error) {
|
|
console.error('Erreur GET /admin/devices/list:', error);
|
|
res.status(500).json({
|
|
error: 'Failed to list audio devices',
|
|
message: error.message,
|
|
platform: process.platform
|
|
});
|
|
}
|
|
});
|
|
|
|
export default router;
|