2022-12-20 22:20:09 +07:00
|
|
|
'use strict';
|
2022-05-21 21:02:00 +07:00
|
|
|
var __create = Object.create;
|
|
|
|
|
var __defProp = Object.defineProperty;
|
|
|
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
|
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
2022-12-20 22:20:09 +07:00
|
|
|
var __defNormalProp = (obj, key, value) =>
|
|
|
|
|
key in obj
|
|
|
|
|
? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value })
|
|
|
|
|
: (obj[key] = value);
|
|
|
|
|
var __name = (target, value) => __defProp(target, 'name', { value, configurable: true });
|
2022-08-01 13:02:58 +07:00
|
|
|
var __export = (target, all) => {
|
2022-12-20 22:20:09 +07:00
|
|
|
for (var name in all) __defProp(target, name, { get: all[name], enumerable: true });
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2022-08-01 13:02:58 +07:00
|
|
|
var __copyProps = (to, from, except, desc) => {
|
2022-12-20 22:20:09 +07:00
|
|
|
if ((from && typeof from === 'object') || typeof from === 'function') {
|
2022-06-26 09:40:04 +07:00
|
|
|
for (let key of __getOwnPropNames(from))
|
|
|
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
2022-12-20 22:20:09 +07:00
|
|
|
__defProp(to, key, {
|
|
|
|
|
get: () => from[key],
|
|
|
|
|
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable,
|
|
|
|
|
});
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
return to;
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
var __toESM = (mod, isNodeMode, target) => (
|
|
|
|
|
(target = mod != null ? __create(__getProtoOf(mod)) : {}),
|
|
|
|
|
__copyProps(
|
2023-03-21 18:47:11 +07:00
|
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
|
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
|
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
|
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
2022-12-20 22:20:09 +07:00
|
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, 'default', { value: mod, enumerable: true }) : target,
|
|
|
|
|
mod,
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
var __toCommonJS = mod => __copyProps(__defProp({}, '__esModule', { value: true }), mod);
|
2022-08-01 13:02:58 +07:00
|
|
|
var __publicField = (obj, key, value) => {
|
2022-12-20 22:20:09 +07:00
|
|
|
__defNormalProp(obj, typeof key !== 'symbol' ? key + '' : key, value);
|
2022-06-26 09:40:04 +07:00
|
|
|
return value;
|
|
|
|
|
};
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/index.ts
|
2022-05-21 21:02:00 +07:00
|
|
|
var src_exports = {};
|
|
|
|
|
__export(src_exports, {
|
|
|
|
|
AudioPlayer: () => AudioPlayer,
|
|
|
|
|
AudioPlayerError: () => AudioPlayerError,
|
|
|
|
|
AudioPlayerStatus: () => AudioPlayerStatus,
|
|
|
|
|
AudioReceiveStream: () => AudioReceiveStream,
|
|
|
|
|
AudioResource: () => AudioResource,
|
|
|
|
|
EndBehaviorType: () => EndBehaviorType,
|
|
|
|
|
NoSubscriberBehavior: () => NoSubscriberBehavior,
|
|
|
|
|
PlayerSubscription: () => PlayerSubscription,
|
|
|
|
|
SSRCMap: () => SSRCMap,
|
|
|
|
|
SpeakingMap: () => SpeakingMap,
|
|
|
|
|
StreamType: () => StreamType,
|
2022-09-28 19:40:46 +07:00
|
|
|
VoiceConnection: () => VoiceConnection,
|
2022-05-21 21:02:00 +07:00
|
|
|
VoiceConnectionDisconnectReason: () => VoiceConnectionDisconnectReason,
|
|
|
|
|
VoiceConnectionStatus: () => VoiceConnectionStatus,
|
|
|
|
|
VoiceReceiver: () => VoiceReceiver,
|
|
|
|
|
createAudioPlayer: () => createAudioPlayer,
|
|
|
|
|
createAudioResource: () => createAudioResource,
|
|
|
|
|
createDefaultAudioReceiveStreamOptions: () => createDefaultAudioReceiveStreamOptions,
|
|
|
|
|
demuxProbe: () => demuxProbe,
|
|
|
|
|
entersState: () => entersState,
|
|
|
|
|
generateDependencyReport: () => generateDependencyReport,
|
|
|
|
|
getGroups: () => getGroups,
|
|
|
|
|
getVoiceConnection: () => getVoiceConnection,
|
|
|
|
|
getVoiceConnections: () => getVoiceConnections,
|
|
|
|
|
joinVoiceChannel: () => joinVoiceChannel,
|
2022-09-28 22:09:00 +07:00
|
|
|
validateDiscordOpusHead: () => validateDiscordOpusHead,
|
2022-12-20 22:20:09 +07:00
|
|
|
version: () => version2,
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
module.exports = __toCommonJS(src_exports);
|
|
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/VoiceConnection.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_events7 = require('events');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/DataStore.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_v10 = require('discord-api-types/v10');
|
2022-08-01 13:02:58 +07:00
|
|
|
function createJoinVoiceChannelPayload(config) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return {
|
|
|
|
|
op: import_v10.GatewayOpcodes.VoiceStateUpdate,
|
2023-03-21 18:47:11 +07:00
|
|
|
// eslint-disable-next-line id-length
|
2022-05-21 21:02:00 +07:00
|
|
|
d: {
|
|
|
|
|
guild_id: config.guildId,
|
|
|
|
|
channel_id: config.channelId,
|
|
|
|
|
self_deaf: config.selfDeaf,
|
2022-12-20 22:20:09 +07:00
|
|
|
self_mute: config.selfMute,
|
|
|
|
|
},
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(createJoinVoiceChannelPayload, 'createJoinVoiceChannelPayload');
|
2022-05-21 21:02:00 +07:00
|
|
|
var groups = /* @__PURE__ */ new Map();
|
2022-12-20 22:20:09 +07:00
|
|
|
groups.set('default', /* @__PURE__ */ new Map());
|
2022-08-01 13:02:58 +07:00
|
|
|
function getOrCreateGroup(group) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const existing = groups.get(group);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (existing) return existing;
|
2022-05-21 21:02:00 +07:00
|
|
|
const map = /* @__PURE__ */ new Map();
|
|
|
|
|
groups.set(group, map);
|
|
|
|
|
return map;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(getOrCreateGroup, 'getOrCreateGroup');
|
2022-08-01 13:02:58 +07:00
|
|
|
function getGroups() {
|
2022-05-21 21:02:00 +07:00
|
|
|
return groups;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(getGroups, 'getGroups');
|
|
|
|
|
function getVoiceConnections(group = 'default') {
|
2022-05-21 21:02:00 +07:00
|
|
|
return groups.get(group);
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(getVoiceConnections, 'getVoiceConnections');
|
|
|
|
|
function getVoiceConnection(guildId, group = 'default') {
|
2022-05-21 21:02:00 +07:00
|
|
|
return getVoiceConnections(group)?.get(guildId);
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(getVoiceConnection, 'getVoiceConnection');
|
2022-08-01 13:02:58 +07:00
|
|
|
function untrackVoiceConnection(voiceConnection) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return getVoiceConnections(voiceConnection.joinConfig.group)?.delete(voiceConnection.joinConfig.guildId);
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(untrackVoiceConnection, 'untrackVoiceConnection');
|
2022-08-01 13:02:58 +07:00
|
|
|
function trackVoiceConnection(voiceConnection) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return getOrCreateGroup(voiceConnection.joinConfig.group).set(voiceConnection.joinConfig.guildId, voiceConnection);
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(trackVoiceConnection, 'trackVoiceConnection');
|
2022-05-21 21:02:00 +07:00
|
|
|
var FRAME_LENGTH = 20;
|
|
|
|
|
var audioCycleInterval;
|
|
|
|
|
var nextTime = -1;
|
|
|
|
|
var audioPlayers = [];
|
2022-08-01 13:02:58 +07:00
|
|
|
function audioCycleStep() {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (nextTime === -1) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
nextTime += FRAME_LENGTH;
|
2022-12-20 22:20:09 +07:00
|
|
|
const available = audioPlayers.filter(player => player.checkPlayable());
|
2022-09-28 19:40:46 +07:00
|
|
|
for (const player of available) {
|
2022-12-20 22:20:09 +07:00
|
|
|
player['_stepDispatch']();
|
2022-09-28 19:40:46 +07:00
|
|
|
}
|
2022-05-21 21:02:00 +07:00
|
|
|
prepareNextAudioFrame(available);
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(audioCycleStep, 'audioCycleStep');
|
2022-08-01 13:02:58 +07:00
|
|
|
function prepareNextAudioFrame(players) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const nextPlayer = players.shift();
|
2022-08-01 13:02:58 +07:00
|
|
|
if (!nextPlayer) {
|
|
|
|
|
if (nextTime !== -1) {
|
2022-05-21 21:02:00 +07:00
|
|
|
audioCycleInterval = setTimeout(() => audioCycleStep(), nextTime - Date.now());
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
nextPlayer['_stepPrepare']();
|
2022-05-21 21:02:00 +07:00
|
|
|
setImmediate(() => prepareNextAudioFrame(players));
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(prepareNextAudioFrame, 'prepareNextAudioFrame');
|
2022-08-01 13:02:58 +07:00
|
|
|
function hasAudioPlayer(target) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return audioPlayers.includes(target);
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(hasAudioPlayer, 'hasAudioPlayer');
|
2022-08-01 13:02:58 +07:00
|
|
|
function addAudioPlayer(player) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (hasAudioPlayer(player)) return player;
|
2022-05-21 21:02:00 +07:00
|
|
|
audioPlayers.push(player);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (audioPlayers.length === 1) {
|
2022-05-21 21:02:00 +07:00
|
|
|
nextTime = Date.now();
|
|
|
|
|
setImmediate(() => audioCycleStep());
|
|
|
|
|
}
|
|
|
|
|
return player;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(addAudioPlayer, 'addAudioPlayer');
|
2022-08-01 13:02:58 +07:00
|
|
|
function deleteAudioPlayer(player) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const index = audioPlayers.indexOf(player);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (index === -1) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
audioPlayers.splice(index, 1);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (audioPlayers.length === 0) {
|
2022-05-21 21:02:00 +07:00
|
|
|
nextTime = -1;
|
2022-12-20 22:20:09 +07:00
|
|
|
if (typeof audioCycleInterval !== 'undefined') clearTimeout(audioCycleInterval);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(deleteAudioPlayer, 'deleteAudioPlayer');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/networking/Networking.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_buffer3 = require('buffer');
|
|
|
|
|
var import_node_events3 = require('events');
|
|
|
|
|
var import_v42 = require('discord-api-types/voice/v4');
|
2022-09-28 19:40:46 +07:00
|
|
|
|
|
|
|
|
// src/util/Secretbox.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_buffer = require('buffer');
|
2022-09-28 19:40:46 +07:00
|
|
|
var libs = {
|
2022-12-20 22:20:09 +07:00
|
|
|
'sodium-native': sodium => ({
|
2022-09-28 19:40:46 +07:00
|
|
|
open: (buffer, nonce2, secretKey) => {
|
|
|
|
|
if (buffer) {
|
|
|
|
|
const output = import_node_buffer.Buffer.allocUnsafe(buffer.length - sodium.crypto_box_MACBYTES);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (sodium.crypto_secretbox_open_easy(output, buffer, nonce2, secretKey)) return output;
|
2022-09-28 19:40:46 +07:00
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
close: (opusPacket, nonce2, secretKey) => {
|
|
|
|
|
const output = import_node_buffer.Buffer.allocUnsafe(opusPacket.length + sodium.crypto_box_MACBYTES);
|
|
|
|
|
sodium.crypto_secretbox_easy(output, opusPacket, nonce2, secretKey);
|
|
|
|
|
return output;
|
|
|
|
|
},
|
|
|
|
|
random: (num, buffer = import_node_buffer.Buffer.allocUnsafe(num)) => {
|
|
|
|
|
sodium.randombytes_buf(buffer);
|
|
|
|
|
return buffer;
|
2022-12-20 22:20:09 +07:00
|
|
|
},
|
2022-09-28 19:40:46 +07:00
|
|
|
}),
|
2022-12-20 22:20:09 +07:00
|
|
|
sodium: sodium => ({
|
2022-09-28 19:40:46 +07:00
|
|
|
open: sodium.api.crypto_secretbox_open_easy,
|
|
|
|
|
close: sodium.api.crypto_secretbox_easy,
|
|
|
|
|
random: (num, buffer = import_node_buffer.Buffer.allocUnsafe(num)) => {
|
|
|
|
|
sodium.api.randombytes_buf(buffer);
|
|
|
|
|
return buffer;
|
2022-12-20 22:20:09 +07:00
|
|
|
},
|
2022-09-28 19:40:46 +07:00
|
|
|
}),
|
2022-12-20 22:20:09 +07:00
|
|
|
'libsodium-wrappers': sodium => ({
|
2022-09-28 19:40:46 +07:00
|
|
|
open: sodium.crypto_secretbox_open_easy,
|
|
|
|
|
close: sodium.crypto_secretbox_easy,
|
2022-12-20 22:20:09 +07:00
|
|
|
random: sodium.randombytes_buf,
|
2022-09-28 19:40:46 +07:00
|
|
|
}),
|
2022-12-20 22:20:09 +07:00
|
|
|
tweetnacl: tweetnacl => ({
|
2022-09-28 19:40:46 +07:00
|
|
|
open: tweetnacl.secretbox.open,
|
|
|
|
|
close: tweetnacl.secretbox,
|
2022-12-20 22:20:09 +07:00
|
|
|
random: tweetnacl.randomBytes,
|
|
|
|
|
}),
|
2022-09-28 19:40:46 +07:00
|
|
|
};
|
|
|
|
|
var fallbackError = /* @__PURE__ */ __name(() => {
|
2023-03-21 18:47:11 +07:00
|
|
|
throw new Error(`Cannot play audio as no valid encryption package is installed.
|
2022-09-28 19:40:46 +07:00
|
|
|
- Install sodium, libsodium-wrappers, or tweetnacl.
|
|
|
|
|
- Use the generateDependencyReport() function for more information.
|
2023-03-21 18:47:11 +07:00
|
|
|
`);
|
2022-12-20 22:20:09 +07:00
|
|
|
}, 'fallbackError');
|
2022-09-28 19:40:46 +07:00
|
|
|
var methods = {
|
|
|
|
|
open: fallbackError,
|
|
|
|
|
close: fallbackError,
|
2022-12-20 22:20:09 +07:00
|
|
|
random: fallbackError,
|
2022-09-28 19:40:46 +07:00
|
|
|
};
|
|
|
|
|
void (async () => {
|
|
|
|
|
for (const libName of Object.keys(libs)) {
|
|
|
|
|
try {
|
|
|
|
|
const lib = require(libName);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready;
|
2022-09-28 19:40:46 +07:00
|
|
|
Object.assign(methods, libs[libName](lib));
|
|
|
|
|
break;
|
2022-12-20 22:20:09 +07:00
|
|
|
} catch {}
|
2022-09-28 19:40:46 +07:00
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
// src/util/util.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var noop = /* @__PURE__ */ __name(() => {}, 'noop');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/networking/VoiceUDPSocket.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_buffer2 = require('buffer');
|
|
|
|
|
var import_node_dgram = require('dgram');
|
|
|
|
|
var import_node_events = require('events');
|
|
|
|
|
var import_node_net = require('net');
|
2022-08-01 13:02:58 +07:00
|
|
|
function parseLocalPacket(message) {
|
2022-09-28 19:40:46 +07:00
|
|
|
const packet = import_node_buffer2.Buffer.from(message);
|
2022-12-20 22:20:09 +07:00
|
|
|
const ip = packet.slice(8, packet.indexOf(0, 8)).toString('utf8');
|
2022-08-01 13:02:58 +07:00
|
|
|
if (!(0, import_node_net.isIPv4)(ip)) {
|
2022-12-20 22:20:09 +07:00
|
|
|
throw new Error('Malformed IP address');
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
const port = packet.readUInt16BE(packet.length - 2);
|
2023-03-21 18:47:11 +07:00
|
|
|
return {
|
|
|
|
|
ip,
|
|
|
|
|
port,
|
|
|
|
|
};
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(parseLocalPacket, 'parseLocalPacket');
|
2022-05-21 21:02:00 +07:00
|
|
|
var KEEP_ALIVE_INTERVAL = 5e3;
|
|
|
|
|
var MAX_COUNTER_VALUE = 2 ** 32 - 1;
|
2022-08-01 13:02:58 +07:00
|
|
|
var VoiceUDPSocket = class extends import_node_events.EventEmitter {
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* The counter used in the keep alive mechanism.
|
|
|
|
|
*/
|
2022-09-28 19:40:46 +07:00
|
|
|
keepAliveCounter = 0;
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Creates a new VoiceUDPSocket.
|
|
|
|
|
*
|
|
|
|
|
* @param remote - Details of the remote socket
|
|
|
|
|
*/
|
|
|
|
|
constructor(remote) {
|
2022-05-21 21:02:00 +07:00
|
|
|
super();
|
2022-12-20 22:20:09 +07:00
|
|
|
this.socket = (0, import_node_dgram.createSocket)('udp4');
|
|
|
|
|
this.socket.on('error', error => this.emit('error', error));
|
|
|
|
|
this.socket.on('message', buffer => this.onMessage(buffer));
|
|
|
|
|
this.socket.on('close', () => this.emit('close'));
|
2022-05-21 21:02:00 +07:00
|
|
|
this.remote = remote;
|
2022-09-28 19:40:46 +07:00
|
|
|
this.keepAliveBuffer = import_node_buffer2.Buffer.alloc(8);
|
2022-05-21 21:02:00 +07:00
|
|
|
this.keepAliveInterval = setInterval(() => this.keepAlive(), KEEP_ALIVE_INTERVAL);
|
|
|
|
|
setImmediate(() => this.keepAlive());
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when a message is received on the UDP socket.
|
|
|
|
|
*
|
|
|
|
|
* @param buffer - The received buffer
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onMessage(buffer) {
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('message', buffer);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called at a regular interval to check whether we are still able to send datagrams to Discord.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
keepAlive() {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0);
|
|
|
|
|
this.send(this.keepAliveBuffer);
|
|
|
|
|
this.keepAliveCounter++;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (this.keepAliveCounter > MAX_COUNTER_VALUE) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.keepAliveCounter = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Sends a buffer to Discord.
|
|
|
|
|
*
|
|
|
|
|
* @param buffer - The buffer to send
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
send(buffer) {
|
2022-09-28 19:40:46 +07:00
|
|
|
this.socket.send(buffer, this.remote.port, this.remote.ip);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Closes the socket, the instance will not be able to be reused.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
destroy() {
|
|
|
|
|
try {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.socket.close();
|
2022-12-20 22:20:09 +07:00
|
|
|
} catch {}
|
2022-05-21 21:02:00 +07:00
|
|
|
clearInterval(this.keepAliveInterval);
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Performs IP discovery to discover the local address and port to be used for the voice connection.
|
|
|
|
|
*
|
|
|
|
|
* @param ssrc - The SSRC received from Discord
|
|
|
|
|
*/
|
2022-09-28 19:40:46 +07:00
|
|
|
async performIPDiscovery(ssrc) {
|
2022-08-01 13:02:58 +07:00
|
|
|
return new Promise((resolve2, reject) => {
|
2022-12-20 22:20:09 +07:00
|
|
|
const listener = /* @__PURE__ */ __name(message => {
|
2022-08-01 13:02:58 +07:00
|
|
|
try {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (message.readUInt16BE(0) !== 2) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
const packet = parseLocalPacket(message);
|
2022-12-20 22:20:09 +07:00
|
|
|
this.socket.off('message', listener);
|
2022-05-21 21:02:00 +07:00
|
|
|
resolve2(packet);
|
2022-12-20 22:20:09 +07:00
|
|
|
} catch {}
|
|
|
|
|
}, 'listener');
|
|
|
|
|
this.socket.on('message', listener);
|
|
|
|
|
this.socket.once('close', () => reject(new Error('Cannot perform IP discovery - socket closed')));
|
2022-09-28 19:40:46 +07:00
|
|
|
const discoveryBuffer = import_node_buffer2.Buffer.alloc(74);
|
2022-05-21 21:02:00 +07:00
|
|
|
discoveryBuffer.writeUInt16BE(1, 0);
|
|
|
|
|
discoveryBuffer.writeUInt16BE(70, 2);
|
|
|
|
|
discoveryBuffer.writeUInt32BE(ssrc, 4);
|
|
|
|
|
this.send(discoveryBuffer);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(VoiceUDPSocket, 'VoiceUDPSocket');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/networking/VoiceWebSocket.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_events2 = require('events');
|
|
|
|
|
var import_v4 = require('discord-api-types/voice/v4');
|
|
|
|
|
var import_ws = __toESM(require('ws'));
|
2022-08-01 13:02:58 +07:00
|
|
|
var VoiceWebSocket = class extends import_node_events2.EventEmitter {
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* The number of consecutively missed heartbeats.
|
|
|
|
|
*/
|
2022-09-28 19:40:46 +07:00
|
|
|
missedHeartbeats = 0;
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Creates a new VoiceWebSocket.
|
|
|
|
|
*
|
|
|
|
|
* @param address - The address to connect to
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
constructor(address, debug) {
|
2022-05-21 21:02:00 +07:00
|
|
|
super();
|
|
|
|
|
this.ws = new import_ws.default(address);
|
2022-12-20 22:20:09 +07:00
|
|
|
this.ws.onmessage = err => this.onMessage(err);
|
|
|
|
|
this.ws.onopen = err => this.emit('open', err);
|
|
|
|
|
this.ws.onerror = err => this.emit('error', err instanceof Error ? err : err.error);
|
|
|
|
|
this.ws.onclose = err => this.emit('close', err);
|
2022-05-21 21:02:00 +07:00
|
|
|
this.lastHeartbeatAck = 0;
|
|
|
|
|
this.lastHeartbeatSend = 0;
|
2022-12-20 22:20:09 +07:00
|
|
|
this.debug = debug ? message => this.emit('debug', message) : null;
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
destroy() {
|
|
|
|
|
try {
|
2022-12-20 22:20:09 +07:00
|
|
|
this.debug?.('destroyed');
|
2022-05-21 21:02:00 +07:00
|
|
|
this.setHeartbeatInterval(-1);
|
|
|
|
|
this.ws.close(1e3);
|
2022-08-01 13:02:58 +07:00
|
|
|
} catch (error) {
|
2022-09-28 19:40:46 +07:00
|
|
|
const err = error;
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('error', err);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Handles message events on the WebSocket. Attempts to JSON parse the messages and emit them
|
|
|
|
|
* as packets.
|
|
|
|
|
*
|
|
|
|
|
* @param event - The message event
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onMessage(event) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (typeof event.data !== 'string') return;
|
2022-05-21 21:02:00 +07:00
|
|
|
this.debug?.(`<< ${event.data}`);
|
|
|
|
|
let packet;
|
2022-08-01 13:02:58 +07:00
|
|
|
try {
|
2022-05-21 21:02:00 +07:00
|
|
|
packet = JSON.parse(event.data);
|
2022-08-01 13:02:58 +07:00
|
|
|
} catch (error) {
|
2022-09-28 19:40:46 +07:00
|
|
|
const err = error;
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('error', err);
|
2022-05-21 21:02:00 +07:00
|
|
|
return;
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
if (packet.op === import_v4.VoiceOpcodes.HeartbeatAck) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.lastHeartbeatAck = Date.now();
|
|
|
|
|
this.missedHeartbeats = 0;
|
|
|
|
|
this.ping = this.lastHeartbeatAck - this.lastHeartbeatSend;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('packet', packet);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Sends a JSON-stringifiable packet over the WebSocket.
|
|
|
|
|
*
|
|
|
|
|
* @param packet - The packet to send
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
sendPacket(packet) {
|
|
|
|
|
try {
|
2022-05-21 21:02:00 +07:00
|
|
|
const stringified = JSON.stringify(packet);
|
|
|
|
|
this.debug?.(`>> ${stringified}`);
|
2022-09-28 19:40:46 +07:00
|
|
|
this.ws.send(stringified);
|
|
|
|
|
return;
|
2022-08-01 13:02:58 +07:00
|
|
|
} catch (error) {
|
2022-09-28 19:40:46 +07:00
|
|
|
const err = error;
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('error', err);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Sends a heartbeat over the WebSocket.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
sendHeartbeat() {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.lastHeartbeatSend = Date.now();
|
|
|
|
|
this.missedHeartbeats++;
|
|
|
|
|
const nonce2 = this.lastHeartbeatSend;
|
2022-09-28 19:40:46 +07:00
|
|
|
this.sendPacket({
|
2022-05-21 21:02:00 +07:00
|
|
|
op: import_v4.VoiceOpcodes.Heartbeat,
|
2023-03-21 18:47:11 +07:00
|
|
|
// eslint-disable-next-line id-length
|
2022-12-20 22:20:09 +07:00
|
|
|
d: nonce2,
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Sets/clears an interval to send heartbeats over the WebSocket.
|
|
|
|
|
*
|
|
|
|
|
* @param ms - The interval in milliseconds. If negative, the interval will be unset
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
setHeartbeatInterval(ms) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (typeof this.heartbeatInterval !== 'undefined') clearInterval(this.heartbeatInterval);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (ms > 0) {
|
|
|
|
|
this.heartbeatInterval = setInterval(() => {
|
|
|
|
|
if (this.lastHeartbeatSend !== 0 && this.missedHeartbeats >= 3) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.ws.close();
|
|
|
|
|
this.setHeartbeatInterval(-1);
|
|
|
|
|
}
|
|
|
|
|
this.sendHeartbeat();
|
|
|
|
|
}, ms);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(VoiceWebSocket, 'VoiceWebSocket');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/networking/Networking.ts
|
2022-05-21 21:02:00 +07:00
|
|
|
var CHANNELS = 2;
|
2022-12-20 22:20:09 +07:00
|
|
|
var TIMESTAMP_INC = (48e3 / 100) * CHANNELS;
|
2022-05-21 21:02:00 +07:00
|
|
|
var MAX_NONCE_SIZE = 2 ** 32 - 1;
|
2022-12-20 22:20:09 +07:00
|
|
|
var SUPPORTED_ENCRYPTION_MODES = ['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305'];
|
2023-03-21 18:47:11 +07:00
|
|
|
var NetworkingStatusCode;
|
|
|
|
|
(function (NetworkingStatusCode2) {
|
|
|
|
|
NetworkingStatusCode2[(NetworkingStatusCode2['OpeningWs'] = 0)] = 'OpeningWs';
|
|
|
|
|
NetworkingStatusCode2[(NetworkingStatusCode2['Identifying'] = 1)] = 'Identifying';
|
|
|
|
|
NetworkingStatusCode2[(NetworkingStatusCode2['UdpHandshaking'] = 2)] = 'UdpHandshaking';
|
|
|
|
|
NetworkingStatusCode2[(NetworkingStatusCode2['SelectingProtocol'] = 3)] = 'SelectingProtocol';
|
|
|
|
|
NetworkingStatusCode2[(NetworkingStatusCode2['Ready'] = 4)] = 'Ready';
|
|
|
|
|
NetworkingStatusCode2[(NetworkingStatusCode2['Resuming'] = 5)] = 'Resuming';
|
|
|
|
|
NetworkingStatusCode2[(NetworkingStatusCode2['Closed'] = 6)] = 'Closed';
|
|
|
|
|
})(NetworkingStatusCode || (NetworkingStatusCode = {}));
|
2022-09-28 19:40:46 +07:00
|
|
|
var nonce = import_node_buffer3.Buffer.alloc(24);
|
2022-08-01 13:02:58 +07:00
|
|
|
function stringifyState(state) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return JSON.stringify({
|
|
|
|
|
...state,
|
2022-12-20 22:20:09 +07:00
|
|
|
ws: Reflect.has(state, 'ws'),
|
|
|
|
|
udp: Reflect.has(state, 'udp'),
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(stringifyState, 'stringifyState');
|
2022-08-01 13:02:58 +07:00
|
|
|
function chooseEncryptionMode(options) {
|
2022-12-20 22:20:09 +07:00
|
|
|
const option = options.find(option2 => SUPPORTED_ENCRYPTION_MODES.includes(option2));
|
2022-08-01 13:02:58 +07:00
|
|
|
if (!option) {
|
2022-12-20 22:20:09 +07:00
|
|
|
throw new Error(`No compatible encryption modes. Available include: ${options.join(', ')}`);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
return option;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(chooseEncryptionMode, 'chooseEncryptionMode');
|
2022-09-28 19:40:46 +07:00
|
|
|
function randomNBit(numberOfBits) {
|
|
|
|
|
return Math.floor(Math.random() * 2 ** numberOfBits);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(randomNBit, 'randomNBit');
|
2022-08-01 13:02:58 +07:00
|
|
|
var Networking = class extends import_node_events3.EventEmitter {
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Creates a new Networking instance.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
constructor(options, debug) {
|
2022-05-21 21:02:00 +07:00
|
|
|
super();
|
|
|
|
|
this.onWsOpen = this.onWsOpen.bind(this);
|
|
|
|
|
this.onChildError = this.onChildError.bind(this);
|
|
|
|
|
this.onWsPacket = this.onWsPacket.bind(this);
|
|
|
|
|
this.onWsClose = this.onWsClose.bind(this);
|
|
|
|
|
this.onWsDebug = this.onWsDebug.bind(this);
|
|
|
|
|
this.onUdpDebug = this.onUdpDebug.bind(this);
|
|
|
|
|
this.onUdpClose = this.onUdpClose.bind(this);
|
2022-12-20 22:20:09 +07:00
|
|
|
this.debug = debug ? message => this.emit('debug', message) : null;
|
2022-05-21 21:02:00 +07:00
|
|
|
this._state = {
|
2023-03-21 18:47:11 +07:00
|
|
|
code: NetworkingStatusCode.OpeningWs,
|
2022-05-21 21:02:00 +07:00
|
|
|
ws: this.createWebSocket(options.endpoint),
|
2022-12-20 22:20:09 +07:00
|
|
|
connectionOptions: options,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Destroys the Networking instance, transitioning it into the Closed state.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
destroy() {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
2023-03-21 18:47:11 +07:00
|
|
|
code: NetworkingStatusCode.Closed,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* The current state of the networking instance.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
get state() {
|
2022-05-21 21:02:00 +07:00
|
|
|
return this._state;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Sets a new state for the networking instance, performing clean-up operations where necessary.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
set state(newState) {
|
2022-12-20 22:20:09 +07:00
|
|
|
const oldWs = Reflect.get(this._state, 'ws');
|
|
|
|
|
const newWs = Reflect.get(newState, 'ws');
|
2022-08-01 13:02:58 +07:00
|
|
|
if (oldWs && oldWs !== newWs) {
|
2022-12-20 22:20:09 +07:00
|
|
|
oldWs.off('debug', this.onWsDebug);
|
|
|
|
|
oldWs.on('error', noop);
|
|
|
|
|
oldWs.off('error', this.onChildError);
|
|
|
|
|
oldWs.off('open', this.onWsOpen);
|
|
|
|
|
oldWs.off('packet', this.onWsPacket);
|
|
|
|
|
oldWs.off('close', this.onWsClose);
|
2022-05-21 21:02:00 +07:00
|
|
|
oldWs.destroy();
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
const oldUdp = Reflect.get(this._state, 'udp');
|
|
|
|
|
const newUdp = Reflect.get(newState, 'udp');
|
2022-08-01 13:02:58 +07:00
|
|
|
if (oldUdp && oldUdp !== newUdp) {
|
2022-12-20 22:20:09 +07:00
|
|
|
oldUdp.on('error', noop);
|
|
|
|
|
oldUdp.off('error', this.onChildError);
|
|
|
|
|
oldUdp.off('close', this.onUdpClose);
|
|
|
|
|
oldUdp.off('debug', this.onUdpDebug);
|
2022-05-21 21:02:00 +07:00
|
|
|
oldUdp.destroy();
|
|
|
|
|
}
|
|
|
|
|
const oldState = this._state;
|
|
|
|
|
this._state = newState;
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('stateChange', oldState, newState);
|
2022-05-21 21:02:00 +07:00
|
|
|
this.debug?.(`state change:
|
|
|
|
|
from ${stringifyState(oldState)}
|
|
|
|
|
to ${stringifyState(newState)}`);
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Creates a new WebSocket to a Discord Voice gateway.
|
|
|
|
|
*
|
|
|
|
|
* @param endpoint - The endpoint to connect to
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
createWebSocket(endpoint) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const ws = new VoiceWebSocket(`wss://${endpoint}?v=4`, Boolean(this.debug));
|
2022-12-20 22:20:09 +07:00
|
|
|
ws.on('error', this.onChildError);
|
|
|
|
|
ws.once('open', this.onWsOpen);
|
|
|
|
|
ws.on('packet', this.onWsPacket);
|
|
|
|
|
ws.once('close', this.onWsClose);
|
|
|
|
|
ws.on('debug', this.onWsDebug);
|
2022-05-21 21:02:00 +07:00
|
|
|
return ws;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Propagates errors from the children VoiceWebSocket and VoiceUDPSocket.
|
|
|
|
|
*
|
|
|
|
|
* @param error - The error that was emitted by a child
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onChildError(error) {
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('error', error);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when the WebSocket opens. Depending on the state that the instance is in,
|
|
|
|
|
* it will either identify with a new session, or it will attempt to resume an existing session.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onWsOpen() {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.code === NetworkingStatusCode.OpeningWs) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const packet = {
|
|
|
|
|
op: import_v42.VoiceOpcodes.Identify,
|
|
|
|
|
d: {
|
|
|
|
|
server_id: this.state.connectionOptions.serverId,
|
|
|
|
|
user_id: this.state.connectionOptions.userId,
|
|
|
|
|
session_id: this.state.connectionOptions.sessionId,
|
2022-12-20 22:20:09 +07:00
|
|
|
token: this.state.connectionOptions.token,
|
|
|
|
|
},
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
this.state.ws.sendPacket(packet);
|
|
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
code: NetworkingStatusCode.Identifying,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2023-03-21 18:47:11 +07:00
|
|
|
} else if (this.state.code === NetworkingStatusCode.Resuming) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const packet = {
|
|
|
|
|
op: import_v42.VoiceOpcodes.Resume,
|
|
|
|
|
d: {
|
|
|
|
|
server_id: this.state.connectionOptions.serverId,
|
|
|
|
|
session_id: this.state.connectionOptions.sessionId,
|
2022-12-20 22:20:09 +07:00
|
|
|
token: this.state.connectionOptions.token,
|
|
|
|
|
},
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
this.state.ws.sendPacket(packet);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when the WebSocket closes. Based on the reason for closing (given by the code parameter),
|
|
|
|
|
* the instance will either attempt to resume, or enter the closed state and emit a 'close' event
|
|
|
|
|
* with the close code, allowing the user to decide whether or not they would like to reconnect.
|
|
|
|
|
*
|
|
|
|
|
* @param code - The close code
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onWsClose({ code }) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const canResume = code === 4015 || code < 4e3;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (canResume && this.state.code === NetworkingStatusCode.Ready) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
code: NetworkingStatusCode.Resuming,
|
2022-12-20 22:20:09 +07:00
|
|
|
ws: this.createWebSocket(this.state.connectionOptions.endpoint),
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2023-03-21 18:47:11 +07:00
|
|
|
} else if (this.state.code !== NetworkingStatusCode.Closed) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.destroy();
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('close', code);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when the UDP socket has closed itself if it has stopped receiving replies from Discord.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onUdpClose() {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.code === NetworkingStatusCode.Ready) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
code: NetworkingStatusCode.Resuming,
|
2022-12-20 22:20:09 +07:00
|
|
|
ws: this.createWebSocket(this.state.connectionOptions.endpoint),
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when a packet is received on the connection's WebSocket.
|
|
|
|
|
*
|
|
|
|
|
* @param packet - The received packet
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onWsPacket(packet) {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (packet.op === import_v42.VoiceOpcodes.Hello && this.state.code !== NetworkingStatusCode.Closed) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval);
|
2023-03-21 18:47:11 +07:00
|
|
|
} else if (packet.op === import_v42.VoiceOpcodes.Ready && this.state.code === NetworkingStatusCode.Identifying) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const { ip, port, ssrc, modes } = packet.d;
|
2023-03-21 18:47:11 +07:00
|
|
|
const udp = new VoiceUDPSocket({
|
|
|
|
|
ip,
|
|
|
|
|
port,
|
|
|
|
|
});
|
2022-12-20 22:20:09 +07:00
|
|
|
udp.on('error', this.onChildError);
|
|
|
|
|
udp.on('debug', this.onUdpDebug);
|
|
|
|
|
udp.once('close', this.onUdpClose);
|
|
|
|
|
udp
|
|
|
|
|
.performIPDiscovery(ssrc)
|
|
|
|
|
.then(localConfig => {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.code !== NetworkingStatusCode.UdpHandshaking) return;
|
2022-12-20 22:20:09 +07:00
|
|
|
this.state.ws.sendPacket({
|
|
|
|
|
op: import_v42.VoiceOpcodes.SelectProtocol,
|
|
|
|
|
d: {
|
|
|
|
|
protocol: 'udp',
|
|
|
|
|
data: {
|
|
|
|
|
address: localConfig.ip,
|
|
|
|
|
port: localConfig.port,
|
|
|
|
|
mode: chooseEncryptionMode(modes),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
code: NetworkingStatusCode.SelectingProtocol,
|
2022-12-20 22:20:09 +07:00
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.catch(error => this.emit('error', error));
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
code: NetworkingStatusCode.UdpHandshaking,
|
2022-05-21 21:02:00 +07:00
|
|
|
udp,
|
|
|
|
|
connectionData: {
|
2022-12-20 22:20:09 +07:00
|
|
|
ssrc,
|
|
|
|
|
},
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
} else if (
|
|
|
|
|
packet.op === import_v42.VoiceOpcodes.SessionDescription &&
|
2023-03-21 18:47:11 +07:00
|
|
|
this.state.code === NetworkingStatusCode.SelectingProtocol
|
2022-12-20 22:20:09 +07:00
|
|
|
) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const { mode: encryptionMode, secret_key: secretKey } = packet.d;
|
|
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
code: NetworkingStatusCode.Ready,
|
2022-05-21 21:02:00 +07:00
|
|
|
connectionData: {
|
|
|
|
|
...this.state.connectionData,
|
|
|
|
|
encryptionMode,
|
|
|
|
|
secretKey: new Uint8Array(secretKey),
|
|
|
|
|
sequence: randomNBit(16),
|
|
|
|
|
timestamp: randomNBit(32),
|
|
|
|
|
nonce: 0,
|
2022-09-28 19:40:46 +07:00
|
|
|
nonceBuffer: import_node_buffer3.Buffer.alloc(24),
|
2022-05-21 21:02:00 +07:00
|
|
|
speaking: false,
|
2022-12-20 22:20:09 +07:00
|
|
|
packetsPlayed: 0,
|
|
|
|
|
},
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2023-03-21 18:47:11 +07:00
|
|
|
} else if (packet.op === import_v42.VoiceOpcodes.Resumed && this.state.code === NetworkingStatusCode.Resuming) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
code: NetworkingStatusCode.Ready,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
this.state.connectionData.speaking = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Propagates debug messages from the child WebSocket.
|
|
|
|
|
*
|
|
|
|
|
* @param message - The emitted debug message
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onWsDebug(message) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.debug?.(`[WS] ${message}`);
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Propagates debug messages from the child UDPSocket.
|
|
|
|
|
*
|
|
|
|
|
* @param message - The emitted debug message
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onUdpDebug(message) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.debug?.(`[UDP] ${message}`);
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it.
|
|
|
|
|
* It will be stored within the instance, and can be played by dispatchAudio()
|
|
|
|
|
*
|
|
|
|
|
* @remarks
|
|
|
|
|
* Calling this method while there is already a prepared audio packet that has not yet been dispatched
|
|
|
|
|
* will overwrite the existing audio packet. This should be avoided.
|
|
|
|
|
* @param opusPacket - The Opus packet to encrypt
|
|
|
|
|
* @returns The audio packet that was prepared
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
prepareAudioPacket(opusPacket) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const state = this.state;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.code !== NetworkingStatusCode.Ready) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
state.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData);
|
|
|
|
|
return state.preparedPacket;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet
|
|
|
|
|
* is consumed and cannot be dispatched again.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
dispatchAudio() {
|
2022-05-21 21:02:00 +07:00
|
|
|
const state = this.state;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.code !== NetworkingStatusCode.Ready) return false;
|
2022-12-20 22:20:09 +07:00
|
|
|
if (typeof state.preparedPacket !== 'undefined') {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.playAudioPacket(state.preparedPacket);
|
|
|
|
|
state.preparedPacket = void 0;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Plays an audio packet, updating timing metadata used for playback.
|
|
|
|
|
*
|
|
|
|
|
* @param audioPacket - The audio packet to play
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
playAudioPacket(audioPacket) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const state = this.state;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.code !== NetworkingStatusCode.Ready) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
const { connectionData } = state;
|
|
|
|
|
connectionData.packetsPlayed++;
|
|
|
|
|
connectionData.sequence++;
|
|
|
|
|
connectionData.timestamp += TIMESTAMP_INC;
|
2022-12-20 22:20:09 +07:00
|
|
|
if (connectionData.sequence >= 2 ** 16) connectionData.sequence = 0;
|
|
|
|
|
if (connectionData.timestamp >= 2 ** 32) connectionData.timestamp = 0;
|
2022-05-21 21:02:00 +07:00
|
|
|
this.setSpeaking(true);
|
|
|
|
|
state.udp.send(audioPacket);
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Sends a packet to the voice gateway indicating that the client has start/stopped sending
|
|
|
|
|
* audio.
|
|
|
|
|
*
|
|
|
|
|
* @param speaking - Whether or not the client should be shown as speaking
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
setSpeaking(speaking) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const state = this.state;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.code !== NetworkingStatusCode.Ready) return;
|
2022-12-20 22:20:09 +07:00
|
|
|
if (state.connectionData.speaking === speaking) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
state.connectionData.speaking = speaking;
|
|
|
|
|
state.ws.sendPacket({
|
|
|
|
|
op: import_v42.VoiceOpcodes.Speaking,
|
|
|
|
|
d: {
|
|
|
|
|
speaking: speaking ? 1 : 0,
|
|
|
|
|
delay: 0,
|
2022-12-20 22:20:09 +07:00
|
|
|
ssrc: state.connectionData.ssrc,
|
|
|
|
|
},
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Creates a new audio packet from an Opus packet. This involves encrypting the packet,
|
|
|
|
|
* then prepending a header that includes metadata.
|
|
|
|
|
*
|
|
|
|
|
* @param opusPacket - The Opus packet to prepare
|
|
|
|
|
* @param connectionData - The current connection data of the instance
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
createAudioPacket(opusPacket, connectionData) {
|
2022-09-28 19:40:46 +07:00
|
|
|
const packetBuffer = import_node_buffer3.Buffer.alloc(12);
|
2022-05-21 21:02:00 +07:00
|
|
|
packetBuffer[0] = 128;
|
|
|
|
|
packetBuffer[1] = 120;
|
|
|
|
|
const { sequence, timestamp, ssrc } = connectionData;
|
|
|
|
|
packetBuffer.writeUIntBE(sequence, 2, 2);
|
|
|
|
|
packetBuffer.writeUIntBE(timestamp, 4, 4);
|
|
|
|
|
packetBuffer.writeUIntBE(ssrc, 8, 4);
|
|
|
|
|
packetBuffer.copy(nonce, 0, 0, 12);
|
2022-09-28 19:40:46 +07:00
|
|
|
return import_node_buffer3.Buffer.concat([packetBuffer, ...this.encryptOpusPacket(opusPacket, connectionData)]);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Encrypts an Opus packet using the format agreed upon by the instance and Discord.
|
|
|
|
|
*
|
|
|
|
|
* @param opusPacket - The Opus packet to encrypt
|
|
|
|
|
* @param connectionData - The current connection data of the instance
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
encryptOpusPacket(opusPacket, connectionData) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const { secretKey, encryptionMode } = connectionData;
|
2022-12-20 22:20:09 +07:00
|
|
|
if (encryptionMode === 'xsalsa20_poly1305_lite') {
|
2022-05-21 21:02:00 +07:00
|
|
|
connectionData.nonce++;
|
2022-12-20 22:20:09 +07:00
|
|
|
if (connectionData.nonce > MAX_NONCE_SIZE) connectionData.nonce = 0;
|
2022-05-21 21:02:00 +07:00
|
|
|
connectionData.nonceBuffer.writeUInt32BE(connectionData.nonce, 0);
|
2022-12-20 22:20:09 +07:00
|
|
|
return [methods.close(opusPacket, connectionData.nonceBuffer, secretKey), connectionData.nonceBuffer.slice(0, 4)];
|
|
|
|
|
} else if (encryptionMode === 'xsalsa20_poly1305_suffix') {
|
2022-05-21 21:02:00 +07:00
|
|
|
const random = methods.random(24, connectionData.nonceBuffer);
|
|
|
|
|
return [methods.close(opusPacket, random, secretKey), random];
|
|
|
|
|
}
|
|
|
|
|
return [methods.close(opusPacket, nonce, secretKey)];
|
|
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(Networking, 'Networking');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/receive/VoiceReceiver.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_buffer5 = require('buffer');
|
|
|
|
|
var import_v43 = require('discord-api-types/voice/v4');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/receive/AudioReceiveStream.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_stream = require('stream');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/audio/AudioPlayer.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_buffer4 = require('buffer');
|
|
|
|
|
var import_node_events4 = require('events');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/audio/AudioPlayerError.ts
|
2022-08-01 13:02:58 +07:00
|
|
|
var AudioPlayerError = class extends Error {
|
|
|
|
|
constructor(error, resource) {
|
2022-05-21 21:02:00 +07:00
|
|
|
super(error.message);
|
|
|
|
|
this.resource = resource;
|
|
|
|
|
this.name = error.name;
|
|
|
|
|
this.stack = error.stack;
|
|
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(AudioPlayerError, 'AudioPlayerError');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/audio/PlayerSubscription.ts
|
2022-08-01 13:02:58 +07:00
|
|
|
var PlayerSubscription = class {
|
|
|
|
|
constructor(connection, player) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.connection = connection;
|
|
|
|
|
this.player = player;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Unsubscribes the connection from the audio player, meaning that the
|
|
|
|
|
* audio player cannot stream audio to it until a new subscription is made.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
unsubscribe() {
|
2022-12-20 22:20:09 +07:00
|
|
|
this.connection['onSubscriptionRemoved'](this);
|
|
|
|
|
this.player['unsubscribe'](this);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(PlayerSubscription, 'PlayerSubscription');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/audio/AudioPlayer.ts
|
2022-09-28 19:40:46 +07:00
|
|
|
var SILENCE_FRAME = import_node_buffer4.Buffer.from([248, 255, 254]);
|
2023-03-21 18:47:11 +07:00
|
|
|
var NoSubscriberBehavior;
|
|
|
|
|
(function (NoSubscriberBehavior2) {
|
|
|
|
|
NoSubscriberBehavior2[
|
|
|
|
|
/**
|
|
|
|
|
* Pauses playing the stream until a voice connection becomes available.
|
|
|
|
|
*/
|
|
|
|
|
'Pause'
|
|
|
|
|
] = 'pause';
|
|
|
|
|
NoSubscriberBehavior2[
|
|
|
|
|
/**
|
|
|
|
|
* Continues to play through the resource regardless.
|
|
|
|
|
*/
|
|
|
|
|
'Play'
|
|
|
|
|
] = 'play';
|
|
|
|
|
NoSubscriberBehavior2[
|
|
|
|
|
/**
|
|
|
|
|
* The player stops and enters the Idle state.
|
|
|
|
|
*/
|
|
|
|
|
'Stop'
|
|
|
|
|
] = 'stop';
|
|
|
|
|
})(NoSubscriberBehavior || (NoSubscriberBehavior = {}));
|
|
|
|
|
var AudioPlayerStatus;
|
|
|
|
|
(function (AudioPlayerStatus2) {
|
|
|
|
|
AudioPlayerStatus2[
|
|
|
|
|
/**
|
|
|
|
|
* When the player has paused itself. Only possible with the "pause" no subscriber behavior.
|
|
|
|
|
*/
|
|
|
|
|
'AutoPaused'
|
|
|
|
|
] = 'autopaused';
|
|
|
|
|
AudioPlayerStatus2[
|
|
|
|
|
/**
|
|
|
|
|
* When the player is waiting for an audio resource to become readable before transitioning to Playing.
|
|
|
|
|
*/
|
|
|
|
|
'Buffering'
|
|
|
|
|
] = 'buffering';
|
|
|
|
|
AudioPlayerStatus2[
|
|
|
|
|
/**
|
|
|
|
|
* When there is currently no resource for the player to be playing.
|
|
|
|
|
*/
|
|
|
|
|
'Idle'
|
|
|
|
|
] = 'idle';
|
|
|
|
|
AudioPlayerStatus2[
|
|
|
|
|
/**
|
|
|
|
|
* When the player has been manually paused.
|
|
|
|
|
*/
|
|
|
|
|
'Paused'
|
|
|
|
|
] = 'paused';
|
|
|
|
|
AudioPlayerStatus2[
|
|
|
|
|
/**
|
|
|
|
|
* When the player is actively playing an audio resource.
|
|
|
|
|
*/
|
|
|
|
|
'Playing'
|
|
|
|
|
] = 'playing';
|
|
|
|
|
})(AudioPlayerStatus || (AudioPlayerStatus = {}));
|
2022-08-01 13:02:58 +07:00
|
|
|
function stringifyState2(state) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return JSON.stringify({
|
|
|
|
|
...state,
|
2022-12-20 22:20:09 +07:00
|
|
|
resource: Reflect.has(state, 'resource'),
|
|
|
|
|
stepTimeout: Reflect.has(state, 'stepTimeout'),
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(stringifyState2, 'stringifyState');
|
2022-09-28 19:40:46 +07:00
|
|
|
var AudioPlayer = class extends import_node_events4.EventEmitter {
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio
|
|
|
|
|
* to the streams in this list.
|
|
|
|
|
*/
|
2022-09-28 19:40:46 +07:00
|
|
|
subscribers = [];
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Creates a new AudioPlayer.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
constructor(options = {}) {
|
2022-05-21 21:02:00 +07:00
|
|
|
super();
|
2023-03-21 18:47:11 +07:00
|
|
|
this._state = {
|
|
|
|
|
status: AudioPlayerStatus.Idle,
|
|
|
|
|
};
|
2022-05-21 21:02:00 +07:00
|
|
|
this.behaviors = {
|
2023-03-21 18:47:11 +07:00
|
|
|
noSubscriber: NoSubscriberBehavior.Pause,
|
2022-05-21 21:02:00 +07:00
|
|
|
maxMissedFrames: 5,
|
2022-12-20 22:20:09 +07:00
|
|
|
...options.behaviors,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
this.debug = options.debug === false ? null : message => this.emit('debug', message);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* A list of subscribed voice connections that can currently receive audio to play.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
get playable() {
|
2022-12-20 22:20:09 +07:00
|
|
|
return this.subscribers
|
2023-03-21 18:47:11 +07:00
|
|
|
.filter(({ connection }) => connection.state.status === VoiceConnectionStatus.Ready)
|
2022-12-20 22:20:09 +07:00
|
|
|
.map(({ connection }) => connection);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Subscribes a VoiceConnection to the audio player's play list. If the VoiceConnection is already subscribed,
|
|
|
|
|
* then the existing subscription is used.
|
|
|
|
|
*
|
|
|
|
|
* @remarks
|
|
|
|
|
* This method should not be directly called. Instead, use VoiceConnection#subscribe.
|
|
|
|
|
* @param connection - The connection to subscribe
|
|
|
|
|
* @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription
|
|
|
|
|
*/
|
|
|
|
|
// @ts-ignore
|
2022-08-01 13:02:58 +07:00
|
|
|
subscribe(connection) {
|
2022-12-20 22:20:09 +07:00
|
|
|
const existingSubscription = this.subscribers.find(subscription => subscription.connection === connection);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (!existingSubscription) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const subscription = new PlayerSubscription(connection, this);
|
|
|
|
|
this.subscribers.push(subscription);
|
2022-12-20 22:20:09 +07:00
|
|
|
setImmediate(() => this.emit('subscribe', subscription));
|
2022-05-21 21:02:00 +07:00
|
|
|
return subscription;
|
|
|
|
|
}
|
|
|
|
|
return existingSubscription;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Unsubscribes a subscription - i.e. removes a voice connection from the play list of the audio player.
|
|
|
|
|
*
|
|
|
|
|
* @remarks
|
|
|
|
|
* This method should not be directly called. Instead, use PlayerSubscription#unsubscribe.
|
|
|
|
|
* @param subscription - The subscription to remove
|
|
|
|
|
* @returns Whether or not the subscription existed on the player and was removed
|
|
|
|
|
*/
|
|
|
|
|
// @ts-ignore
|
2022-08-01 13:02:58 +07:00
|
|
|
unsubscribe(subscription) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const index = this.subscribers.indexOf(subscription);
|
|
|
|
|
const exists = index !== -1;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (exists) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.subscribers.splice(index, 1);
|
|
|
|
|
subscription.connection.setSpeaking(false);
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('unsubscribe', subscription);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
return exists;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* The state that the player is in.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
get state() {
|
2022-05-21 21:02:00 +07:00
|
|
|
return this._state;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Sets a new state for the player, performing clean-up operations where necessary.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
set state(newState) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const oldState = this._state;
|
2022-12-20 22:20:09 +07:00
|
|
|
const newResource = Reflect.get(newState, 'resource');
|
2023-03-21 18:47:11 +07:00
|
|
|
if (oldState.status !== AudioPlayerStatus.Idle && oldState.resource !== newResource) {
|
2022-12-20 22:20:09 +07:00
|
|
|
oldState.resource.playStream.on('error', noop);
|
|
|
|
|
oldState.resource.playStream.off('error', oldState.onStreamError);
|
2022-05-21 21:02:00 +07:00
|
|
|
oldState.resource.audioPlayer = void 0;
|
|
|
|
|
oldState.resource.playStream.destroy();
|
|
|
|
|
oldState.resource.playStream.read();
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
if (
|
2023-03-21 18:47:11 +07:00
|
|
|
oldState.status === AudioPlayerStatus.Buffering &&
|
|
|
|
|
(newState.status !== AudioPlayerStatus.Buffering || newState.resource !== oldState.resource)
|
2022-12-20 22:20:09 +07:00
|
|
|
) {
|
|
|
|
|
oldState.resource.playStream.off('end', oldState.onFailureCallback);
|
|
|
|
|
oldState.resource.playStream.off('close', oldState.onFailureCallback);
|
|
|
|
|
oldState.resource.playStream.off('finish', oldState.onFailureCallback);
|
|
|
|
|
oldState.resource.playStream.off('readable', oldState.onReadableCallback);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
if (newState.status === AudioPlayerStatus.Idle) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this._signalStopSpeaking();
|
|
|
|
|
deleteAudioPlayer(this);
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
if (newResource) {
|
2022-05-21 21:02:00 +07:00
|
|
|
addAudioPlayer(this);
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
const didChangeResources =
|
2023-03-21 18:47:11 +07:00
|
|
|
oldState.status !== AudioPlayerStatus.Idle &&
|
|
|
|
|
newState.status === AudioPlayerStatus.Playing &&
|
2022-12-20 22:20:09 +07:00
|
|
|
oldState.resource !== newState.resource;
|
2022-05-21 21:02:00 +07:00
|
|
|
this._state = newState;
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('stateChange', oldState, this._state);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (oldState.status !== newState.status || didChangeResources) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.emit(newState.status, oldState, this._state);
|
|
|
|
|
}
|
|
|
|
|
this.debug?.(`state change:
|
|
|
|
|
from ${stringifyState2(oldState)}
|
|
|
|
|
to ${stringifyState2(newState)}`);
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Plays a new resource on the player. If the player is already playing a resource, the existing resource is destroyed
|
|
|
|
|
* (it cannot be reused, even in another player) and is replaced with the new resource.
|
|
|
|
|
*
|
|
|
|
|
* @remarks
|
|
|
|
|
* The player will transition to the Playing state once playback begins, and will return to the Idle state once
|
|
|
|
|
* playback is ended.
|
|
|
|
|
*
|
|
|
|
|
* If the player was previously playing a resource and this method is called, the player will not transition to the
|
|
|
|
|
* Idle state during the swap over.
|
|
|
|
|
* @param resource - The resource to play
|
|
|
|
|
* @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
play(resource) {
|
|
|
|
|
if (resource.ended) {
|
2022-12-20 22:20:09 +07:00
|
|
|
throw new Error('Cannot play a resource that has already ended.');
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
if (resource.audioPlayer) {
|
|
|
|
|
if (resource.audioPlayer === this) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
throw new Error('Resource is already being played by another audio player.');
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
resource.audioPlayer = this;
|
2022-12-20 22:20:09 +07:00
|
|
|
const onStreamError = /* @__PURE__ */ __name(error => {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status !== AudioPlayerStatus.Idle) {
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('error', new AudioPlayerError(error, this.state.resource));
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status !== AudioPlayerStatus.Idle && this.state.resource === resource) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.Idle,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
}, 'onStreamError');
|
|
|
|
|
resource.playStream.once('error', onStreamError);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (resource.started) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.Playing,
|
2022-05-21 21:02:00 +07:00
|
|
|
missedFrames: 0,
|
|
|
|
|
playbackDuration: 0,
|
|
|
|
|
resource,
|
2022-12-20 22:20:09 +07:00
|
|
|
onStreamError,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2022-08-01 13:02:58 +07:00
|
|
|
} else {
|
|
|
|
|
const onReadableCallback = /* @__PURE__ */ __name(() => {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status === AudioPlayerStatus.Buffering && this.state.resource === resource) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.Playing,
|
2022-05-21 21:02:00 +07:00
|
|
|
missedFrames: 0,
|
|
|
|
|
playbackDuration: 0,
|
|
|
|
|
resource,
|
2022-12-20 22:20:09 +07:00
|
|
|
onStreamError,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
}, 'onReadableCallback');
|
2022-08-01 13:02:58 +07:00
|
|
|
const onFailureCallback = /* @__PURE__ */ __name(() => {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status === AudioPlayerStatus.Buffering && this.state.resource === resource) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.Idle,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
}, 'onFailureCallback');
|
|
|
|
|
resource.playStream.once('readable', onReadableCallback);
|
|
|
|
|
resource.playStream.once('end', onFailureCallback);
|
|
|
|
|
resource.playStream.once('close', onFailureCallback);
|
|
|
|
|
resource.playStream.once('finish', onFailureCallback);
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.Buffering,
|
2022-05-21 21:02:00 +07:00
|
|
|
resource,
|
|
|
|
|
onReadableCallback,
|
|
|
|
|
onFailureCallback,
|
2022-12-20 22:20:09 +07:00
|
|
|
onStreamError,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Pauses playback of the current resource, if any.
|
|
|
|
|
*
|
|
|
|
|
* @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches
|
|
|
|
|
* @returns `true` if the player was successfully paused, otherwise `false`
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
pause(interpolateSilence = true) {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status !== AudioPlayerStatus.Playing) return false;
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.Paused,
|
2022-12-20 22:20:09 +07:00
|
|
|
silencePacketsRemaining: interpolateSilence ? 5 : 0,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Unpauses playback of the current resource, if any.
|
|
|
|
|
*
|
|
|
|
|
* @returns `true` if the player was successfully unpaused, otherwise `false`
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
unpause() {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status !== AudioPlayerStatus.Paused) return false;
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.Playing,
|
2022-12-20 22:20:09 +07:00
|
|
|
missedFrames: 0,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Stops playback of the current resource and destroys the resource. The player will either transition to the Idle state,
|
|
|
|
|
* or remain in its current state until the silence padding frames of the resource have been played.
|
|
|
|
|
*
|
|
|
|
|
* @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames
|
|
|
|
|
* @returns `true` if the player will come to a stop, otherwise `false`
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
stop(force = false) {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status === AudioPlayerStatus.Idle) return false;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (force || this.state.resource.silencePaddingFrames === 0) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.Idle,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2022-08-01 13:02:58 +07:00
|
|
|
} else if (this.state.resource.silenceRemaining === -1) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Checks whether the underlying resource (if any) is playable (readable)
|
|
|
|
|
*
|
|
|
|
|
* @returns `true` if the resource is playable, otherwise `false`
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
checkPlayable() {
|
2022-05-21 21:02:00 +07:00
|
|
|
const state = this._state;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return false;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (!state.resource.readable) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.Idle,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered
|
|
|
|
|
* by the active connections of this audio player.
|
|
|
|
|
*/
|
|
|
|
|
// @ts-ignore
|
2022-08-01 13:02:58 +07:00
|
|
|
_stepDispatch() {
|
2022-05-21 21:02:00 +07:00
|
|
|
const state = this._state;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return;
|
2022-09-28 19:40:46 +07:00
|
|
|
for (const connection of this.playable) {
|
|
|
|
|
connection.dispatchAudio();
|
|
|
|
|
}
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the
|
|
|
|
|
* underlying resource of the stream, and then has all the active connections of the audio player prepare it
|
|
|
|
|
* (encrypt it, append header data) so that it is ready to play at the start of the next cycle.
|
|
|
|
|
*/
|
|
|
|
|
// @ts-ignore
|
2022-08-01 13:02:58 +07:00
|
|
|
_stepPrepare() {
|
2022-05-21 21:02:00 +07:00
|
|
|
const state = this._state;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
const playable = this.playable;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.status === AudioPlayerStatus.AutoPaused && playable.length > 0) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.Playing,
|
2022-12-20 22:20:09 +07:00
|
|
|
missedFrames: 0,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.status === AudioPlayerStatus.Paused || state.status === AudioPlayerStatus.AutoPaused) {
|
2022-08-01 13:02:58 +07:00
|
|
|
if (state.silencePacketsRemaining > 0) {
|
2022-05-21 21:02:00 +07:00
|
|
|
state.silencePacketsRemaining--;
|
|
|
|
|
this._preparePacket(SILENCE_FRAME, playable, state);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (state.silencePacketsRemaining === 0) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this._signalStopSpeaking();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
if (playable.length === 0) {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.behaviors.noSubscriber === NoSubscriberBehavior.Pause) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: AudioPlayerStatus.AutoPaused,
|
2022-12-20 22:20:09 +07:00
|
|
|
silencePacketsRemaining: 5,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
return;
|
2023-03-21 18:47:11 +07:00
|
|
|
} else if (this.behaviors.noSubscriber === NoSubscriberBehavior.Stop) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.stop(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const packet = state.resource.read();
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.status === AudioPlayerStatus.Playing) {
|
2022-08-01 13:02:58 +07:00
|
|
|
if (packet) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this._preparePacket(packet, playable, state);
|
|
|
|
|
state.missedFrames = 0;
|
2022-08-01 13:02:58 +07:00
|
|
|
} else {
|
2022-05-21 21:02:00 +07:00
|
|
|
this._preparePacket(SILENCE_FRAME, playable, state);
|
|
|
|
|
state.missedFrames++;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (state.missedFrames >= this.behaviors.maxMissedFrames) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.stop();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Signals to all the subscribed connections that they should send a packet to Discord indicating
|
|
|
|
|
* they are no longer speaking. Called once playback of a resource ends.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
_signalStopSpeaking() {
|
2022-09-28 19:40:46 +07:00
|
|
|
for (const { connection } of this.subscribers) {
|
|
|
|
|
connection.setSpeaking(false);
|
|
|
|
|
}
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Instructs the given connections to each prepare this packet to be played at the start of the
|
|
|
|
|
* next cycle.
|
|
|
|
|
*
|
|
|
|
|
* @param packet - The Opus packet to be prepared by each receiver
|
|
|
|
|
* @param receivers - The connections that should play this packet
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
_preparePacket(packet, receivers, state) {
|
2022-05-21 21:02:00 +07:00
|
|
|
state.playbackDuration += 20;
|
2022-09-28 19:40:46 +07:00
|
|
|
for (const connection of receivers) {
|
|
|
|
|
connection.prepareAudioPacket(packet);
|
|
|
|
|
}
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(AudioPlayer, 'AudioPlayer');
|
2022-08-01 13:02:58 +07:00
|
|
|
function createAudioPlayer(options) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return new AudioPlayer(options);
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(createAudioPlayer, 'createAudioPlayer');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/receive/AudioReceiveStream.ts
|
2023-03-21 18:47:11 +07:00
|
|
|
var EndBehaviorType;
|
|
|
|
|
(function (EndBehaviorType2) {
|
|
|
|
|
EndBehaviorType2[
|
|
|
|
|
(EndBehaviorType2[
|
|
|
|
|
/**
|
|
|
|
|
* The stream will only end when manually destroyed.
|
|
|
|
|
*/
|
|
|
|
|
'Manual'
|
|
|
|
|
] = 0)
|
|
|
|
|
] = 'Manual';
|
|
|
|
|
EndBehaviorType2[
|
|
|
|
|
(EndBehaviorType2[
|
|
|
|
|
/**
|
|
|
|
|
* The stream will end after a given time period of silence/no audio packets.
|
|
|
|
|
*/
|
|
|
|
|
'AfterSilence'
|
|
|
|
|
] = 1)
|
|
|
|
|
] = 'AfterSilence';
|
|
|
|
|
EndBehaviorType2[
|
|
|
|
|
(EndBehaviorType2[
|
|
|
|
|
/**
|
|
|
|
|
* The stream will end after a given time period of no audio packets.
|
|
|
|
|
*/
|
|
|
|
|
'AfterInactivity'
|
|
|
|
|
] = 2)
|
|
|
|
|
] = 'AfterInactivity';
|
|
|
|
|
})(EndBehaviorType || (EndBehaviorType = {}));
|
2022-08-01 13:02:58 +07:00
|
|
|
function createDefaultAudioReceiveStreamOptions() {
|
2022-05-21 21:02:00 +07:00
|
|
|
return {
|
|
|
|
|
end: {
|
2023-03-21 18:47:11 +07:00
|
|
|
behavior: EndBehaviorType.Manual,
|
2022-12-20 22:20:09 +07:00
|
|
|
},
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(createDefaultAudioReceiveStreamOptions, 'createDefaultAudioReceiveStreamOptions');
|
2022-08-01 13:02:58 +07:00
|
|
|
var AudioReceiveStream = class extends import_node_stream.Readable {
|
|
|
|
|
constructor({ end, ...options }) {
|
2022-05-21 21:02:00 +07:00
|
|
|
super({
|
|
|
|
|
...options,
|
2022-12-20 22:20:09 +07:00
|
|
|
objectMode: true,
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
this.end = end;
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
push(buffer) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (
|
|
|
|
|
buffer &&
|
2023-03-21 18:47:11 +07:00
|
|
|
(this.end.behavior === EndBehaviorType.AfterInactivity ||
|
|
|
|
|
(this.end.behavior === EndBehaviorType.AfterSilence &&
|
2022-12-20 22:20:09 +07:00
|
|
|
(buffer.compare(SILENCE_FRAME) !== 0 || typeof this.endTimeout === 'undefined')))
|
|
|
|
|
) {
|
2022-09-28 19:40:46 +07:00
|
|
|
this.renewEndTimeout(this.end);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
return super.push(buffer);
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
renewEndTimeout(end) {
|
|
|
|
|
if (this.endTimeout) {
|
2022-05-21 21:02:00 +07:00
|
|
|
clearTimeout(this.endTimeout);
|
|
|
|
|
}
|
|
|
|
|
this.endTimeout = setTimeout(() => this.push(null), end.duration);
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
_read() {}
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(AudioReceiveStream, 'AudioReceiveStream');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/receive/SSRCMap.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_events5 = require('events');
|
2022-08-01 13:02:58 +07:00
|
|
|
var SSRCMap = class extends import_node_events5.EventEmitter {
|
|
|
|
|
constructor() {
|
2022-05-21 21:02:00 +07:00
|
|
|
super();
|
|
|
|
|
this.map = /* @__PURE__ */ new Map();
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Updates the map with new user data
|
|
|
|
|
*
|
|
|
|
|
* @param data - The data to update with
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
update(data) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const existing = this.map.get(data.audioSSRC);
|
|
|
|
|
const newValue = {
|
|
|
|
|
...this.map.get(data.audioSSRC),
|
2022-12-20 22:20:09 +07:00
|
|
|
...data,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
this.map.set(data.audioSSRC, newValue);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (!existing) this.emit('create', newValue);
|
|
|
|
|
this.emit('update', existing, newValue);
|
2022-08-01 13:02:58 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Gets the stored voice data of a user.
|
|
|
|
|
*
|
|
|
|
|
* @param target - The target, either their user id or audio SSRC
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
get(target) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (typeof target === 'number') {
|
2022-05-21 21:02:00 +07:00
|
|
|
return this.map.get(target);
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
for (const data of this.map.values()) {
|
|
|
|
|
if (data.userId === target) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return void 0;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Deletes the stored voice data about a user.
|
|
|
|
|
*
|
|
|
|
|
* @param target - The target of the delete operation, either their audio SSRC or user id
|
|
|
|
|
* @returns The data that was deleted, if any
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
delete(target) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (typeof target === 'number') {
|
2022-05-21 21:02:00 +07:00
|
|
|
const existing = this.map.get(target);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (existing) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.map.delete(target);
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('delete', existing);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
return existing;
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
for (const [audioSSRC, data] of this.map.entries()) {
|
|
|
|
|
if (data.userId === target) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.map.delete(audioSSRC);
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('delete', data);
|
2022-05-21 21:02:00 +07:00
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return void 0;
|
|
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(SSRCMap, 'SSRCMap');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/receive/SpeakingMap.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_events6 = require('events');
|
2022-08-01 13:02:58 +07:00
|
|
|
var _SpeakingMap = class extends import_node_events6.EventEmitter {
|
|
|
|
|
constructor() {
|
2022-05-21 21:02:00 +07:00
|
|
|
super();
|
|
|
|
|
this.users = /* @__PURE__ */ new Map();
|
|
|
|
|
this.speakingTimeouts = /* @__PURE__ */ new Map();
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
onPacket(userId) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const timeout = this.speakingTimeouts.get(userId);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (timeout) {
|
2022-05-21 21:02:00 +07:00
|
|
|
clearTimeout(timeout);
|
2022-08-01 13:02:58 +07:00
|
|
|
} else {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.users.set(userId, Date.now());
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('start', userId);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
this.startTimeout(userId);
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
startTimeout(userId) {
|
|
|
|
|
this.speakingTimeouts.set(
|
|
|
|
|
userId,
|
|
|
|
|
setTimeout(() => {
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('end', userId);
|
2022-08-01 13:02:58 +07:00
|
|
|
this.speakingTimeouts.delete(userId);
|
|
|
|
|
this.users.delete(userId);
|
2022-12-20 22:20:09 +07:00
|
|
|
}, _SpeakingMap.DELAY),
|
2022-08-01 13:02:58 +07:00
|
|
|
);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
var SpeakingMap = _SpeakingMap;
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(SpeakingMap, 'SpeakingMap');
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* The delay after a packet is received from a user until they're marked as not speaking anymore.
|
|
|
|
|
*/
|
2022-12-20 22:20:09 +07:00
|
|
|
__publicField(SpeakingMap, 'DELAY', 100);
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/receive/VoiceReceiver.ts
|
2022-08-01 13:02:58 +07:00
|
|
|
var VoiceReceiver = class {
|
|
|
|
|
constructor(voiceConnection) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.voiceConnection = voiceConnection;
|
|
|
|
|
this.ssrcMap = new SSRCMap();
|
|
|
|
|
this.speaking = new SpeakingMap();
|
|
|
|
|
this.subscriptions = /* @__PURE__ */ new Map();
|
|
|
|
|
this.connectionData = {};
|
|
|
|
|
this.onWsPacket = this.onWsPacket.bind(this);
|
|
|
|
|
this.onUdpMessage = this.onUdpMessage.bind(this);
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when a packet is received on the attached connection's WebSocket.
|
|
|
|
|
*
|
|
|
|
|
* @param packet - The received packet
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onWsPacket(packet) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (packet.op === import_v43.VoiceOpcodes.ClientDisconnect && typeof packet.d?.user_id === 'string') {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.ssrcMap.delete(packet.d.user_id);
|
2022-12-20 22:20:09 +07:00
|
|
|
} else if (
|
|
|
|
|
packet.op === import_v43.VoiceOpcodes.Speaking &&
|
|
|
|
|
typeof packet.d?.user_id === 'string' &&
|
|
|
|
|
typeof packet.d?.ssrc === 'number'
|
|
|
|
|
) {
|
2023-03-21 18:47:11 +07:00
|
|
|
this.ssrcMap.update({
|
|
|
|
|
userId: packet.d.user_id,
|
|
|
|
|
audioSSRC: packet.d.ssrc,
|
|
|
|
|
});
|
2022-12-20 22:20:09 +07:00
|
|
|
} else if (
|
|
|
|
|
packet.op === import_v43.VoiceOpcodes.ClientConnect &&
|
|
|
|
|
typeof packet.d?.user_id === 'string' &&
|
|
|
|
|
typeof packet.d?.audio_ssrc === 'number'
|
|
|
|
|
) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.ssrcMap.update({
|
|
|
|
|
userId: packet.d.user_id,
|
|
|
|
|
audioSSRC: packet.d.audio_ssrc,
|
2022-12-20 22:20:09 +07:00
|
|
|
videoSSRC: packet.d.video_ssrc === 0 ? void 0 : packet.d.video_ssrc,
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
decrypt(buffer, mode, nonce2, secretKey) {
|
2022-05-21 21:02:00 +07:00
|
|
|
let end;
|
2022-12-20 22:20:09 +07:00
|
|
|
if (mode === 'xsalsa20_poly1305_lite') {
|
2022-05-21 21:02:00 +07:00
|
|
|
buffer.copy(nonce2, 0, buffer.length - 4);
|
|
|
|
|
end = buffer.length - 4;
|
2022-12-20 22:20:09 +07:00
|
|
|
} else if (mode === 'xsalsa20_poly1305_suffix') {
|
2022-05-21 21:02:00 +07:00
|
|
|
buffer.copy(nonce2, 0, buffer.length - 24);
|
|
|
|
|
end = buffer.length - 24;
|
2022-08-01 13:02:58 +07:00
|
|
|
} else {
|
2022-05-21 21:02:00 +07:00
|
|
|
buffer.copy(nonce2, 0, 0, 12);
|
|
|
|
|
}
|
|
|
|
|
const decrypted = methods.open(buffer.slice(12, end), nonce2, secretKey);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (!decrypted) return;
|
2022-09-28 19:40:46 +07:00
|
|
|
return import_node_buffer5.Buffer.from(decrypted);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Parses an audio packet, decrypting it to yield an Opus packet.
|
|
|
|
|
*
|
|
|
|
|
* @param buffer - The buffer to parse
|
|
|
|
|
* @param mode - The encryption mode
|
|
|
|
|
* @param nonce - The nonce buffer used by the connection for encryption
|
|
|
|
|
* @param secretKey - The secret key used by the connection for encryption
|
|
|
|
|
* @returns The parsed Opus packet
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
parsePacket(buffer, mode, nonce2, secretKey) {
|
2022-05-21 21:02:00 +07:00
|
|
|
let packet = this.decrypt(buffer, mode, nonce2, secretKey);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (!packet) return;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (packet[0] === 190 && packet[1] === 222) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const headerExtensionLength = packet.readUInt16BE(2);
|
2022-07-19 20:31:14 +07:00
|
|
|
packet = packet.subarray(4 + 4 * headerExtensionLength);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
return packet;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when the UDP socket of the attached connection receives a message.
|
|
|
|
|
*
|
|
|
|
|
* @param msg - The received message
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onUdpMessage(msg) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (msg.length <= 8) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
const ssrc = msg.readUInt32BE(8);
|
|
|
|
|
const userData = this.ssrcMap.get(ssrc);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (!userData) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
this.speaking.onPacket(userData.userId);
|
|
|
|
|
const stream = this.subscriptions.get(userData.userId);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (!stream) return;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (this.connectionData.encryptionMode && this.connectionData.nonceBuffer && this.connectionData.secretKey) {
|
|
|
|
|
const packet = this.parsePacket(
|
|
|
|
|
msg,
|
|
|
|
|
this.connectionData.encryptionMode,
|
|
|
|
|
this.connectionData.nonceBuffer,
|
2022-12-20 22:20:09 +07:00
|
|
|
this.connectionData.secretKey,
|
2022-08-01 13:02:58 +07:00
|
|
|
);
|
|
|
|
|
if (packet) {
|
2022-05-21 21:02:00 +07:00
|
|
|
stream.push(packet);
|
2022-08-01 13:02:58 +07:00
|
|
|
} else {
|
2022-12-20 22:20:09 +07:00
|
|
|
stream.destroy(new Error('Failed to parse packet'));
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Creates a subscription for the given user id.
|
|
|
|
|
*
|
|
|
|
|
* @param target - The id of the user to subscribe to
|
|
|
|
|
* @returns A readable stream of Opus packets received from the target
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
subscribe(userId, options) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const existing = this.subscriptions.get(userId);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (existing) return existing;
|
2022-05-21 21:02:00 +07:00
|
|
|
const stream = new AudioReceiveStream({
|
|
|
|
|
...createDefaultAudioReceiveStreamOptions(),
|
2022-12-20 22:20:09 +07:00
|
|
|
...options,
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
2022-12-20 22:20:09 +07:00
|
|
|
stream.once('close', () => this.subscriptions.delete(userId));
|
2022-05-21 21:02:00 +07:00
|
|
|
this.subscriptions.set(userId, stream);
|
|
|
|
|
return stream;
|
|
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(VoiceReceiver, 'VoiceReceiver');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/VoiceConnection.ts
|
2023-03-21 18:47:11 +07:00
|
|
|
var VoiceConnectionStatus;
|
|
|
|
|
(function (VoiceConnectionStatus2) {
|
|
|
|
|
VoiceConnectionStatus2[
|
|
|
|
|
/**
|
|
|
|
|
* The `VOICE_SERVER_UPDATE` and `VOICE_STATE_UPDATE` packets have been received, now attempting to establish a voice connection.
|
|
|
|
|
*/
|
|
|
|
|
'Connecting'
|
|
|
|
|
] = 'connecting';
|
|
|
|
|
VoiceConnectionStatus2[
|
|
|
|
|
/**
|
|
|
|
|
* The voice connection has been destroyed and untracked, it cannot be reused.
|
|
|
|
|
*/
|
|
|
|
|
'Destroyed'
|
|
|
|
|
] = 'destroyed';
|
|
|
|
|
VoiceConnectionStatus2[
|
|
|
|
|
/**
|
|
|
|
|
* The voice connection has either been severed or not established.
|
|
|
|
|
*/
|
|
|
|
|
'Disconnected'
|
|
|
|
|
] = 'disconnected';
|
|
|
|
|
VoiceConnectionStatus2[
|
|
|
|
|
/**
|
|
|
|
|
* A voice connection has been established, and is ready to be used.
|
|
|
|
|
*/
|
|
|
|
|
'Ready'
|
|
|
|
|
] = 'ready';
|
|
|
|
|
VoiceConnectionStatus2[
|
|
|
|
|
/**
|
|
|
|
|
* Sending a packet to the main Discord gateway to indicate we want to change our voice state.
|
|
|
|
|
*/
|
|
|
|
|
'Signalling'
|
|
|
|
|
] = 'signalling';
|
|
|
|
|
})(VoiceConnectionStatus || (VoiceConnectionStatus = {}));
|
|
|
|
|
var VoiceConnectionDisconnectReason;
|
|
|
|
|
(function (VoiceConnectionDisconnectReason2) {
|
|
|
|
|
VoiceConnectionDisconnectReason2[
|
|
|
|
|
(VoiceConnectionDisconnectReason2[
|
|
|
|
|
/**
|
|
|
|
|
* When the WebSocket connection has been closed.
|
|
|
|
|
*/
|
|
|
|
|
'WebSocketClose'
|
|
|
|
|
] = 0)
|
|
|
|
|
] = 'WebSocketClose';
|
|
|
|
|
VoiceConnectionDisconnectReason2[
|
|
|
|
|
(VoiceConnectionDisconnectReason2[
|
|
|
|
|
/**
|
|
|
|
|
* When the adapter was unable to send a message requested by the VoiceConnection.
|
|
|
|
|
*/
|
|
|
|
|
'AdapterUnavailable'
|
|
|
|
|
] = 1)
|
|
|
|
|
] = 'AdapterUnavailable';
|
|
|
|
|
VoiceConnectionDisconnectReason2[
|
|
|
|
|
(VoiceConnectionDisconnectReason2[
|
|
|
|
|
/**
|
|
|
|
|
* When a VOICE_SERVER_UPDATE packet is received with a null endpoint, causing the connection to be severed.
|
|
|
|
|
*/
|
|
|
|
|
'EndpointRemoved'
|
|
|
|
|
] = 2)
|
|
|
|
|
] = 'EndpointRemoved';
|
|
|
|
|
VoiceConnectionDisconnectReason2[
|
|
|
|
|
(VoiceConnectionDisconnectReason2[
|
|
|
|
|
/**
|
|
|
|
|
* When a manual disconnect was requested.
|
|
|
|
|
*/
|
|
|
|
|
'Manual'
|
|
|
|
|
] = 3)
|
|
|
|
|
] = 'Manual';
|
|
|
|
|
})(VoiceConnectionDisconnectReason || (VoiceConnectionDisconnectReason = {}));
|
2022-09-28 19:40:46 +07:00
|
|
|
var VoiceConnection = class extends import_node_events7.EventEmitter {
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Creates a new voice connection.
|
|
|
|
|
*
|
|
|
|
|
* @param joinConfig - The data required to establish the voice connection
|
|
|
|
|
* @param options - The options used to create this voice connection
|
|
|
|
|
*/
|
2022-09-28 19:40:46 +07:00
|
|
|
constructor(joinConfig, options) {
|
2022-05-21 21:02:00 +07:00
|
|
|
super();
|
2022-12-20 22:20:09 +07:00
|
|
|
this.debug = options.debug ? message => this.emit('debug', message) : null;
|
2022-05-21 21:02:00 +07:00
|
|
|
this.rejoinAttempts = 0;
|
|
|
|
|
this.receiver = new VoiceReceiver(this);
|
|
|
|
|
this.onNetworkingClose = this.onNetworkingClose.bind(this);
|
|
|
|
|
this.onNetworkingStateChange = this.onNetworkingStateChange.bind(this);
|
|
|
|
|
this.onNetworkingError = this.onNetworkingError.bind(this);
|
|
|
|
|
this.onNetworkingDebug = this.onNetworkingDebug.bind(this);
|
2022-09-28 19:40:46 +07:00
|
|
|
const adapter = options.adapterCreator({
|
2022-12-20 22:20:09 +07:00
|
|
|
onVoiceServerUpdate: data => this.addServerPacket(data),
|
|
|
|
|
onVoiceStateUpdate: data => this.addStatePacket(data),
|
|
|
|
|
destroy: () => this.destroy(false),
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
2023-03-21 18:47:11 +07:00
|
|
|
this._state = {
|
|
|
|
|
status: VoiceConnectionStatus.Signalling,
|
|
|
|
|
adapter,
|
|
|
|
|
};
|
2022-05-21 21:02:00 +07:00
|
|
|
this.packets = {
|
|
|
|
|
server: void 0,
|
2022-12-20 22:20:09 +07:00
|
|
|
state: void 0,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
this.joinConfig = joinConfig;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* The current state of the voice connection.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
get state() {
|
2022-05-21 21:02:00 +07:00
|
|
|
return this._state;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Updates the state of the voice connection, performing clean-up operations where necessary.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
set state(newState) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const oldState = this._state;
|
2022-12-20 22:20:09 +07:00
|
|
|
const oldNetworking = Reflect.get(oldState, 'networking');
|
|
|
|
|
const newNetworking = Reflect.get(newState, 'networking');
|
|
|
|
|
const oldSubscription = Reflect.get(oldState, 'subscription');
|
|
|
|
|
const newSubscription = Reflect.get(newState, 'subscription');
|
2022-08-01 13:02:58 +07:00
|
|
|
if (oldNetworking !== newNetworking) {
|
|
|
|
|
if (oldNetworking) {
|
2022-12-20 22:20:09 +07:00
|
|
|
oldNetworking.on('error', noop);
|
|
|
|
|
oldNetworking.off('debug', this.onNetworkingDebug);
|
|
|
|
|
oldNetworking.off('error', this.onNetworkingError);
|
|
|
|
|
oldNetworking.off('close', this.onNetworkingClose);
|
|
|
|
|
oldNetworking.off('stateChange', this.onNetworkingStateChange);
|
2022-05-21 21:02:00 +07:00
|
|
|
oldNetworking.destroy();
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
if (newNetworking) this.updateReceiveBindings(newNetworking.state, oldNetworking?.state);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
if (newState.status === VoiceConnectionStatus.Ready) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.rejoinAttempts = 0;
|
2023-03-21 18:47:11 +07:00
|
|
|
} else if (newState.status === VoiceConnectionStatus.Destroyed) {
|
2022-08-01 13:02:58 +07:00
|
|
|
for (const stream of this.receiver.subscriptions.values()) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (!stream.destroyed) stream.destroy();
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
if (oldState.status !== VoiceConnectionStatus.Destroyed && newState.status === VoiceConnectionStatus.Destroyed) {
|
2022-05-21 21:02:00 +07:00
|
|
|
oldState.adapter.destroy();
|
|
|
|
|
}
|
|
|
|
|
this._state = newState;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (oldSubscription && oldSubscription !== newSubscription) {
|
2022-05-21 21:02:00 +07:00
|
|
|
oldSubscription.unsubscribe();
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('stateChange', oldState, newState);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (oldState.status !== newState.status) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.emit(newState.status, oldState, newState);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Registers a `VOICE_SERVER_UPDATE` packet to the voice connection. This will cause it to reconnect using the
|
|
|
|
|
* new data provided in the packet.
|
|
|
|
|
*
|
|
|
|
|
* @param packet - The received `VOICE_SERVER_UPDATE` packet
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
addServerPacket(packet) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.packets.server = packet;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (packet.endpoint) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.configureNetworking();
|
2023-03-21 18:47:11 +07:00
|
|
|
} else if (this.state.status !== VoiceConnectionStatus.Destroyed) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Disconnected,
|
|
|
|
|
reason: VoiceConnectionDisconnectReason.EndpointRemoved,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Registers a `VOICE_STATE_UPDATE` packet to the voice connection. Most importantly, it stores the id of the
|
|
|
|
|
* channel that the client is connected to.
|
|
|
|
|
*
|
|
|
|
|
* @param packet - The received `VOICE_STATE_UPDATE` packet
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
addStatePacket(packet) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.packets.state = packet;
|
2022-12-20 22:20:09 +07:00
|
|
|
if (typeof packet.self_deaf !== 'undefined') this.joinConfig.selfDeaf = packet.self_deaf;
|
|
|
|
|
if (typeof packet.self_mute !== 'undefined') this.joinConfig.selfMute = packet.self_mute;
|
|
|
|
|
if (packet.channel_id) this.joinConfig.channelId = packet.channel_id;
|
2022-08-01 13:02:58 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when the networking state changes, and the new ws/udp packet/message handlers need to be rebound
|
|
|
|
|
* to the new instances.
|
|
|
|
|
*
|
|
|
|
|
* @param newState - The new networking state
|
|
|
|
|
* @param oldState - The old networking state, if there is one
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
updateReceiveBindings(newState, oldState) {
|
2022-12-20 22:20:09 +07:00
|
|
|
const oldWs = Reflect.get(oldState ?? {}, 'ws');
|
|
|
|
|
const newWs = Reflect.get(newState, 'ws');
|
|
|
|
|
const oldUdp = Reflect.get(oldState ?? {}, 'udp');
|
|
|
|
|
const newUdp = Reflect.get(newState, 'udp');
|
2022-08-01 13:02:58 +07:00
|
|
|
if (oldWs !== newWs) {
|
2022-12-20 22:20:09 +07:00
|
|
|
oldWs?.off('packet', this.receiver.onWsPacket);
|
|
|
|
|
newWs?.on('packet', this.receiver.onWsPacket);
|
2022-08-01 13:02:58 +07:00
|
|
|
}
|
|
|
|
|
if (oldUdp !== newUdp) {
|
2022-12-20 22:20:09 +07:00
|
|
|
oldUdp?.off('message', this.receiver.onUdpMessage);
|
|
|
|
|
newUdp?.on('message', this.receiver.onUdpMessage);
|
2022-08-01 13:02:58 +07:00
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
this.receiver.connectionData = Reflect.get(newState, 'connectionData') ?? {};
|
2022-08-01 13:02:58 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Attempts to configure a networking instance for this voice connection using the received packets.
|
|
|
|
|
* Both packets are required, and any existing networking instance will be destroyed.
|
|
|
|
|
*
|
|
|
|
|
* @remarks
|
|
|
|
|
* This is called when the voice server of the connection changes, e.g. if the bot is moved into a
|
|
|
|
|
* different channel in the same guild but has a different voice server. In this instance, the connection
|
|
|
|
|
* needs to be re-established to the new voice server.
|
|
|
|
|
*
|
|
|
|
|
* The connection will transition to the Connecting state when this is called.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
configureNetworking() {
|
2022-05-21 21:02:00 +07:00
|
|
|
const { server, state } = this.packets;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (!server || !state || this.state.status === VoiceConnectionStatus.Destroyed || !server.endpoint) return;
|
2022-08-01 13:02:58 +07:00
|
|
|
const networking = new Networking(
|
|
|
|
|
{
|
|
|
|
|
endpoint: server.endpoint,
|
2022-09-28 19:40:46 +07:00
|
|
|
serverId: server.guild_id ?? server.channel_id,
|
2022-08-01 13:02:58 +07:00
|
|
|
token: server.token,
|
|
|
|
|
sessionId: state.session_id,
|
2022-12-20 22:20:09 +07:00
|
|
|
userId: state.user_id,
|
2022-08-01 13:02:58 +07:00
|
|
|
},
|
2022-12-20 22:20:09 +07:00
|
|
|
Boolean(this.debug),
|
2022-08-01 13:02:58 +07:00
|
|
|
);
|
2022-12-20 22:20:09 +07:00
|
|
|
networking.once('close', this.onNetworkingClose);
|
|
|
|
|
networking.on('stateChange', this.onNetworkingStateChange);
|
|
|
|
|
networking.on('error', this.onNetworkingError);
|
|
|
|
|
networking.on('debug', this.onNetworkingDebug);
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Connecting,
|
2022-12-20 22:20:09 +07:00
|
|
|
networking,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when the networking instance for this connection closes. If the close code is 4014 (do not reconnect),
|
|
|
|
|
* the voice connection will transition to the Disconnected state which will store the close code. You can
|
|
|
|
|
* decide whether or not to reconnect when this occurs by listening for the state change and calling reconnect().
|
|
|
|
|
*
|
|
|
|
|
* @remarks
|
|
|
|
|
* If the close code was anything other than 4014, it is likely that the closing was not intended, and so the
|
|
|
|
|
* VoiceConnection will signal to Discord that it would like to rejoin the channel. This automatically attempts
|
|
|
|
|
* to re-establish the connection. This would be seen as a transition from the Ready state to the Signalling state.
|
|
|
|
|
* @param code - The close code
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onNetworkingClose(code) {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status === VoiceConnectionStatus.Destroyed) return;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (code === 4014) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Disconnected,
|
|
|
|
|
reason: VoiceConnectionDisconnectReason.WebSocketClose,
|
2022-12-20 22:20:09 +07:00
|
|
|
closeCode: code,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2022-08-01 13:02:58 +07:00
|
|
|
} else {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Signalling,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
this.rejoinAttempts++;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Disconnected,
|
|
|
|
|
reason: VoiceConnectionDisconnectReason.AdapterUnavailable,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when the state of the networking instance changes. This is used to derive the state of the voice connection.
|
|
|
|
|
*
|
|
|
|
|
* @param oldState - The previous state
|
|
|
|
|
* @param newState - The new state
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onNetworkingStateChange(oldState, newState) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.updateReceiveBindings(newState, oldState);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (oldState.code === newState.code) return;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status !== VoiceConnectionStatus.Connecting && this.state.status !== VoiceConnectionStatus.Ready)
|
|
|
|
|
return;
|
|
|
|
|
if (newState.code === NetworkingStatusCode.Ready) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Ready,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2023-03-21 18:47:11 +07:00
|
|
|
} else if (newState.code !== NetworkingStatusCode.Closed) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Connecting,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Propagates errors from the underlying network instance.
|
|
|
|
|
*
|
|
|
|
|
* @param error - The error to propagate
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onNetworkingError(error) {
|
2022-12-20 22:20:09 +07:00
|
|
|
this.emit('error', error);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Propagates debug messages from the underlying network instance.
|
|
|
|
|
*
|
|
|
|
|
* @param message - The debug message to propagate
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onNetworkingDebug(message) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.debug?.(`[NW] ${message}`);
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Prepares an audio packet for dispatch.
|
|
|
|
|
*
|
|
|
|
|
* @param buffer - The Opus packet to prepare
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
prepareAudioPacket(buffer) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const state = this.state;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.status !== VoiceConnectionStatus.Ready) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
return state.networking.prepareAudioPacket(buffer);
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Dispatches the previously prepared audio packet (if any)
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
dispatchAudio() {
|
2022-05-21 21:02:00 +07:00
|
|
|
const state = this.state;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.status !== VoiceConnectionStatus.Ready) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
return state.networking.dispatchAudio();
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Prepares an audio packet and dispatches it immediately.
|
|
|
|
|
*
|
|
|
|
|
* @param buffer - The Opus packet to play
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
playOpusPacket(buffer) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const state = this.state;
|
2023-03-21 18:47:11 +07:00
|
|
|
if (state.status !== VoiceConnectionStatus.Ready) return;
|
2022-05-21 21:02:00 +07:00
|
|
|
state.networking.prepareAudioPacket(buffer);
|
|
|
|
|
return state.networking.dispatchAudio();
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Destroys the VoiceConnection, preventing it from connecting to voice again.
|
|
|
|
|
* This method should be called when you no longer require the VoiceConnection to
|
|
|
|
|
* prevent memory leaks.
|
|
|
|
|
*
|
|
|
|
|
* @param adapterAvailable - Whether the adapter can be used
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
destroy(adapterAvailable = true) {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status === VoiceConnectionStatus.Destroyed) {
|
2022-12-20 22:20:09 +07:00
|
|
|
throw new Error('Cannot destroy VoiceConnection - it has already been destroyed');
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
if (getVoiceConnection(this.joinConfig.guildId, this.joinConfig.group) === this) {
|
2022-05-21 21:02:00 +07:00
|
|
|
untrackVoiceConnection(this);
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
if (adapterAvailable) {
|
2023-03-21 18:47:11 +07:00
|
|
|
this.state.adapter.sendPayload(
|
|
|
|
|
createJoinVoiceChannelPayload({
|
|
|
|
|
...this.joinConfig,
|
|
|
|
|
channelId: null,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
this.state = {
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Destroyed,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Disconnects the VoiceConnection, allowing the possibility of rejoining later on.
|
|
|
|
|
*
|
|
|
|
|
* @returns `true` if the connection was successfully disconnected
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
disconnect() {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (
|
|
|
|
|
this.state.status === VoiceConnectionStatus.Destroyed ||
|
|
|
|
|
this.state.status === VoiceConnectionStatus.Signalling
|
|
|
|
|
) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
this.joinConfig.channelId = null;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
adapter: this.state.adapter,
|
|
|
|
|
subscription: this.state.subscription,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Disconnected,
|
|
|
|
|
reason: VoiceConnectionDisconnectReason.AdapterUnavailable,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
this.state = {
|
|
|
|
|
adapter: this.state.adapter,
|
2023-03-21 18:47:11 +07:00
|
|
|
reason: VoiceConnectionDisconnectReason.Manual,
|
|
|
|
|
status: VoiceConnectionStatus.Disconnected,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Attempts to rejoin (better explanation soon:tm:)
|
|
|
|
|
*
|
|
|
|
|
* @remarks
|
|
|
|
|
* Calling this method successfully will automatically increment the `rejoinAttempts` counter,
|
|
|
|
|
* which you can use to inform whether or not you'd like to keep attempting to reconnect your
|
|
|
|
|
* voice connection.
|
|
|
|
|
*
|
|
|
|
|
* A state transition from Disconnected to Signalling will be observed when this is called.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
rejoin(joinConfig) {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status === VoiceConnectionStatus.Destroyed) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return false;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
const notReady = this.state.status !== VoiceConnectionStatus.Ready;
|
2022-12-20 22:20:09 +07:00
|
|
|
if (notReady) this.rejoinAttempts++;
|
2022-05-21 21:02:00 +07:00
|
|
|
Object.assign(this.joinConfig, joinConfig);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) {
|
|
|
|
|
if (notReady) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Signalling,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
this.state = {
|
|
|
|
|
adapter: this.state.adapter,
|
|
|
|
|
subscription: this.state.subscription,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Disconnected,
|
|
|
|
|
reason: VoiceConnectionDisconnectReason.AdapterUnavailable,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Updates the speaking status of the voice connection. This is used when audio players are done playing audio,
|
|
|
|
|
* and need to signal that the connection is no longer playing audio.
|
|
|
|
|
*
|
|
|
|
|
* @param enabled - Whether or not to show as speaking
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
setSpeaking(enabled) {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status !== VoiceConnectionStatus.Ready) return false;
|
2022-05-21 21:02:00 +07:00
|
|
|
return this.state.networking.setSpeaking(enabled);
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Subscribes to an audio player, allowing the player to play audio on this voice connection.
|
|
|
|
|
*
|
|
|
|
|
* @param player - The audio player to subscribe to
|
|
|
|
|
* @returns The created subscription
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
subscribe(player) {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status === VoiceConnectionStatus.Destroyed) return;
|
2022-12-20 22:20:09 +07:00
|
|
|
const subscription = player['subscribe'](this);
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2022-12-20 22:20:09 +07:00
|
|
|
subscription,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
return subscription;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* The latest ping (in milliseconds) for the WebSocket connection and audio playback for this voice
|
|
|
|
|
* connection, if this data is available.
|
|
|
|
|
*
|
|
|
|
|
* @remarks
|
|
|
|
|
* For this data to be available, the VoiceConnection must be in the Ready state, and its underlying
|
|
|
|
|
* WebSocket connection and UDP socket must have had at least one ping-pong exchange.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
get ping() {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (
|
|
|
|
|
this.state.status === VoiceConnectionStatus.Ready &&
|
|
|
|
|
this.state.networking.state.code === NetworkingStatusCode.Ready
|
|
|
|
|
) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return {
|
|
|
|
|
ws: this.state.networking.state.ws.ping,
|
2022-12-20 22:20:09 +07:00
|
|
|
udp: this.state.networking.state.udp.ping,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
ws: void 0,
|
2022-12-20 22:20:09 +07:00
|
|
|
udp: void 0,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Called when a subscription of this voice connection to an audio player is removed.
|
|
|
|
|
*
|
|
|
|
|
* @param subscription - The removed subscription
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
onSubscriptionRemoved(subscription) {
|
2023-03-21 18:47:11 +07:00
|
|
|
if (this.state.status !== VoiceConnectionStatus.Destroyed && this.state.subscription === subscription) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.state = {
|
|
|
|
|
...this.state,
|
2022-12-20 22:20:09 +07:00
|
|
|
subscription: void 0,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(VoiceConnection, 'VoiceConnection');
|
2022-08-01 13:02:58 +07:00
|
|
|
function createVoiceConnection(joinConfig, options) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const payload = createJoinVoiceChannelPayload(joinConfig);
|
|
|
|
|
const existing = getVoiceConnection(joinConfig.guildId, joinConfig.group);
|
2023-03-21 18:47:11 +07:00
|
|
|
if (existing && existing.state.status !== VoiceConnectionStatus.Destroyed) {
|
|
|
|
|
if (existing.state.status === VoiceConnectionStatus.Disconnected) {
|
2022-05-21 21:02:00 +07:00
|
|
|
existing.rejoin({
|
|
|
|
|
channelId: joinConfig.channelId,
|
|
|
|
|
selfDeaf: joinConfig.selfDeaf,
|
2022-12-20 22:20:09 +07:00
|
|
|
selfMute: joinConfig.selfMute,
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
2022-08-01 13:02:58 +07:00
|
|
|
} else if (!existing.state.adapter.sendPayload(payload)) {
|
2022-05-21 21:02:00 +07:00
|
|
|
existing.state = {
|
|
|
|
|
...existing.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Disconnected,
|
|
|
|
|
reason: VoiceConnectionDisconnectReason.AdapterUnavailable,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return existing;
|
|
|
|
|
}
|
2022-09-28 19:40:46 +07:00
|
|
|
const voiceConnection = new VoiceConnection(joinConfig, options);
|
2022-05-21 21:02:00 +07:00
|
|
|
trackVoiceConnection(voiceConnection);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (
|
2023-03-21 18:47:11 +07:00
|
|
|
voiceConnection.state.status !== VoiceConnectionStatus.Destroyed &&
|
2022-12-20 22:20:09 +07:00
|
|
|
!voiceConnection.state.adapter.sendPayload(payload)
|
|
|
|
|
) {
|
2022-09-28 19:40:46 +07:00
|
|
|
voiceConnection.state = {
|
|
|
|
|
...voiceConnection.state,
|
2023-03-21 18:47:11 +07:00
|
|
|
status: VoiceConnectionStatus.Disconnected,
|
|
|
|
|
reason: VoiceConnectionDisconnectReason.AdapterUnavailable,
|
2022-09-28 19:40:46 +07:00
|
|
|
};
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
return voiceConnection;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(createVoiceConnection, 'createVoiceConnection');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/joinVoiceChannel.ts
|
2022-08-01 13:02:58 +07:00
|
|
|
function joinVoiceChannel(options) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const joinConfig = {
|
|
|
|
|
selfDeaf: true,
|
|
|
|
|
selfMute: false,
|
2022-12-20 22:20:09 +07:00
|
|
|
group: 'default',
|
|
|
|
|
...options,
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
|
|
|
|
return createVoiceConnection(joinConfig, {
|
|
|
|
|
adapterCreator: options.adapterCreator,
|
2022-12-20 22:20:09 +07:00
|
|
|
debug: options.debug,
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(joinVoiceChannel, 'joinVoiceChannel');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/audio/AudioResource.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_stream2 = require('stream');
|
|
|
|
|
var import_prism_media2 = __toESM(require('prism-media'));
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/audio/TransformerGraph.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_prism_media = __toESM(require('prism-media'));
|
|
|
|
|
var FFMPEG_PCM_ARGUMENTS = ['-analyzeduration', '0', '-loglevel', '0', '-f', 's16le', '-ar', '48000', '-ac', '2'];
|
2022-05-21 21:02:00 +07:00
|
|
|
var FFMPEG_OPUS_ARGUMENTS = [
|
2022-12-20 22:20:09 +07:00
|
|
|
'-analyzeduration',
|
|
|
|
|
'0',
|
|
|
|
|
'-loglevel',
|
|
|
|
|
'0',
|
|
|
|
|
'-acodec',
|
|
|
|
|
'libopus',
|
|
|
|
|
'-f',
|
|
|
|
|
'opus',
|
|
|
|
|
'-ar',
|
|
|
|
|
'48000',
|
|
|
|
|
'-ac',
|
|
|
|
|
'2',
|
2022-05-21 21:02:00 +07:00
|
|
|
];
|
2023-03-21 18:47:11 +07:00
|
|
|
var StreamType;
|
|
|
|
|
(function (StreamType2) {
|
2022-12-20 22:20:09 +07:00
|
|
|
StreamType2['Arbitrary'] = 'arbitrary';
|
|
|
|
|
StreamType2['OggOpus'] = 'ogg/opus';
|
|
|
|
|
StreamType2['Opus'] = 'opus';
|
|
|
|
|
StreamType2['Raw'] = 'raw';
|
|
|
|
|
StreamType2['WebmOpus'] = 'webm/opus';
|
2023-03-21 18:47:11 +07:00
|
|
|
})(StreamType || (StreamType = {}));
|
|
|
|
|
var TransformerType;
|
|
|
|
|
(function (TransformerType2) {
|
|
|
|
|
TransformerType2['FFmpegOgg'] = 'ffmpeg ogg';
|
|
|
|
|
TransformerType2['FFmpegPCM'] = 'ffmpeg pcm';
|
|
|
|
|
TransformerType2['InlineVolume'] = 'volume transformer';
|
|
|
|
|
TransformerType2['OggOpusDemuxer'] = 'ogg/opus demuxer';
|
|
|
|
|
TransformerType2['OpusDecoder'] = 'opus decoder';
|
|
|
|
|
TransformerType2['OpusEncoder'] = 'opus encoder';
|
|
|
|
|
TransformerType2['WebmOpusDemuxer'] = 'webm/opus demuxer';
|
|
|
|
|
})(TransformerType || (TransformerType = {}));
|
2022-08-01 13:02:58 +07:00
|
|
|
var Node = class {
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* The outbound edges from this node.
|
|
|
|
|
*/
|
2022-09-28 19:40:46 +07:00
|
|
|
edges = [];
|
2022-08-01 13:02:58 +07:00
|
|
|
constructor(type) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.type = type;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Creates an outbound edge from this node.
|
|
|
|
|
*
|
|
|
|
|
* @param edge - The edge to create
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
addEdge(edge) {
|
2023-03-21 18:47:11 +07:00
|
|
|
this.edges.push({
|
|
|
|
|
...edge,
|
|
|
|
|
from: this,
|
|
|
|
|
});
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(Node, 'Node');
|
2022-05-21 21:02:00 +07:00
|
|
|
var NODES = /* @__PURE__ */ new Map();
|
2022-08-01 13:02:58 +07:00
|
|
|
for (const streamType of Object.values(StreamType)) {
|
2022-05-21 21:02:00 +07:00
|
|
|
NODES.set(streamType, new Node(streamType));
|
|
|
|
|
}
|
2022-08-01 13:02:58 +07:00
|
|
|
function getNode(type) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const node = NODES.get(type);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (!node) throw new Error(`Node type '${type}' does not exist!`);
|
2022-05-21 21:02:00 +07:00
|
|
|
return node;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(getNode, 'getNode');
|
2023-03-21 18:47:11 +07:00
|
|
|
getNode(StreamType.Raw).addEdge({
|
|
|
|
|
type: TransformerType.OpusEncoder,
|
|
|
|
|
to: getNode(StreamType.Opus),
|
2022-05-21 21:02:00 +07:00
|
|
|
cost: 1.5,
|
2023-03-21 18:47:11 +07:00
|
|
|
transformer: () =>
|
|
|
|
|
new import_prism_media.default.opus.Encoder({
|
|
|
|
|
rate: 48e3,
|
|
|
|
|
channels: 2,
|
|
|
|
|
frameSize: 960,
|
|
|
|
|
}),
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
2023-03-21 18:47:11 +07:00
|
|
|
getNode(StreamType.Opus).addEdge({
|
|
|
|
|
type: TransformerType.OpusDecoder,
|
|
|
|
|
to: getNode(StreamType.Raw),
|
2022-05-21 21:02:00 +07:00
|
|
|
cost: 1.5,
|
2023-03-21 18:47:11 +07:00
|
|
|
transformer: () =>
|
|
|
|
|
new import_prism_media.default.opus.Decoder({
|
|
|
|
|
rate: 48e3,
|
|
|
|
|
channels: 2,
|
|
|
|
|
frameSize: 960,
|
|
|
|
|
}),
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
2023-03-21 18:47:11 +07:00
|
|
|
getNode(StreamType.OggOpus).addEdge({
|
|
|
|
|
type: TransformerType.OggOpusDemuxer,
|
|
|
|
|
to: getNode(StreamType.Opus),
|
2022-05-21 21:02:00 +07:00
|
|
|
cost: 1,
|
2022-12-20 22:20:09 +07:00
|
|
|
transformer: () => new import_prism_media.default.opus.OggDemuxer(),
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
2023-03-21 18:47:11 +07:00
|
|
|
getNode(StreamType.WebmOpus).addEdge({
|
|
|
|
|
type: TransformerType.WebmOpusDemuxer,
|
|
|
|
|
to: getNode(StreamType.Opus),
|
2022-05-21 21:02:00 +07:00
|
|
|
cost: 1,
|
2022-12-20 22:20:09 +07:00
|
|
|
transformer: () => new import_prism_media.default.opus.WebmDemuxer(),
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
var FFMPEG_PCM_EDGE = {
|
2023-03-21 18:47:11 +07:00
|
|
|
type: TransformerType.FFmpegPCM,
|
|
|
|
|
to: getNode(StreamType.Raw),
|
2022-05-21 21:02:00 +07:00
|
|
|
cost: 2,
|
2022-12-20 22:20:09 +07:00
|
|
|
transformer: input =>
|
|
|
|
|
new import_prism_media.default.FFmpeg({
|
|
|
|
|
args: typeof input === 'string' ? ['-i', input, ...FFMPEG_PCM_ARGUMENTS] : FFMPEG_PCM_ARGUMENTS,
|
|
|
|
|
}),
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2023-03-21 18:47:11 +07:00
|
|
|
getNode(StreamType.Arbitrary).addEdge(FFMPEG_PCM_EDGE);
|
|
|
|
|
getNode(StreamType.OggOpus).addEdge(FFMPEG_PCM_EDGE);
|
|
|
|
|
getNode(StreamType.WebmOpus).addEdge(FFMPEG_PCM_EDGE);
|
|
|
|
|
getNode(StreamType.Raw).addEdge({
|
|
|
|
|
type: TransformerType.InlineVolume,
|
|
|
|
|
to: getNode(StreamType.Raw),
|
2022-05-21 21:02:00 +07:00
|
|
|
cost: 0.5,
|
2023-03-21 18:47:11 +07:00
|
|
|
transformer: () =>
|
|
|
|
|
new import_prism_media.default.VolumeTransformer({
|
|
|
|
|
type: 's16le',
|
|
|
|
|
}),
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
2022-08-01 13:02:58 +07:00
|
|
|
function canEnableFFmpegOptimizations() {
|
|
|
|
|
try {
|
2022-12-20 22:20:09 +07:00
|
|
|
return import_prism_media.default.FFmpeg.getInfo().output.includes('--enable-libopus');
|
|
|
|
|
} catch {}
|
2022-05-21 21:02:00 +07:00
|
|
|
return false;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(canEnableFFmpegOptimizations, 'canEnableFFmpegOptimizations');
|
2022-08-01 13:02:58 +07:00
|
|
|
if (canEnableFFmpegOptimizations()) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const FFMPEG_OGG_EDGE = {
|
2023-03-21 18:47:11 +07:00
|
|
|
type: TransformerType.FFmpegOgg,
|
|
|
|
|
to: getNode(StreamType.OggOpus),
|
2022-05-21 21:02:00 +07:00
|
|
|
cost: 2,
|
2022-12-20 22:20:09 +07:00
|
|
|
transformer: input =>
|
|
|
|
|
new import_prism_media.default.FFmpeg({
|
|
|
|
|
args: typeof input === 'string' ? ['-i', input, ...FFMPEG_OPUS_ARGUMENTS] : FFMPEG_OPUS_ARGUMENTS,
|
|
|
|
|
}),
|
2022-05-21 21:02:00 +07:00
|
|
|
};
|
2023-03-21 18:47:11 +07:00
|
|
|
getNode(StreamType.Arbitrary).addEdge(FFMPEG_OGG_EDGE);
|
|
|
|
|
getNode(StreamType.OggOpus).addEdge(FFMPEG_OGG_EDGE);
|
|
|
|
|
getNode(StreamType.WebmOpus).addEdge(FFMPEG_OGG_EDGE);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
function findPath(from, constraints, goal = getNode(StreamType.Opus), path = [], depth = 5) {
|
2022-08-01 13:02:58 +07:00
|
|
|
if (from === goal && constraints(path)) {
|
2023-03-21 18:47:11 +07:00
|
|
|
return {
|
|
|
|
|
cost: 0,
|
|
|
|
|
};
|
2022-08-01 13:02:58 +07:00
|
|
|
} else if (depth === 0) {
|
2023-03-21 18:47:11 +07:00
|
|
|
return {
|
|
|
|
|
cost: Number.POSITIVE_INFINITY,
|
|
|
|
|
};
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-09-28 19:40:46 +07:00
|
|
|
let currentBest;
|
2022-08-01 13:02:58 +07:00
|
|
|
for (const edge of from.edges) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (currentBest && edge.cost > currentBest.cost) continue;
|
2022-05-21 21:02:00 +07:00
|
|
|
const next = findPath(edge.to, constraints, goal, [...path, edge], depth - 1);
|
|
|
|
|
const cost = edge.cost + next.cost;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (!currentBest || cost < currentBest.cost) {
|
2023-03-21 18:47:11 +07:00
|
|
|
currentBest = {
|
|
|
|
|
cost,
|
|
|
|
|
edge,
|
|
|
|
|
next,
|
|
|
|
|
};
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
return (
|
|
|
|
|
currentBest ?? {
|
|
|
|
|
cost: Number.POSITIVE_INFINITY,
|
|
|
|
|
}
|
|
|
|
|
);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(findPath, 'findPath');
|
2022-08-01 13:02:58 +07:00
|
|
|
function constructPipeline(step) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const edges = [];
|
|
|
|
|
let current = step;
|
2022-08-01 13:02:58 +07:00
|
|
|
while (current?.edge) {
|
2022-05-21 21:02:00 +07:00
|
|
|
edges.push(current.edge);
|
|
|
|
|
current = current.next;
|
|
|
|
|
}
|
|
|
|
|
return edges;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(constructPipeline, 'constructPipeline');
|
2022-08-01 13:02:58 +07:00
|
|
|
function findPipeline(from, constraint) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return constructPipeline(findPath(getNode(from), constraint));
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(findPipeline, 'findPipeline');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/audio/AudioResource.ts
|
2022-08-01 13:02:58 +07:00
|
|
|
var AudioResource = class {
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* The playback duration of this audio resource, given in milliseconds.
|
|
|
|
|
*/
|
2022-09-28 19:40:46 +07:00
|
|
|
playbackDuration = 0;
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Whether or not the stream for this resource has started (data has become readable)
|
|
|
|
|
*/
|
2022-09-28 19:40:46 +07:00
|
|
|
started = false;
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* The number of remaining silence frames to play. If -1, the frames have not yet started playing.
|
|
|
|
|
*/
|
2022-09-28 19:40:46 +07:00
|
|
|
silenceRemaining = -1;
|
2022-08-01 13:02:58 +07:00
|
|
|
constructor(edges, streams, metadata, silencePaddingFrames) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.edges = edges;
|
|
|
|
|
this.playStream = streams.length > 1 ? (0, import_node_stream2.pipeline)(streams, noop) : streams[0];
|
|
|
|
|
this.metadata = metadata;
|
|
|
|
|
this.silencePaddingFrames = silencePaddingFrames;
|
2022-08-01 13:02:58 +07:00
|
|
|
for (const stream of streams) {
|
|
|
|
|
if (stream instanceof import_prism_media2.default.VolumeTransformer) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.volume = stream;
|
2022-08-01 13:02:58 +07:00
|
|
|
} else if (stream instanceof import_prism_media2.default.opus.Encoder) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.encoder = stream;
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
this.playStream.once('readable', () => (this.started = true));
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Whether this resource is readable. If the underlying resource is no longer readable, this will still return true
|
|
|
|
|
* while there are silence padding frames left to play.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
get readable() {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (this.silenceRemaining === 0) return false;
|
2022-05-21 21:02:00 +07:00
|
|
|
const real = this.playStream.readable;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (!real) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (this.silenceRemaining === -1) this.silenceRemaining = this.silencePaddingFrames;
|
2022-05-21 21:02:00 +07:00
|
|
|
return this.silenceRemaining !== 0;
|
|
|
|
|
}
|
|
|
|
|
return real;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Whether this resource has ended or not.
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
get ended() {
|
2022-05-21 21:02:00 +07:00
|
|
|
return this.playStream.readableEnded || this.playStream.destroyed || this.silenceRemaining === 0;
|
|
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
/**
|
|
|
|
|
* Attempts to read an Opus packet from the audio resource. If a packet is available, the playbackDuration
|
|
|
|
|
* is incremented.
|
|
|
|
|
*
|
|
|
|
|
* @remarks
|
|
|
|
|
* It is advisable to check that the playStream is readable before calling this method. While no runtime
|
|
|
|
|
* errors will be thrown, you should check that the resource is still available before attempting to
|
|
|
|
|
* read from it.
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
2022-08-01 13:02:58 +07:00
|
|
|
read() {
|
|
|
|
|
if (this.silenceRemaining === 0) {
|
2022-05-21 21:02:00 +07:00
|
|
|
return null;
|
2022-08-01 13:02:58 +07:00
|
|
|
} else if (this.silenceRemaining > 0) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.silenceRemaining--;
|
|
|
|
|
return SILENCE_FRAME;
|
|
|
|
|
}
|
|
|
|
|
const packet = this.playStream.read();
|
2022-08-01 13:02:58 +07:00
|
|
|
if (packet) {
|
2022-05-21 21:02:00 +07:00
|
|
|
this.playbackDuration += 20;
|
|
|
|
|
}
|
|
|
|
|
return packet;
|
|
|
|
|
}
|
|
|
|
|
};
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(AudioResource, 'AudioResource');
|
|
|
|
|
var VOLUME_CONSTRAINT = /* @__PURE__ */ __name(
|
2023-03-21 18:47:11 +07:00
|
|
|
path => path.some(edge => edge.type === TransformerType.InlineVolume),
|
2022-12-20 22:20:09 +07:00
|
|
|
'VOLUME_CONSTRAINT',
|
|
|
|
|
);
|
|
|
|
|
var NO_CONSTRAINT = /* @__PURE__ */ __name(() => true, 'NO_CONSTRAINT');
|
2022-08-01 13:02:58 +07:00
|
|
|
function inferStreamType(stream) {
|
|
|
|
|
if (stream instanceof import_prism_media2.default.opus.Encoder) {
|
2023-03-21 18:47:11 +07:00
|
|
|
return {
|
|
|
|
|
streamType: StreamType.Opus,
|
|
|
|
|
hasVolume: false,
|
|
|
|
|
};
|
2022-08-01 13:02:58 +07:00
|
|
|
} else if (stream instanceof import_prism_media2.default.opus.Decoder) {
|
2023-03-21 18:47:11 +07:00
|
|
|
return {
|
|
|
|
|
streamType: StreamType.Raw,
|
|
|
|
|
hasVolume: false,
|
|
|
|
|
};
|
2022-08-01 13:02:58 +07:00
|
|
|
} else if (stream instanceof import_prism_media2.default.VolumeTransformer) {
|
2023-03-21 18:47:11 +07:00
|
|
|
return {
|
|
|
|
|
streamType: StreamType.Raw,
|
|
|
|
|
hasVolume: true,
|
|
|
|
|
};
|
2022-08-01 13:02:58 +07:00
|
|
|
} else if (stream instanceof import_prism_media2.default.opus.OggDemuxer) {
|
2023-03-21 18:47:11 +07:00
|
|
|
return {
|
|
|
|
|
streamType: StreamType.Opus,
|
|
|
|
|
hasVolume: false,
|
|
|
|
|
};
|
2022-08-01 13:02:58 +07:00
|
|
|
} else if (stream instanceof import_prism_media2.default.opus.WebmDemuxer) {
|
2023-03-21 18:47:11 +07:00
|
|
|
return {
|
|
|
|
|
streamType: StreamType.Opus,
|
|
|
|
|
hasVolume: false,
|
|
|
|
|
};
|
2022-08-01 13:02:58 +07:00
|
|
|
}
|
2023-03-21 18:47:11 +07:00
|
|
|
return {
|
|
|
|
|
streamType: StreamType.Arbitrary,
|
|
|
|
|
hasVolume: false,
|
|
|
|
|
};
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(inferStreamType, 'inferStreamType');
|
2022-08-01 13:02:58 +07:00
|
|
|
function createAudioResource(input, options = {}) {
|
2022-05-21 21:02:00 +07:00
|
|
|
let inputType = options.inputType;
|
|
|
|
|
let needsInlineVolume = Boolean(options.inlineVolume);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (typeof input === 'string') {
|
2023-03-21 18:47:11 +07:00
|
|
|
inputType = StreamType.Arbitrary;
|
2022-12-20 22:20:09 +07:00
|
|
|
} else if (typeof inputType === 'undefined') {
|
2022-05-21 21:02:00 +07:00
|
|
|
const analysis = inferStreamType(input);
|
|
|
|
|
inputType = analysis.streamType;
|
|
|
|
|
needsInlineVolume = needsInlineVolume && !analysis.hasVolume;
|
|
|
|
|
}
|
|
|
|
|
const transformerPipeline = findPipeline(inputType, needsInlineVolume ? VOLUME_CONSTRAINT : NO_CONSTRAINT);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (transformerPipeline.length === 0) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (typeof input === 'string') throw new Error(`Invalid pipeline constructed for string resource '${input}'`);
|
2022-05-21 21:02:00 +07:00
|
|
|
return new AudioResource([], [input], options.metadata ?? null, options.silencePaddingFrames ?? 5);
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
const streams = transformerPipeline.map(edge => edge.transformer(input));
|
|
|
|
|
if (typeof input !== 'string') streams.unshift(input);
|
|
|
|
|
return new AudioResource(transformerPipeline, streams, options.metadata ?? null, options.silencePaddingFrames ?? 5);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(createAudioResource, 'createAudioResource');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/util/generateDependencyReport.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_path = require('path');
|
|
|
|
|
var import_prism_media3 = __toESM(require('prism-media'));
|
2022-08-01 13:02:58 +07:00
|
|
|
function findPackageJSON(dir, packageName, depth) {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (depth === 0) return void 0;
|
|
|
|
|
const attemptedPath = (0, import_node_path.resolve)(dir, './package.json');
|
2022-08-01 13:02:58 +07:00
|
|
|
try {
|
2022-05-21 21:02:00 +07:00
|
|
|
const pkg = require(attemptedPath);
|
2022-12-20 22:20:09 +07:00
|
|
|
if (pkg.name !== packageName) throw new Error('package.json does not match');
|
2022-05-21 21:02:00 +07:00
|
|
|
return pkg;
|
2022-09-28 19:40:46 +07:00
|
|
|
} catch {
|
2022-12-20 22:20:09 +07:00
|
|
|
return findPackageJSON((0, import_node_path.resolve)(dir, '..'), packageName, depth - 1);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(findPackageJSON, 'findPackageJSON');
|
2022-08-01 13:02:58 +07:00
|
|
|
function version(name) {
|
|
|
|
|
try {
|
2022-12-20 22:20:09 +07:00
|
|
|
if (name === '@discordjs/voice') {
|
2023-03-21 18:47:11 +07:00
|
|
|
return '[VI]{{inject}}[/VI]';
|
2022-10-14 18:12:47 +07:00
|
|
|
}
|
|
|
|
|
const pkg = findPackageJSON((0, import_node_path.dirname)(require.resolve(name)), name, 3);
|
2022-12-20 22:20:09 +07:00
|
|
|
return pkg?.version ?? 'not found';
|
2022-09-28 19:40:46 +07:00
|
|
|
} catch {
|
2022-12-20 22:20:09 +07:00
|
|
|
return 'not found';
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(version, 'version');
|
2022-08-01 13:02:58 +07:00
|
|
|
function generateDependencyReport() {
|
2022-05-21 21:02:00 +07:00
|
|
|
const report = [];
|
2022-12-20 22:20:09 +07:00
|
|
|
const addVersion = /* @__PURE__ */ __name(name => report.push(`- ${name}: ${version(name)}`), 'addVersion');
|
|
|
|
|
report.push('Core Dependencies');
|
|
|
|
|
addVersion('@discordjs/voice');
|
|
|
|
|
addVersion('prism-media');
|
|
|
|
|
report.push('');
|
|
|
|
|
report.push('Opus Libraries');
|
|
|
|
|
addVersion('@discordjs/opus');
|
|
|
|
|
addVersion('opusscript');
|
|
|
|
|
report.push('');
|
|
|
|
|
report.push('Encryption Libraries');
|
|
|
|
|
addVersion('sodium-native');
|
|
|
|
|
addVersion('sodium');
|
|
|
|
|
addVersion('libsodium-wrappers');
|
|
|
|
|
addVersion('tweetnacl');
|
|
|
|
|
report.push('');
|
|
|
|
|
report.push('FFmpeg');
|
2022-08-01 13:02:58 +07:00
|
|
|
try {
|
2022-05-21 21:02:00 +07:00
|
|
|
const info = import_prism_media3.default.FFmpeg.getInfo();
|
|
|
|
|
report.push(`- version: ${info.version}`);
|
2022-12-20 22:20:09 +07:00
|
|
|
report.push(`- libopus: ${info.output.includes('--enable-libopus') ? 'yes' : 'no'}`);
|
2022-09-28 19:40:46 +07:00
|
|
|
} catch {
|
2022-12-20 22:20:09 +07:00
|
|
|
report.push('- not found');
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
return ['-'.repeat(50), ...report, '-'.repeat(50)].join('\n');
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(generateDependencyReport, 'generateDependencyReport');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/util/entersState.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_events8 = require('events');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/util/abortAfter.ts
|
2022-08-01 13:02:58 +07:00
|
|
|
function abortAfter(delay) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const ac = new AbortController();
|
|
|
|
|
const timeout = setTimeout(() => ac.abort(), delay);
|
2022-12-20 22:20:09 +07:00
|
|
|
ac.signal.addEventListener('abort', () => clearTimeout(timeout));
|
2022-05-21 21:02:00 +07:00
|
|
|
return [ac, ac.signal];
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(abortAfter, 'abortAfter');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/util/entersState.ts
|
2022-08-01 13:02:58 +07:00
|
|
|
async function entersState(target, status, timeoutOrSignal) {
|
|
|
|
|
if (target.state.status !== status) {
|
2022-12-20 22:20:09 +07:00
|
|
|
const [ac, signal] = typeof timeoutOrSignal === 'number' ? abortAfter(timeoutOrSignal) : [void 0, timeoutOrSignal];
|
2022-08-01 13:02:58 +07:00
|
|
|
try {
|
2023-03-21 18:47:11 +07:00
|
|
|
await (0, import_node_events8.once)(target, status, {
|
|
|
|
|
signal,
|
|
|
|
|
});
|
2022-08-01 13:02:58 +07:00
|
|
|
} finally {
|
2022-05-21 21:02:00 +07:00
|
|
|
ac?.abort();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return target;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(entersState, 'entersState');
|
2022-05-21 21:02:00 +07:00
|
|
|
|
2022-06-26 09:40:04 +07:00
|
|
|
// src/util/demuxProbe.ts
|
2022-12-20 22:20:09 +07:00
|
|
|
var import_node_buffer6 = require('buffer');
|
|
|
|
|
var import_node_process = __toESM(require('process'));
|
|
|
|
|
var import_node_stream3 = require('stream');
|
|
|
|
|
var import_prism_media4 = __toESM(require('prism-media'));
|
2022-08-01 13:02:58 +07:00
|
|
|
function validateDiscordOpusHead(opusHead) {
|
2022-05-21 21:02:00 +07:00
|
|
|
const channels = opusHead.readUInt8(9);
|
|
|
|
|
const sampleRate = opusHead.readUInt32LE(12);
|
|
|
|
|
return channels === 2 && sampleRate === 48e3;
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(validateDiscordOpusHead, 'validateDiscordOpusHead');
|
2022-09-28 19:40:46 +07:00
|
|
|
async function demuxProbe(stream, probeSize = 1024, validator = validateDiscordOpusHead) {
|
2022-08-01 13:02:58 +07:00
|
|
|
return new Promise((resolve2, reject) => {
|
2022-09-28 19:40:46 +07:00
|
|
|
if (stream.readableObjectMode) {
|
2022-12-20 22:20:09 +07:00
|
|
|
reject(new Error('Cannot probe a readable stream in object mode'));
|
2022-09-28 19:40:46 +07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (stream.readableEnded) {
|
2022-12-20 22:20:09 +07:00
|
|
|
reject(new Error('Cannot probe a stream that has ended'));
|
2022-09-28 19:40:46 +07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let readBuffer = import_node_buffer6.Buffer.alloc(0);
|
|
|
|
|
let resolved;
|
2022-12-20 22:20:09 +07:00
|
|
|
const finish = /* @__PURE__ */ __name(type => {
|
|
|
|
|
stream.off('data', onData);
|
|
|
|
|
stream.off('close', onClose);
|
|
|
|
|
stream.off('end', onClose);
|
2022-05-21 21:02:00 +07:00
|
|
|
stream.pause();
|
|
|
|
|
resolved = type;
|
2022-08-01 13:02:58 +07:00
|
|
|
if (stream.readableEnded) {
|
2022-05-21 21:02:00 +07:00
|
|
|
resolve2({
|
|
|
|
|
stream: import_node_stream3.Readable.from(readBuffer),
|
2022-12-20 22:20:09 +07:00
|
|
|
type,
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
2022-08-01 13:02:58 +07:00
|
|
|
} else {
|
|
|
|
|
if (readBuffer.length > 0) {
|
2022-05-21 21:02:00 +07:00
|
|
|
stream.push(readBuffer);
|
|
|
|
|
}
|
|
|
|
|
resolve2({
|
|
|
|
|
stream,
|
2022-12-20 22:20:09 +07:00
|
|
|
type,
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
}, 'finish');
|
|
|
|
|
const foundHead = /* @__PURE__ */ __name(
|
|
|
|
|
type => head => {
|
|
|
|
|
if (validator(head)) {
|
|
|
|
|
finish(type);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
'foundHead',
|
|
|
|
|
);
|
2022-05-21 21:02:00 +07:00
|
|
|
const webm = new import_prism_media4.default.opus.WebmDemuxer();
|
2022-12-20 22:20:09 +07:00
|
|
|
webm.once('error', noop);
|
2023-03-21 18:47:11 +07:00
|
|
|
webm.on('head', foundHead(StreamType.WebmOpus));
|
2022-05-21 21:02:00 +07:00
|
|
|
const ogg = new import_prism_media4.default.opus.OggDemuxer();
|
2022-12-20 22:20:09 +07:00
|
|
|
ogg.once('error', noop);
|
2023-03-21 18:47:11 +07:00
|
|
|
ogg.on('head', foundHead(StreamType.OggOpus));
|
2022-08-01 13:02:58 +07:00
|
|
|
const onClose = /* @__PURE__ */ __name(() => {
|
|
|
|
|
if (!resolved) {
|
2023-03-21 18:47:11 +07:00
|
|
|
finish(StreamType.Arbitrary);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
}, 'onClose');
|
|
|
|
|
const onData = /* @__PURE__ */ __name(buffer => {
|
2022-09-28 19:40:46 +07:00
|
|
|
readBuffer = import_node_buffer6.Buffer.concat([readBuffer, buffer]);
|
2022-05-21 21:02:00 +07:00
|
|
|
webm.write(buffer);
|
|
|
|
|
ogg.write(buffer);
|
2022-08-01 13:02:58 +07:00
|
|
|
if (readBuffer.length >= probeSize) {
|
2022-12-20 22:20:09 +07:00
|
|
|
stream.off('data', onData);
|
2022-05-21 21:02:00 +07:00
|
|
|
stream.pause();
|
2022-09-28 19:40:46 +07:00
|
|
|
import_node_process.default.nextTick(onClose);
|
2022-05-21 21:02:00 +07:00
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
}, 'onData');
|
|
|
|
|
stream.once('error', reject);
|
|
|
|
|
stream.on('data', onData);
|
|
|
|
|
stream.once('close', onClose);
|
|
|
|
|
stream.once('end', onClose);
|
2022-05-21 21:02:00 +07:00
|
|
|
});
|
|
|
|
|
}
|
2022-12-20 22:20:09 +07:00
|
|
|
__name(demuxProbe, 'demuxProbe');
|
2022-10-14 18:12:47 +07:00
|
|
|
|
|
|
|
|
// src/index.ts
|
2023-03-21 18:47:11 +07:00
|
|
|
var version2 = '[VI]{{inject}}[/VI]';
|
2022-05-21 21:02:00 +07:00
|
|
|
// Annotate the CommonJS export names for ESM import in node:
|
2022-12-20 22:20:09 +07:00
|
|
|
0 &&
|
|
|
|
|
(module.exports = {
|
|
|
|
|
AudioPlayer,
|
|
|
|
|
AudioPlayerError,
|
|
|
|
|
AudioPlayerStatus,
|
|
|
|
|
AudioReceiveStream,
|
|
|
|
|
AudioResource,
|
|
|
|
|
EndBehaviorType,
|
|
|
|
|
NoSubscriberBehavior,
|
|
|
|
|
PlayerSubscription,
|
|
|
|
|
SSRCMap,
|
|
|
|
|
SpeakingMap,
|
|
|
|
|
StreamType,
|
|
|
|
|
VoiceConnection,
|
|
|
|
|
VoiceConnectionDisconnectReason,
|
|
|
|
|
VoiceConnectionStatus,
|
|
|
|
|
VoiceReceiver,
|
|
|
|
|
createAudioPlayer,
|
|
|
|
|
createAudioResource,
|
|
|
|
|
createDefaultAudioReceiveStreamOptions,
|
|
|
|
|
demuxProbe,
|
|
|
|
|
entersState,
|
|
|
|
|
generateDependencyReport,
|
|
|
|
|
getGroups,
|
|
|
|
|
getVoiceConnection,
|
|
|
|
|
getVoiceConnections,
|
|
|
|
|
joinVoiceChannel,
|
|
|
|
|
validateDiscordOpusHead,
|
|
|
|
|
version,
|
|
|
|
|
});
|
|
|
|
|
//# sourceMappingURL=index.js.map
|