mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 16:13:31 +01:00
190 lines
5.3 KiB
TypeScript
190 lines
5.3 KiB
TypeScript
import { GatewayOpcodes } from 'discord-api-types/v10';
|
|
import type { VoiceConnection } from './VoiceConnection';
|
|
import type { AudioPlayer } from './audio/index';
|
|
|
|
export interface JoinConfig {
|
|
channelId: string | null;
|
|
group: string;
|
|
guildId: string;
|
|
selfDeaf: boolean;
|
|
selfMute: boolean;
|
|
}
|
|
|
|
/**
|
|
* Sends a voice state update to the main websocket shard of a guild, to indicate joining/leaving/moving across
|
|
* voice channels.
|
|
*
|
|
* @param config - The configuration to use when joining the voice channel
|
|
*/
|
|
export function createJoinVoiceChannelPayload(config: JoinConfig) {
|
|
return {
|
|
op: GatewayOpcodes.VoiceStateUpdate,
|
|
// eslint-disable-next-line id-length
|
|
d: {
|
|
guild_id: config.guildId,
|
|
channel_id: config.channelId,
|
|
self_deaf: config.selfDeaf,
|
|
self_mute: config.selfMute,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Voice Connections
|
|
const groups = new Map<string, Map<string, VoiceConnection>>();
|
|
groups.set('default', new Map());
|
|
|
|
function getOrCreateGroup(group: string) {
|
|
const existing = groups.get(group);
|
|
if (existing) return existing;
|
|
const map = new Map<string, VoiceConnection>();
|
|
groups.set(group, map);
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the map of group names to maps of voice connections. By default, all voice connections
|
|
* are created under the 'default' group.
|
|
*
|
|
* @returns The group map
|
|
*/
|
|
export function getGroups() {
|
|
return groups;
|
|
}
|
|
|
|
/**
|
|
* Retrieves all the voice connections under the 'default' group.
|
|
*
|
|
* @param group - The group to look up
|
|
* @returns The map of voice connections
|
|
*/
|
|
export function getVoiceConnections(group?: 'default'): Map<string, VoiceConnection>;
|
|
|
|
/**
|
|
* Retrieves all the voice connections under the given group name.
|
|
*
|
|
* @param group - The group to look up
|
|
* @returns The map of voice connections
|
|
*/
|
|
export function getVoiceConnections(group: string): Map<string, VoiceConnection> | undefined;
|
|
|
|
/**
|
|
* Retrieves all the voice connections under the given group name. Defaults to the 'default' group.
|
|
*
|
|
* @param group - The group to look up
|
|
* @returns The map of voice connections
|
|
*/
|
|
export function getVoiceConnections(group = 'default') {
|
|
return groups.get(group);
|
|
}
|
|
|
|
/**
|
|
* Finds a voice connection with the given guild id and group. Defaults to the 'default' group.
|
|
*
|
|
* @param guildId - The guild id of the voice connection
|
|
* @param group - the group that the voice connection was registered with
|
|
* @returns The voice connection, if it exists
|
|
*/
|
|
export function getVoiceConnection(guildId: string, group = 'default') {
|
|
return getVoiceConnections(group)?.get(guildId);
|
|
}
|
|
|
|
export function untrackVoiceConnection(voiceConnection: VoiceConnection) {
|
|
return getVoiceConnections(voiceConnection.joinConfig.group)?.delete(voiceConnection.joinConfig.guildId);
|
|
}
|
|
|
|
export function trackVoiceConnection(voiceConnection: VoiceConnection) {
|
|
return getOrCreateGroup(voiceConnection.joinConfig.group).set(voiceConnection.joinConfig.guildId, voiceConnection);
|
|
}
|
|
|
|
// Audio Players
|
|
|
|
// Each audio packet is 20ms long
|
|
const FRAME_LENGTH = 20;
|
|
|
|
let audioCycleInterval: NodeJS.Timeout | undefined;
|
|
let nextTime = -1;
|
|
|
|
/**
|
|
* A list of created audio players that are still active and haven't been destroyed.
|
|
*/
|
|
const audioPlayers: AudioPlayer[] = [];
|
|
|
|
/**
|
|
* Called roughly every 20 milliseconds. Dispatches audio from all players, and then gets the players to prepare
|
|
* the next audio frame.
|
|
*/
|
|
function audioCycleStep() {
|
|
if (nextTime === -1) return;
|
|
|
|
nextTime += FRAME_LENGTH;
|
|
const available = audioPlayers.filter((player) => player.checkPlayable());
|
|
|
|
for (const player of available) {
|
|
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
player['_stepDispatch']();
|
|
}
|
|
|
|
prepareNextAudioFrame(available);
|
|
}
|
|
|
|
/**
|
|
* Recursively gets the players that have been passed as parameters to prepare audio frames that can be played
|
|
* at the start of the next cycle.
|
|
*/
|
|
function prepareNextAudioFrame(players: AudioPlayer[]) {
|
|
const nextPlayer = players.shift();
|
|
|
|
if (!nextPlayer) {
|
|
if (nextTime !== -1) {
|
|
audioCycleInterval = setTimeout(() => audioCycleStep(), nextTime - Date.now());
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/dot-notation
|
|
nextPlayer['_stepPrepare']();
|
|
|
|
// setImmediate to avoid long audio player chains blocking other scheduled tasks
|
|
setImmediate(() => prepareNextAudioFrame(players));
|
|
}
|
|
|
|
/**
|
|
* Checks whether or not the given audio player is being driven by the data store clock.
|
|
*
|
|
* @param target - The target to test for
|
|
* @returns `true` if it is being tracked, `false` otherwise
|
|
*/
|
|
export function hasAudioPlayer(target: AudioPlayer) {
|
|
return audioPlayers.includes(target);
|
|
}
|
|
|
|
/**
|
|
* Adds an audio player to the data store tracking list, if it isn't already there.
|
|
*
|
|
* @param player - The player to track
|
|
*/
|
|
export function addAudioPlayer(player: AudioPlayer) {
|
|
if (hasAudioPlayer(player)) return player;
|
|
audioPlayers.push(player);
|
|
if (audioPlayers.length === 1) {
|
|
nextTime = Date.now();
|
|
setImmediate(() => audioCycleStep());
|
|
}
|
|
|
|
return player;
|
|
}
|
|
|
|
/**
|
|
* Removes an audio player from the data store tracking list, if it is present there.
|
|
*/
|
|
export function deleteAudioPlayer(player: AudioPlayer) {
|
|
const index = audioPlayers.indexOf(player);
|
|
if (index === -1) return;
|
|
audioPlayers.splice(index, 1);
|
|
if (audioPlayers.length === 0) {
|
|
nextTime = -1;
|
|
if (audioCycleInterval !== undefined) clearTimeout(audioCycleInterval);
|
|
}
|
|
}
|