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 = `
- | Nom | Groupe | Entrée | Sortie | |
+ | Nom | Groupe | Mode | Entrée | Sortie | |
- ${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 `
| ${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
|
-
`).join('')}
+ `;
+ }).join('')}
`;
}
@@ -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);