From b0f7d294d802ec52f18b5f9e359f5e93a0d60ba0 Mon Sep 17 00:00:00 2001 From: Benoit Date: Fri, 3 Jul 2026 17:13:58 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20mode=20=C3=A9coute=20seule=20pour?= =?UTF-8?q?=20les=20server=20audio=20users=20(Master=20par=20groupe)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un participant serveur peut être configuré sans publier de micro — il reçoit le mix du groupe et le sort sur un canal physique. - ServerAudioUser: flag publish (défaut true), sendAudio no-op si false - AudioBridgeManager: canPublish LiveKit selon flag, input_channel null si écoute - AudioBridge: passe publish à ServerAudioUser, log adapté - Electron UI: checkbox "Écoute seule" dans add/edit, badges 🎤/👂 dans table - main.js IPC: persist publish + input_channel null en écoute --- electron/main.js | 22 ++++++++++--- electron/ui/app.js | 50 +++++++++++++++++++++-------- electron/ui/styles.css | 41 +++++++++++++++++++++++ server/bridge/AudioBridge.js | 6 +++- server/bridge/AudioBridgeManager.js | 8 +++-- server/bridge/ServerAudioUser.js | 10 ++++-- 6 files changed, 114 insertions(+), 23 deletions(-) diff --git a/electron/main.js b/electron/main.js index 4fade6c..a21a34b 100644 --- a/electron/main.js +++ b/electron/main.js @@ -447,14 +447,21 @@ app.whenReady().then(async () => { } }); - ipcMain.handle('server-audio-users:create', (event, { name, group, input_channel, output_channel }) => { + ipcMain.handle('server-audio-users:create', (event, { name, group, input_channel, output_channel, publish }) => { try { const config = readConfig(); const users = config.server_audio_users || []; 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 isPublish = publish !== false; + const user = { + name, + group, + publish: isPublish, + input_channel: isPublish && input_channel !== null && input_channel !== undefined ? parseInt(input_channel) : null, + output_channel: output_channel !== null && output_channel !== '' ? parseInt(output_channel) : null + }; config.server_audio_users = [...users, user]; writeConfig(config); return { success: true, user }; @@ -463,13 +470,20 @@ app.whenReady().then(async () => { } }); - ipcMain.handle('server-audio-users:update', (event, { name, group, input_channel, output_channel }) => { + ipcMain.handle('server-audio-users:update', (event, { name, group, input_channel, output_channel, publish }) => { try { const config = readConfig(); 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 isPublish = publish !== false; + config.server_audio_users[idx] = { + name, + group, + publish: isPublish, + input_channel: isPublish && input_channel !== null && input_channel !== undefined ? parseInt(input_channel) : null, + 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..f4c0393 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, btn.dataset.sauPublish !== 'false'); } else if (action === 'delete') { await deleteServerAudioUser(name); } @@ -834,14 +834,19 @@ function renderServerAudioUsers() { container.innerHTML = ` - + - ${users.map(u => ` + ${users.map(u => { + const isListenOnly = u.publish === false; + const modeLabel = isListenOnly ? '👂 Écoute' : '🎤 Actif'; + const inputLabel = isListenOnly ? '' : `${chLabel(u.input_channel, 'input')}`; + return ` - + + - `).join('')} + `; + }).join('')}
NomGroupeEntréeSortie
NomGroupeModeEntréeSortie
${escapeHtml(u.name)} ${escapeHtml(u.group)}${chLabel(u.input_channel, 'input')}${modeLabel}${inputLabel} ${chLabel(u.output_channel, 'output')} + data-sau-output="${u.output_channel}" + data-sau-publish="${u.publish !== false}">Éditer
`; } @@ -901,6 +908,7 @@ async function addServerAudioUser() { fields: [ { name: 'name', label: 'Nom (identifiant unique, ex: foh)' }, { name: 'group', label: 'Groupe', type: 'select', options: groupOptions, default: defaultGroup }, + { name: 'publish', label: 'Publier audio vers le groupe (décocher = écoute seule)', type: 'checkbox', default: true }, inputField, outputField ], @@ -909,10 +917,12 @@ async function addServerAudioUser() { if (!result || !result.name.trim()) return; + const publish = result.publish !== false && result.publish !== 'false'; const res = await window.electronAPI.serverAudioUsers.create({ name: result.name.trim(), group: result.group, - input_channel: parseInt(result.input_channel), + publish, + input_channel: publish ? (result.input_channel !== '' ? parseInt(result.input_channel) : null) : null, output_channel: result.output_channel !== '' ? parseInt(result.output_channel) : null }); @@ -924,17 +934,18 @@ async function addServerAudioUser() { } } -async function editServerAudioUser(name, group, input_channel, output_channel) { +async function editServerAudioUser(name, group, input_channel, output_channel, publish = true) { const groupsData = await window.electronAPI.groups.list(); const groupOptions = (groupsData.groups || []).map(g => ({ value: slugify(g.name), label: g.name })); const inOpts = buildChannelOptions('input'); const outOpts = buildChannelOptions('output'); + const inputDefault = input_channel !== null && input_channel !== undefined && input_channel !== 'null' ? String(input_channel) : '0'; 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)', 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 }; @@ -943,6 +954,7 @@ async function editServerAudioUser(name, group, input_channel, output_channel) { title: `Modifier "${name}"`, fields: [ { name: 'group', label: 'Groupe', type: 'select', options: groupOptions, default: group }, + { name: 'publish', label: 'Publier audio vers le groupe (décocher = écoute seule)', type: 'checkbox', default: publish }, inputField, outputField ], @@ -951,10 +963,12 @@ async function editServerAudioUser(name, group, input_channel, output_channel) { if (!result) return; + const newPublish = result.publish !== false && result.publish !== 'false'; const res = await window.electronAPI.serverAudioUsers.update({ name, group: result.group, - input_channel: parseInt(result.input_channel), + publish: newPublish, + input_channel: newPublish ? (result.input_channel !== '' ? parseInt(result.input_channel) : null) : null, output_channel: result.output_channel !== '' ? parseInt(result.output_channel) : null }); @@ -1118,6 +1132,15 @@ function showModal({ title, fields = [], confirmLabel = 'Confirmer', confirmClas `; } + if (field.type === 'checkbox') { + return ` +
+ +
`; + } return `
@@ -1155,7 +1178,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..9d297fd 100644 --- a/electron/ui/styles.css +++ b/electron/ui/styles.css @@ -1044,6 +1044,47 @@ body { font-family: inherit; } +.ch-badge-active { + background: rgba(76, 175, 80, 0.12); + border-color: rgba(76, 175, 80, 0.3); + color: #4caf50; + font-family: inherit; +} + +.ch-badge-listen { + background: rgba(255, 152, 0, 0.12); + border-color: rgba(255, 152, 0, 0.3); + color: #ff9800; + font-family: inherit; +} + +.ch-badge-muted { + color: var(--text-secondary); + opacity: 0.5; +} + +.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..0fc82bf 100644 --- a/server/bridge/AudioBridgeManager.js +++ b/server/bridge/AudioBridgeManager.js @@ -66,10 +66,12 @@ class AudioBridgeManager extends EventEmitter { } ); + const publish = user.publish !== false; + token.addGrant({ room: groupId, roomJoin: true, - canPublish: true, + canPublish: publish, canSubscribe: true, canPublishData: true }); @@ -77,12 +79,14 @@ class AudioBridgeManager extends EventEmitter { const jwt = await token.toJwt(); const outputChannel = user.output_channel ?? user.outputChannel; + const rawInputChannel = user.input_channel ?? user.inputChannel; serverAudioUsers.push({ name: user.name, groupId, - inputChannel: user.input_channel ?? user.inputChannel ?? 0, + inputChannel: rawInputChannel !== null && rawInputChannel !== undefined ? rawInputChannel : null, 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); From 0cbad12e49bd21663ac971a31ec710ed128d5f1c Mon Sep 17 00:00:00 2001 From: Benoit Date: Fri, 3 Jul 2026 17:21:48 +0200 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20simplifier=20UI=20server=20audi?= =?UTF-8?q?o=20users=20=E2=80=94=20entr=C3=A9e=20vide=20=3D=20=C3=A9coute?= =?UTF-8?q?=20seule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supprime la checkbox "Publier audio" et la colonne Mode. Le comportement est déduit de l'entrée : aucune entrée = écoute seule. Option "Aucune entrée" ajoutée au select canal d'entrée (comme pour sortie). publish dérivé de inputChannel !== null dans AudioBridgeManager. --- electron/main.js | 14 ++++------ electron/ui/app.js | 43 ++++++++++------------------- electron/ui/styles.css | 18 ------------ server/bridge/AudioBridgeManager.js | 7 +++-- 4 files changed, 24 insertions(+), 58 deletions(-) diff --git a/electron/main.js b/electron/main.js index a21a34b..b4ca903 100644 --- a/electron/main.js +++ b/electron/main.js @@ -447,19 +447,18 @@ app.whenReady().then(async () => { } }); - ipcMain.handle('server-audio-users:create', (event, { name, group, input_channel, output_channel, publish }) => { + ipcMain.handle('server-audio-users:create', (event, { name, group, input_channel, output_channel }) => { try { const config = readConfig(); const users = config.server_audio_users || []; if (users.find(u => u.name === name)) { return { success: false, error: `Un utilisateur "${name}" existe déjà` }; } - const isPublish = publish !== false; + const parsedInput = input_channel !== null && input_channel !== undefined ? parseInt(input_channel) : null; const user = { name, group, - publish: isPublish, - input_channel: isPublish && input_channel !== null && input_channel !== undefined ? parseInt(input_channel) : null, + input_channel: parsedInput, output_channel: output_channel !== null && output_channel !== '' ? parseInt(output_channel) : null }; config.server_audio_users = [...users, user]; @@ -470,18 +469,17 @@ app.whenReady().then(async () => { } }); - ipcMain.handle('server-audio-users:update', (event, { name, group, input_channel, output_channel, publish }) => { + ipcMain.handle('server-audio-users:update', (event, { name, group, input_channel, output_channel }) => { try { const config = readConfig(); const users = config.server_audio_users || []; const idx = users.findIndex(u => u.name === name); if (idx === -1) return { success: false, error: `Utilisateur "${name}" introuvable` }; - const isPublish = publish !== false; + const parsedInput = input_channel !== null && input_channel !== undefined ? parseInt(input_channel) : null; config.server_audio_users[idx] = { name, group, - publish: isPublish, - input_channel: isPublish && input_channel !== null && input_channel !== undefined ? parseInt(input_channel) : null, + input_channel: parsedInput, output_channel: output_channel !== null && output_channel !== '' ? parseInt(output_channel) : null }; writeConfig(config); diff --git a/electron/ui/app.js b/electron/ui/app.js index f4c0393..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, btn.dataset.sauInput, btn.dataset.sauOutput, btn.dataset.sauPublish !== 'false'); + await editServerAudioUser(name, btn.dataset.sauGroup, btn.dataset.sauInput, btn.dataset.sauOutput); } else if (action === 'delete') { await deleteServerAudioUser(name); } @@ -834,19 +834,14 @@ function renderServerAudioUsers() { container.innerHTML = ` - + - ${users.map(u => { - const isListenOnly = u.publish === false; - const modeLabel = isListenOnly ? '👂 Écoute' : '🎤 Actif'; - const inputLabel = isListenOnly ? '' : `${chLabel(u.input_channel, 'input')}`; - return ` + ${users.map(u => ` - - + - `; - }).join('')} + `).join('')}
NomGroupeModeEntréeSortie
NomGroupeEntréeSortie
${escapeHtml(u.name)} ${escapeHtml(u.group)}${modeLabel}${inputLabel}${chLabel(u.input_channel, 'input')} ${chLabel(u.output_channel, 'output')} + data-sau-output="${u.output_channel}">Éditer
`; } @@ -881,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; } @@ -897,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 }; @@ -908,7 +899,6 @@ async function addServerAudioUser() { fields: [ { name: 'name', label: 'Nom (identifiant unique, ex: foh)' }, { name: 'group', label: 'Groupe', type: 'select', options: groupOptions, default: defaultGroup }, - { name: 'publish', label: 'Publier audio vers le groupe (décocher = écoute seule)', type: 'checkbox', default: true }, inputField, outputField ], @@ -917,12 +907,10 @@ async function addServerAudioUser() { if (!result || !result.name.trim()) return; - const publish = result.publish !== false && result.publish !== 'false'; const res = await window.electronAPI.serverAudioUsers.create({ name: result.name.trim(), group: result.group, - publish, - input_channel: publish ? (result.input_channel !== '' ? parseInt(result.input_channel) : null) : null, + input_channel: result.input_channel !== '' ? parseInt(result.input_channel) : null, output_channel: result.output_channel !== '' ? parseInt(result.output_channel) : null }); @@ -934,17 +922,17 @@ async function addServerAudioUser() { } } -async function editServerAudioUser(name, group, input_channel, output_channel, publish = true) { +async function editServerAudioUser(name, group, input_channel, output_channel) { const groupsData = await window.electronAPI.groups.list(); const groupOptions = (groupsData.groups || []).map(g => ({ value: slugify(g.name), label: g.name })); const inOpts = buildChannelOptions('input'); const outOpts = buildChannelOptions('output'); - const inputDefault = input_channel !== null && input_channel !== undefined && input_channel !== 'null' ? String(input_channel) : '0'; + 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: inputDefault } - : { name: 'input_channel', label: 'Canal entrée (index)', type: 'number', default: inputDefault, min: 0, max: 63 }; + : { 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 } @@ -954,7 +942,6 @@ async function editServerAudioUser(name, group, input_channel, output_channel, p title: `Modifier "${name}"`, fields: [ { name: 'group', label: 'Groupe', type: 'select', options: groupOptions, default: group }, - { name: 'publish', label: 'Publier audio vers le groupe (décocher = écoute seule)', type: 'checkbox', default: publish }, inputField, outputField ], @@ -963,12 +950,10 @@ async function editServerAudioUser(name, group, input_channel, output_channel, p if (!result) return; - const newPublish = result.publish !== false && result.publish !== 'false'; const res = await window.electronAPI.serverAudioUsers.update({ name, group: result.group, - publish: newPublish, - input_channel: newPublish ? (result.input_channel !== '' ? parseInt(result.input_channel) : null) : null, + input_channel: result.input_channel !== '' ? parseInt(result.input_channel) : null, output_channel: result.output_channel !== '' ? parseInt(result.output_channel) : null }); diff --git a/electron/ui/styles.css b/electron/ui/styles.css index 9d297fd..9d1fca1 100644 --- a/electron/ui/styles.css +++ b/electron/ui/styles.css @@ -1044,24 +1044,6 @@ body { font-family: inherit; } -.ch-badge-active { - background: rgba(76, 175, 80, 0.12); - border-color: rgba(76, 175, 80, 0.3); - color: #4caf50; - font-family: inherit; -} - -.ch-badge-listen { - background: rgba(255, 152, 0, 0.12); - border-color: rgba(255, 152, 0, 0.3); - color: #ff9800; - font-family: inherit; -} - -.ch-badge-muted { - color: var(--text-secondary); - opacity: 0.5; -} .form-group-check { display: flex; diff --git a/server/bridge/AudioBridgeManager.js b/server/bridge/AudioBridgeManager.js index 0fc82bf..7d7b0e8 100644 --- a/server/bridge/AudioBridgeManager.js +++ b/server/bridge/AudioBridgeManager.js @@ -66,7 +66,9 @@ class AudioBridgeManager extends EventEmitter { } ); - const publish = user.publish !== false; + const rawInputChannel = user.input_channel ?? user.inputChannel ?? null; + const inputChannel = rawInputChannel !== null && rawInputChannel !== undefined ? rawInputChannel : null; + const publish = inputChannel !== null; token.addGrant({ room: groupId, @@ -79,12 +81,11 @@ class AudioBridgeManager extends EventEmitter { const jwt = await token.toJwt(); const outputChannel = user.output_channel ?? user.outputChannel; - const rawInputChannel = user.input_channel ?? user.inputChannel; serverAudioUsers.push({ name: user.name, groupId, - inputChannel: rawInputChannel !== null && rawInputChannel !== undefined ? rawInputChannel : null, + inputChannel, outputChannel: outputChannel !== null && outputChannel !== undefined ? outputChannel : null, publish, token: jwt