Salons vocaux
Depuis la 2.1.0, un bot peut rejoindre un salon vocal et y diffuser de l'audio (musique, texte-à-parole, effets sonores…), à la manière de @discordjs/voice. Depuis la 3.0.0, le transport média est LiveKit (voir ci-dessous).
FFmpeg requis
La lecture audio (connection.play()) décode la source via FFmpeg avant de l'envoyer à LiveKit. Installez FFmpeg sur la machine qui exécute le bot (voir ffmpeg.org/download.html) — sans FFmpeg accessible sur le PATH, play() lève une BloumeChatVoiceError explicite. Alternative sans installation système : ajoutez le paquet npm ffmpeg-static à votre bot (prism-media, utilisé en interne, le détecte automatiquement).
Architecture : SFU LiveKit self-hosted
(depuis la 3.0.0 — avant cette version, le vocal BloumeChat fonctionnait en maillage WebRTC pair-à-pair, une connexion directe par participant)
Le vocal BloumeChat passe désormais par un SFU (Selective Forwarding Unit) LiveKit auto-hébergé : le bot ouvre une seule connexion vers le serveur média, qui se charge de router l'audio vers/depuis tous les autres participants. C'est le même transport que le client web BloumeChat (livekit-client), juste via le SDK Node officiel de LiveKit (@livekit/rtc-node) côté bot.
Ce changement élimine le coût CPU/bande passante en O(N) par bot (une connexion par participant) de l'ancien maillage — une seule connexion, quel que soit le nombre de participants dans le salon.
Le SDK gère cette complexité pour vous — connection.play() publie un seul flux audio sur la piste LiveKit du bot ; le SFU se charge de le diffuser à tout le salon.
Rejoindre un salon vocal
Channel.join() — disponible sur tout salon de type "VOICE" — ouvre la connexion et retourne une VoiceConnection une fois que le serveur a confirmé la présence du bot dans le salon.
import { BloumeChat } from "bloumechat";
const client = new BloumeChat();
client.on("messageCreate", async (message) => {
if (message.content !== "!join") return;
const guild = client.guilds.cache.get(message.channel?.serverId ?? "");
const voiceChannel = guild?.channels.find((c) => c.type === "VOICE" && c.name === "Musique");
if (!voiceChannel) return message.reply("Salon vocal introuvable.");
const connection = await voiceChannel.join();
await message.reply("Connecté au salon vocal 🔊");
});
client.login(process.env.BOT_TOKEN!);const { BloumeChat } = require("bloumechat");
const client = new BloumeChat();
client.on("messageCreate", async (message) => {
if (message.content !== "!join") return;
const guild = client.guilds.cache.get(message.channel && message.channel.serverId);
const voiceChannel = guild && guild.channels.find((c) => c.type === "VOICE" && c.name === "Musique");
if (!voiceChannel) return message.reply("Salon vocal introuvable.");
const connection = await voiceChannel.join();
await message.reply("Connecté au salon vocal 🔊");
});
client.login(process.env.BOT_TOKEN);Un seul salon vocal à la fois
La session du bot ne peut être membre que d'un seul salon vocal à la fois (contrainte du serveur BloumeChat, pas du SDK). Appeler channel.join() sur un nouveau salon quitte automatiquement le précédent. Retrouvez la connexion active via client.voice.connection.
Savoir dans quel salon vocal se trouve un membre
(4.1.0+) client.voiceStates est un cache — alimenté automatiquement par les événements voice:* (voir le guide Événements) — qui répond à « dans quel salon vocal se trouve cet utilisateur, là maintenant ? » sans que vous ayez à suivre voiceUserJoined/voiceUserLeft vous-même pour le reconstruire.
const state = client.voiceStates.cache.get(userPublicId); // VoiceState | undefined
if (state) console.log(`Dans le salon ${state.channelId}, parle : ${state.speaking}`);
// Raccourci équivalent depuis un Member déjà en main :
console.log(member.voice?.channelId);const state = client.voiceStates.cache.get(userPublicId);
if (state) console.log(`Dans le salon ${state.channelId}, parle : ${state.speaking}`);
console.log(member.voice && member.voice.channelId);Cas d'usage typique : rejoindre le salon de l'auteur d'un message
client.on("messageCreate", async (message) => {
if (message.content !== "!play") return;
const authorVoice = client.voiceStates.cache.get(message.author.id);
if (!authorVoice) return message.reply("Rejoignez d'abord un salon vocal.");
const voiceChannel = client.channels.cache.get(authorVoice.channelId);
if (voiceChannel) await voiceChannel.join();
});Voir l'exemple complet Bot de musique vocal.
Cache best-effort, pas une source de vérité garantie
Ce cache n'est fiable qu'à partir du moment où le bot a reçu au moins un événement voice:* concernant le salon en question (le rejoindre lui-même en fait partie). Un utilisateur déjà en vocal avant le démarrage du process bot n'apparaîtra dans client.voiceStates qu'après le prochain événement de présence le concernant — ce n'est pas l'équivalent d'une requête REST « état actuel garanti ».
Diffuser de l'audio
connection.play(resource: string | NodeJS.ReadableStream, options?: PlayOptions): voidresource accepte un chemin de fichier local, une URL http(s) (FFmpeg gère nativement le streaming HTTP), ou un flux Readable. Voir la Référence API — Voix pour la forme complète de PlayOptions.
const connection = await voiceChannel.join();
connection.play("https://example.com/musique.mp3");
connection.on("playerFinish", () => console.log("Lecture terminée."));const connection = await voiceChannel.join();
connection.play("https://example.com/musique.mp3");
connection.on("playerFinish", () => console.log("Lecture terminée."));Contrôler la lecture
connection.pause();
connection.resume();
connection.setVolume(0.5); // 50%
connection.stopPlaying();connection.pause();
connection.resume();
connection.setVolume(0.5); // 50%
connection.stopPlaying();connection.isPlaying / connection.isPaused reflètent l'état courant. Le SDK marque automatiquement le bot comme « en train de parler » (speaking: true) pendant la lecture, et le repasse à false à l'arrêt — inutile de gérer cet état vous-même.
Muet / sourdine
connection.setMuted(true); // coupe le micro sortant du bot (n'affecte pas connection.play())
connection.setDeafened(true);setMuted n'interrompt pas play()
setMuted/setDeafened ne pilotent que l'indicateur d'état visible par les autres participants — ils ne coupent pas la diffusion audio en cours. Utilisez connection.pause()/stopPlaying() pour ça.
Participants du salon
connection.participants; // VoiceUser[] — snapshot courant, y compris le botconnection.on("userJoined", (user) => console.log(`${user.userName} a rejoint.`));
connection.on("userLeft", (userPublicId) => console.log(`${userPublicId} est parti.`));
connection.on("userStateUpdate", (data) => console.log("État mis à jour :", data));connection.on("userJoined", (user) => console.log(`${user.userName} a rejoint.`));
connection.on("userLeft", (userPublicId) => console.log(`${userPublicId} est parti.`));
connection.on("userStateUpdate", (data) => console.log("État mis à jour :", data));Quitter le salon
voiceChannel.leave();
// équivalent :
client.voice.leave();voiceChannel.leave();
// équivalent :
client.voice.leave();client.destroy() quitte aussi automatiquement le salon vocal actif.
Audio entrant (avancé)
LiveKit décode déjà l'audio reçu en PCM pour vous (contrairement à l'ancien maillage, qui exposait de l'Opus brut) — l'événement audioFrame fournit directement des échantillons PCM 16-bit prêts à l'emploi, pour les bots qui veulent implémenter leur propre traitement (reconnaissance vocale, enregistrement…) :
connection.on("audioFrame", (userPublicId: string, samples: Int16Array, sampleRate: number, channels: number) => {
// samples : PCM 16-bit déjà décodé (20ms de audio) — pas de décodage à faire.
});connection.on("audioFrame", (userPublicId, samples, sampleRate, channels) => {
// samples : PCM 16-bit déjà décodé (20ms de audio) — pas de décodage à faire.
});Prochaine étape
Consultez l'exemple complet Bot de musique vocal ou la Référence API — Voix pour la liste exhaustive des méthodes, événements et types.
