feat: ajouter les server audio users (participants LiveKit côté serveur avec I/O physique)

Chaque server audio user est un participant LiveKit indépendant géré par le serveur :
- publie un canal physique d'entrée comme track audio
- reçoit et mixe l'audio de tous les autres participants (mix-minus naturel)
- sort le mix vers un canal physique dédié

Nouvelle classe ServerAudioUser.js, intégration dans AudioBridge et AudioBridgeManager,
section server_audio_users dans config.yaml (vide par défaut, exemple commenté).
This commit is contained in:
2026-07-01 14:14:26 +02:00
parent 51245db256
commit 87a1370ad4
4 changed files with 320 additions and 1 deletions
+74 -1
View File
@@ -19,6 +19,7 @@ import OpusCodec, { OpusPresets } from './OpusCodec.js';
import JitterBuffer, { JitterBufferPresets } from './JitterBuffer.js';
import LiveKitClient from './LiveKitClient.js';
import GroupAudioRouter from './GroupAudioRouter.js';
import ServerAudioUser from './ServerAudioUser.js';
export class AudioBridge extends EventEmitter {
constructor(options = {}) {
@@ -68,6 +69,9 @@ export class AudioBridge extends EventEmitter {
// Frame accumulators pour LiveKit (240 samples → 960 samples)
this.liveKitFrameAccumulators = new Map(); // Map<groupName, { buffer: Float32Array, offset: number }>
// Utilisateurs audio gérés côté serveur (participants LiveKit avec I/O physique dédiés)
this.serverAudioUsers = new Map(); // Map<name, ServerAudioUser>
// Pool de buffers pré-alloués pour éviter allocations répétées
this.bufferPool = {
float32: [], // Pool de Float32Array réutilisables
@@ -120,7 +124,10 @@ export class AudioBridge extends EventEmitter {
// 5. Connexion à LiveKit
await this._initLiveKit();
// 6. Démarrage du routing audio
// 6. Initialisation des server audio users
await this._initServerAudioUsers();
// 7. Démarrage du routing audio
await this._startAudioRouting();
this.isRunning = true;
@@ -386,6 +393,57 @@ export class AudioBridge extends EventEmitter {
console.log(`${this.liveKitClients.size} connexions LiveKit établies`);
}
/**
* Initialise les utilisateurs audio serveur (participants LiveKit avec I/O physique)
* @private
*/
async _initServerAudioUsers() {
const users = this.options.serverAudioUsers;
if (!users || users.length === 0) return;
console.log(`🎤 Initialisation ${users.length} server audio user(s)...`);
for (const userConfig of users) {
const user = new ServerAudioUser({
name: userConfig.name,
groupId: userConfig.groupId,
inputChannel: userConfig.inputChannel,
outputChannel: userConfig.outputChannel,
liveKitUrl: this.options.liveKitUrl,
token: userConfig.token,
sampleRate: this.options.sampleRate,
frameSize: this.options.frameSize
});
// Quand une frame de mix est prête, l'envoyer vers le canal physique de sortie
const outputCh = userConfig.outputChannel;
user.on('outputReady', (mixBuffer) => {
if (!this.audioBackend) return;
const numChannels = this.options.channels || 1;
const frameSize = this.options.frameSize;
if (numChannels <= 1) {
const pcmBuffer = this._float32ToBuffer(mixBuffer);
this.audioBackend.queueAudio(pcmBuffer);
} else {
// Construire un buffer multi-canaux avec l'audio du user sur son canal de sortie
const interleaved = new Float32Array(frameSize * numChannels);
for (let i = 0; i < frameSize; i++) {
interleaved[i * numChannels + outputCh] = mixBuffer[i];
}
const pcmBuffer = this._float32ToBuffer(interleaved);
this.audioBackend.queueAudio(pcmBuffer);
}
});
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})`);
}
console.log(`${this.serverAudioUsers.size} server audio user(s) initialisés`);
}
/**
* Démarre le routing audio bidirectionnel complet
* @private
@@ -424,6 +482,14 @@ export class AudioBridge extends EventEmitter {
}
}
// ÉTAPE 0 : Envoyer les données de chaque canal vers les server audio users
for (const [, user] of this.serverAudioUsers) {
const channelData = this.inputChannelBuffers.get(user.inputChannel);
if (channelData) {
user.sendAudio(channelData);
}
}
// ÉTAPE 1 : Inputs physiques → Groupes (via GroupAudioRouter)
const groupBuffers = this.groupAudioRouter.processInputsToGroups(
this.inputChannelBuffers
@@ -760,6 +826,13 @@ export class AudioBridge extends EventEmitter {
}
this.liveKitClients.clear();
// Arrêter les server audio users
for (const [name, user] of this.serverAudioUsers.entries()) {
console.log(`🔌 Arrêt server audio user "${name}"...`);
await user.stop();
}
this.serverAudioUsers.clear();
if (this.groupAudioRouter) {
this.groupAudioRouter.destroy();
this.groupAudioRouter = null;