Merge pull request 'Routage complexe' (#4) from Routage-complexe into desktop-app

Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-07-03 17:29:07 +02:00
6 changed files with 77 additions and 20 deletions
+14 -2
View File
@@ -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) {
+21 -12
View File
@@ -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
<select id="modal-field-${field.name}" class="form-control">${optionsHtml}</select>
</div>`;
}
if (field.type === 'checkbox') {
return `
<div class="form-group form-group-check">
<label class="check-label">
<input type="checkbox" id="modal-field-${field.name}" ${field.default !== false ? 'checked' : ''}>
${escapeHtml(field.label)}
</label>
</div>`;
}
return `
<div class="form-group">
<label>${escapeHtml(field.label)}</label>
@@ -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);
}
+23
View File
@@ -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 {
+5 -1
View File
@@ -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`);
+7 -2
View File
@@ -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
});
+7 -3
View File
@@ -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);