diff --git a/electron/main.js b/electron/main.js index 4fade6c..b4ca903 100644 --- a/electron/main.js +++ b/electron/main.js @@ -454,7 +454,13 @@ app.whenReady().then(async () => { if (users.find(u => u.name === name)) { return { success: false, error: `Un utilisateur "${name}" existe déjà` }; } - const user = { name, group, input_channel: parseInt(input_channel), output_channel: output_channel !== null && output_channel !== '' ? parseInt(output_channel) : null }; + const parsedInput = input_channel !== null && input_channel !== undefined ? parseInt(input_channel) : null; + const user = { + name, + group, + input_channel: parsedInput, + output_channel: output_channel !== null && output_channel !== '' ? parseInt(output_channel) : null + }; config.server_audio_users = [...users, user]; writeConfig(config); return { success: true, user }; @@ -469,7 +475,13 @@ app.whenReady().then(async () => { const users = config.server_audio_users || []; const idx = users.findIndex(u => u.name === name); if (idx === -1) return { success: false, error: `Utilisateur "${name}" introuvable` }; - config.server_audio_users[idx] = { name, group, input_channel: parseInt(input_channel), output_channel: output_channel !== null && output_channel !== '' ? parseInt(output_channel) : null }; + const parsedInput = input_channel !== null && input_channel !== undefined ? parseInt(input_channel) : null; + config.server_audio_users[idx] = { + name, + group, + input_channel: parsedInput, + output_channel: output_channel !== null && output_channel !== '' ? parseInt(output_channel) : null + }; writeConfig(config); return { success: true }; } catch (error) { diff --git a/electron/ui/app.js b/electron/ui/app.js index 49c96a5..98c5262 100644 --- a/electron/ui/app.js +++ b/electron/ui/app.js @@ -803,7 +803,7 @@ document.addEventListener('DOMContentLoaded', () => { const action = btn.dataset.sauAction; const name = btn.dataset.sauName; if (action === 'edit') { - await editServerAudioUser(name, btn.dataset.sauGroup, parseInt(btn.dataset.sauInput), parseInt(btn.dataset.sauOutput)); + await editServerAudioUser(name, btn.dataset.sauGroup, btn.dataset.sauInput, btn.dataset.sauOutput); } else if (action === 'delete') { await deleteServerAudioUser(name); } @@ -874,9 +874,7 @@ function buildChannelOptions(dir) { label: names[i] ? `Ch ${i}: ${names[i]}` : `Ch ${i}` })); - if (dir === 'output') { - opts.unshift({ value: '', label: 'Aucune sortie' }); - } + opts.unshift({ value: '', label: dir === 'input' ? 'Aucune entrée' : 'Aucune sortie' }); return opts; } @@ -890,8 +888,8 @@ async function addServerAudioUser() { const outOpts = buildChannelOptions('output'); const inputField = inOpts - ? { name: 'input_channel', label: 'Canal d\'entrée', type: 'select', options: inOpts, default: '0' } - : { name: 'input_channel', label: 'Canal entrée (index)', type: 'number', default: 0, min: 0, max: 63 }; + ? { name: 'input_channel', label: 'Canal d\'entrée', type: 'select', options: inOpts, default: '' } + : { name: 'input_channel', label: 'Canal entrée (index, vide = aucune)', type: 'number', default: '', min: 0, max: 63 }; const outputField = outOpts ? { name: 'output_channel', label: 'Canal de sortie', type: 'select', options: outOpts, default: '' } : { name: 'output_channel', label: 'Canal sortie (index, vide = aucune)', type: 'number', default: '', min: 0, max: 63 }; @@ -912,7 +910,7 @@ async function addServerAudioUser() { const res = await window.electronAPI.serverAudioUsers.create({ name: result.name.trim(), group: result.group, - input_channel: parseInt(result.input_channel), + input_channel: result.input_channel !== '' ? parseInt(result.input_channel) : null, output_channel: result.output_channel !== '' ? parseInt(result.output_channel) : null }); @@ -931,10 +929,11 @@ async function editServerAudioUser(name, group, input_channel, output_channel) { const inOpts = buildChannelOptions('input'); const outOpts = buildChannelOptions('output'); + const inputDefault = input_channel !== null && input_channel !== undefined && input_channel !== 'null' ? String(input_channel) : ''; const inputField = inOpts - ? { name: 'input_channel', label: 'Canal d\'entrée', type: 'select', options: inOpts, default: String(input_channel) } - : { name: 'input_channel', label: 'Canal entrée (index)', type: 'number', default: input_channel, min: 0, max: 63 }; - const outputDefault = output_channel !== null && output_channel !== undefined ? String(output_channel) : ''; + ? { name: 'input_channel', label: 'Canal d\'entrée', type: 'select', options: inOpts, default: inputDefault } + : { name: 'input_channel', label: 'Canal entrée (index, vide = aucune)', type: 'number', default: inputDefault, min: 0, max: 63 }; + const outputDefault = output_channel !== null && output_channel !== undefined && output_channel !== 'null' ? String(output_channel) : ''; const outputField = outOpts ? { name: 'output_channel', label: 'Canal de sortie', type: 'select', options: outOpts, default: outputDefault } : { name: 'output_channel', label: 'Canal sortie (index, vide = aucune)', type: 'number', default: outputDefault, min: 0, max: 63 }; @@ -954,7 +953,7 @@ async function editServerAudioUser(name, group, input_channel, output_channel) { const res = await window.electronAPI.serverAudioUsers.update({ name, group: result.group, - input_channel: parseInt(result.input_channel), + input_channel: result.input_channel !== '' ? parseInt(result.input_channel) : null, output_channel: result.output_channel !== '' ? parseInt(result.output_channel) : null }); @@ -1118,6 +1117,15 @@ function showModal({ title, fields = [], confirmLabel = 'Confirmer', confirmClas `; } + if (field.type === 'checkbox') { + return ` +
+ +
`; + } return `
@@ -1155,7 +1163,8 @@ function showModal({ title, fields = [], confirmLabel = 'Confirmer', confirmClas const result = {}; fields.forEach(f => { const input = document.getElementById(`modal-field-${f.name}`); - result[f.name] = input ? input.value : ''; + if (!input) { result[f.name] = ''; return; } + result[f.name] = f.type === 'checkbox' ? input.checked : input.value; }); cleanup(); resolve(result); } diff --git a/electron/ui/styles.css b/electron/ui/styles.css index 066949a..9d1fca1 100644 --- a/electron/ui/styles.css +++ b/electron/ui/styles.css @@ -1044,6 +1044,29 @@ body { font-family: inherit; } + +.form-group-check { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.check-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + font-size: 0.9rem; + color: var(--text-primary); +} + +.check-label input[type="checkbox"] { + width: 1rem; + height: 1rem; + cursor: pointer; + accent-color: var(--accent-primary); +} + /* Routing actions bar */ .routing-actions { diff --git a/server/bridge/AudioBridge.js b/server/bridge/AudioBridge.js index a32eb50..d2d9579 100644 --- a/server/bridge/AudioBridge.js +++ b/server/bridge/AudioBridge.js @@ -309,6 +309,7 @@ export class AudioBridge extends EventEmitter { groupId: userConfig.groupId, inputChannel: userConfig.inputChannel, outputChannel: userConfig.outputChannel, + publish: userConfig.publish !== false, liveKitUrl: this.options.liveKitUrl, token: userConfig.token, sampleRate: this.options.sampleRate, @@ -338,7 +339,10 @@ export class AudioBridge extends EventEmitter { await user.start(); this.serverAudioUsers.set(userConfig.name, user); - console.log(`✓ Server audio user "${userConfig.name}" démarré (entrée canal ${userConfig.inputChannel} → sortie canal ${userConfig.outputChannel}, room: ${userConfig.groupId})`); + const modeStr = userConfig.publish !== false + ? `canal ${userConfig.inputChannel} → sortie canal ${userConfig.outputChannel ?? 'aucune'}` + : `écoute seule → sortie canal ${userConfig.outputChannel ?? 'aucune'}`; + console.log(`✓ Server audio user "${userConfig.name}" démarré (${modeStr}, room: ${userConfig.groupId})`); } console.log(`✓ ${this.serverAudioUsers.size} server audio user(s) initialisés`); diff --git a/server/bridge/AudioBridgeManager.js b/server/bridge/AudioBridgeManager.js index e79e0af..7d7b0e8 100644 --- a/server/bridge/AudioBridgeManager.js +++ b/server/bridge/AudioBridgeManager.js @@ -66,10 +66,14 @@ class AudioBridgeManager extends EventEmitter { } ); + const rawInputChannel = user.input_channel ?? user.inputChannel ?? null; + const inputChannel = rawInputChannel !== null && rawInputChannel !== undefined ? rawInputChannel : null; + const publish = inputChannel !== null; + token.addGrant({ room: groupId, roomJoin: true, - canPublish: true, + canPublish: publish, canSubscribe: true, canPublishData: true }); @@ -81,8 +85,9 @@ class AudioBridgeManager extends EventEmitter { serverAudioUsers.push({ name: user.name, groupId, - inputChannel: user.input_channel ?? user.inputChannel ?? 0, + inputChannel, outputChannel: outputChannel !== null && outputChannel !== undefined ? outputChannel : null, + publish, token: jwt }); diff --git a/server/bridge/ServerAudioUser.js b/server/bridge/ServerAudioUser.js index 231b223..630def4 100644 --- a/server/bridge/ServerAudioUser.js +++ b/server/bridge/ServerAudioUser.js @@ -17,10 +17,13 @@ class ServerAudioUser extends EventEmitter { super(); this.name = options.name; - this.inputChannel = parseInt(options.inputChannel, 10); + this.inputChannel = (options.inputChannel !== null && options.inputChannel !== undefined) + ? parseInt(options.inputChannel, 10) + : null; this.outputChannel = (options.outputChannel !== null && options.outputChannel !== undefined) ? parseInt(options.outputChannel, 10) : null; + this.publish = options.publish !== false; // false = écoute seule this.groupId = options.groupId; this.frameSize = options.frameSize || 960; this.sampleRate = options.sampleRate || 48000; @@ -45,7 +48,8 @@ class ServerAudioUser extends EventEmitter { _setupClientEvents() { this.client.on('connected', () => { - console.log(`[ServerAudioUser:${this.name}] Connecté à room "${this.groupId}" (in:${this.inputChannel} → out:${this.outputChannel ?? 'aucune'})`); + const mode = this.publish ? `in:${this.inputChannel} → out:${this.outputChannel ?? 'aucune'}` : `écoute → out:${this.outputChannel ?? 'aucune'}`; + console.log(`[ServerAudioUser:${this.name}] Connecté à room "${this.groupId}" (${mode})`); this.emit('connected'); }); @@ -78,7 +82,7 @@ class ServerAudioUser extends EventEmitter { * @param {Float32Array} float32Data - Données PCM normalisées [-1.0, 1.0] */ sendAudio(float32Data) { - if (!this.client.isConnected) return; + if (!this.publish || !this.client.isConnected) return; const pcmBuffer = this._float32ToBuffer(float32Data); this.client.sendAudioData(pcmBuffer);