mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-14 02:23:31 +01:00
refactor: use eslint-config-neon for packages. (#8579)
Co-authored-by: Noel <buechler.noel@outlook.com>
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
"extends": "../../.eslintrc.json",
|
||||
"plugins": ["eslint-plugin-tsdoc"],
|
||||
"rules": {
|
||||
"tsdoc/syntax": "warn"
|
||||
"import/extensions": 0,
|
||||
"no-restricted-globals": 0
|
||||
},
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.eslint.json",
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/dot-notation */
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { once } from 'node:events';
|
||||
import process from 'node:process';
|
||||
import { Readable } from 'node:stream';
|
||||
import { NoSubscriberBehavior } from '../src';
|
||||
import { addAudioPlayer, deleteAudioPlayer } from '../src/DataStore';
|
||||
import { VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection';
|
||||
import { createAudioPlayer, AudioPlayerStatus, AudioPlayer, SILENCE_FRAME } from '../src/audio/AudioPlayer';
|
||||
import { createAudioPlayer, AudioPlayerStatus, SILENCE_FRAME, type AudioPlayerState } from '../src/audio/AudioPlayer';
|
||||
import { AudioPlayerError } from '../src/audio/AudioPlayerError';
|
||||
import { AudioResource } from '../src/audio/AudioResource';
|
||||
import { NoSubscriberBehavior } from '../src/index';
|
||||
|
||||
jest.mock('../src/DataStore');
|
||||
jest.mock('../src/VoiceConnection');
|
||||
@@ -39,7 +42,8 @@ function createVoiceConnectionMock() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
function wait() {
|
||||
async function wait() {
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
return new Promise((resolve) => process.nextTick(resolve));
|
||||
}
|
||||
|
||||
@@ -47,6 +51,7 @@ async function started(resource: AudioResource) {
|
||||
while (!resource.started) {
|
||||
await wait();
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
@@ -197,6 +202,7 @@ describe('State transitions', () => {
|
||||
if (connection.state.status !== VoiceConnectionStatus.Signalling) {
|
||||
throw new Error('Voice connection should have been Signalling');
|
||||
}
|
||||
|
||||
connection.state = {
|
||||
...connection.state,
|
||||
status: VoiceConnectionStatus.Ready,
|
||||
@@ -216,18 +222,18 @@ describe('State transitions', () => {
|
||||
expect(player.checkPlayable()).toEqual(true);
|
||||
|
||||
// Run through a few packet cycles
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
for (let index = 1; index <= 5; index++) {
|
||||
player['_stepDispatch']();
|
||||
expect(connection.dispatchAudio).toHaveBeenCalledTimes(i);
|
||||
expect(connection.dispatchAudio).toHaveBeenCalledTimes(index);
|
||||
|
||||
await wait(); // Wait for the stream
|
||||
|
||||
player['_stepPrepare']();
|
||||
expect(connection.prepareAudioPacket).toHaveBeenCalledTimes(i);
|
||||
expect(connection.prepareAudioPacket).toHaveBeenCalledTimes(index);
|
||||
expect(connection.prepareAudioPacket).toHaveBeenLastCalledWith(buffer);
|
||||
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
|
||||
if (player.state.status === AudioPlayerStatus.Playing) {
|
||||
expect(player.state.playbackDuration).toStrictEqual(i * 20);
|
||||
expect(player.state.playbackDuration).toStrictEqual(index * 20);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +260,7 @@ describe('State transitions', () => {
|
||||
if (connection.state.status !== VoiceConnectionStatus.Signalling) {
|
||||
throw new Error('Voice connection should have been Signalling');
|
||||
}
|
||||
|
||||
connection.state = {
|
||||
...connection.state,
|
||||
status: VoiceConnectionStatus.Ready,
|
||||
@@ -275,20 +282,21 @@ describe('State transitions', () => {
|
||||
player.stop();
|
||||
|
||||
// Run through a few packet cycles
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
for (let index = 1; index <= 5; index++) {
|
||||
player['_stepDispatch']();
|
||||
expect(connection.dispatchAudio).toHaveBeenCalledTimes(i);
|
||||
expect(connection.dispatchAudio).toHaveBeenCalledTimes(index);
|
||||
|
||||
await wait(); // Wait for the stream
|
||||
|
||||
player['_stepPrepare']();
|
||||
expect(connection.prepareAudioPacket).toHaveBeenCalledTimes(i);
|
||||
expect(connection.prepareAudioPacket).toHaveBeenCalledTimes(index);
|
||||
expect(connection.prepareAudioPacket).toHaveBeenLastCalledWith(SILENCE_FRAME);
|
||||
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
|
||||
if (player.state.status === AudioPlayerStatus.Playing) {
|
||||
expect(player.state.playbackDuration).toStrictEqual(i * 20);
|
||||
expect(player.state.playbackDuration).toStrictEqual(index * 20);
|
||||
}
|
||||
}
|
||||
|
||||
await wait();
|
||||
expect(player.checkPlayable()).toEqual(false);
|
||||
const prepareAudioPacket = connection.prepareAudioPacket as unknown as jest.Mock<
|
||||
@@ -307,6 +315,7 @@ describe('State transitions', () => {
|
||||
if (connection.state.status !== VoiceConnectionStatus.Signalling) {
|
||||
throw new Error('Voice connection should have been Signalling');
|
||||
}
|
||||
|
||||
connection.state = {
|
||||
...connection.state,
|
||||
status: VoiceConnectionStatus.Ready,
|
||||
@@ -328,16 +337,16 @@ describe('State transitions', () => {
|
||||
>;
|
||||
|
||||
// Run through a few packet cycles
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
for (let index = 1; index <= 5; index++) {
|
||||
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
|
||||
if (player.state.status !== AudioPlayerStatus.Playing) throw new Error('Error');
|
||||
expect(player.state.playbackDuration).toStrictEqual((i - 1) * 20);
|
||||
expect(player.state.missedFrames).toEqual(i - 1);
|
||||
expect(player.state.playbackDuration).toStrictEqual((index - 1) * 20);
|
||||
expect(player.state.missedFrames).toEqual(index - 1);
|
||||
player['_stepDispatch']();
|
||||
expect(connection.dispatchAudio).toHaveBeenCalledTimes(i);
|
||||
expect(connection.dispatchAudio).toHaveBeenCalledTimes(index);
|
||||
player['_stepPrepare']();
|
||||
expect(prepareAudioPacket).toHaveBeenCalledTimes(i);
|
||||
expect(prepareAudioPacket.mock.calls[i - 1][0]).toEqual(silence().next().value);
|
||||
expect(prepareAudioPacket).toHaveBeenCalledTimes(index);
|
||||
expect(prepareAudioPacket.mock.calls[index - 1][0]).toEqual(silence().next().value);
|
||||
}
|
||||
|
||||
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
|
||||
@@ -352,10 +361,11 @@ describe('State transitions', () => {
|
||||
player.play(resource);
|
||||
expect(player.checkPlayable()).toEqual(true);
|
||||
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (let index = 0; index < 3; index++) {
|
||||
resource.playStream.read();
|
||||
await wait();
|
||||
}
|
||||
|
||||
expect(resource.playStream.readableEnded).toEqual(true);
|
||||
expect(player.checkPlayable()).toEqual(false);
|
||||
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
|
||||
@@ -367,10 +377,11 @@ test('play() throws when playing a resource that has already ended', async () =>
|
||||
player = createAudioPlayer();
|
||||
player.play(resource);
|
||||
expect(player.state.status).toEqual(AudioPlayerStatus.Playing);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (let index = 0; index < 3; index++) {
|
||||
resource.playStream.read();
|
||||
await wait();
|
||||
}
|
||||
|
||||
expect(resource.playStream.readableEnded).toEqual(true);
|
||||
player.stop(true);
|
||||
expect(player.state.status).toEqual(AudioPlayerStatus.Idle);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
/* eslint-disable no-promise-executor-return */
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { SILENCE_FRAME } from '../src/audio/AudioPlayer';
|
||||
import { AudioReceiveStream, EndBehaviorType } from '../src/receive/AudioReceiveStream';
|
||||
|
||||
const DUMMY_BUFFER = Buffer.allocUnsafe(16);
|
||||
|
||||
function wait(ms: number) {
|
||||
async function wait(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -52,13 +54,13 @@ describe('AudioReceiveStream', () => {
|
||||
const stream = new AudioReceiveStream({ end: { behavior: EndBehaviorType.AfterInactivity, duration: 100 } });
|
||||
stream.resume();
|
||||
|
||||
for (let i = increment; i < duration / 2; i += increment) {
|
||||
for (let index = increment; index < duration / 2; index += increment) {
|
||||
await stepSilence(stream, increment);
|
||||
}
|
||||
|
||||
stream.push(DUMMY_BUFFER);
|
||||
|
||||
for (let i = increment; i < duration; i += increment) {
|
||||
for (let index = increment; index < duration; index += increment) {
|
||||
await stepSilence(stream, increment);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
import { Buffer } from 'node:buffer';
|
||||
import process from 'node:process';
|
||||
import { PassThrough, Readable } from 'node:stream';
|
||||
import { opus, VolumeTransformer } from 'prism-media';
|
||||
import { SILENCE_FRAME } from '../src/audio/AudioPlayer';
|
||||
import { AudioResource, createAudioResource, NO_CONSTRAINT, VOLUME_CONSTRAINT } from '../src/audio/AudioResource';
|
||||
import { Edge, findPipeline as _findPipeline, StreamType, TransformerType } from '../src/audio/TransformerGraph';
|
||||
import { findPipeline as _findPipeline, StreamType, TransformerType, type Edge } from '../src/audio/TransformerGraph';
|
||||
|
||||
jest.mock('prism-media');
|
||||
jest.mock('../src/audio/TransformerGraph');
|
||||
|
||||
function wait() {
|
||||
async function wait() {
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
return new Promise((resolve) => process.nextTick(resolve));
|
||||
}
|
||||
|
||||
@@ -16,12 +19,14 @@ async function started(resource: AudioResource) {
|
||||
while (!resource.started) {
|
||||
await wait();
|
||||
}
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
const findPipeline = _findPipeline as unknown as jest.MockedFunction<typeof _findPipeline>;
|
||||
|
||||
beforeAll(() => {
|
||||
// @ts-expect-error no type
|
||||
findPipeline.mockImplementation((from: StreamType, constraint: (path: Edge[]) => boolean) => {
|
||||
const base = [
|
||||
{
|
||||
@@ -38,6 +43,7 @@ beforeAll(() => {
|
||||
type: TransformerType.InlineVolume,
|
||||
});
|
||||
}
|
||||
|
||||
return base as any[];
|
||||
});
|
||||
});
|
||||
@@ -113,11 +119,12 @@ describe('createAudioResource', () => {
|
||||
await started(resource);
|
||||
expect(resource.readable).toEqual(true);
|
||||
expect(resource.read()).toEqual(Buffer.from([1]));
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let index = 0; index < 5; index++) {
|
||||
await wait();
|
||||
expect(resource.readable).toEqual(true);
|
||||
expect(resource.read()).toEqual(SILENCE_FRAME);
|
||||
}
|
||||
|
||||
await wait();
|
||||
expect(resource.readable).toEqual(false);
|
||||
expect(resource.read()).toEqual(null);
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
/* eslint-disable @typescript-eslint/dot-notation */
|
||||
import { GatewayOpcodes } from 'discord-api-types/v10';
|
||||
import * as DataStore from '../src/DataStore';
|
||||
import { VoiceConnection } from '../src/VoiceConnection';
|
||||
import type { VoiceConnection } from '../src/VoiceConnection';
|
||||
import * as _AudioPlayer from '../src/audio/AudioPlayer';
|
||||
|
||||
jest.mock('../src/VoiceConnection');
|
||||
jest.mock('../src/audio/AudioPlayer');
|
||||
|
||||
@@ -15,8 +16,9 @@ function createVoiceConnection(joinConfig: Pick<DataStore.JoinConfig, 'group' |
|
||||
} as any;
|
||||
}
|
||||
|
||||
function waitForEventLoop() {
|
||||
return new Promise((res) => setImmediate(res));
|
||||
async function waitForEventLoop() {
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
return new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -24,6 +26,7 @@ beforeEach(() => {
|
||||
for (const groupKey of groups.keys()) {
|
||||
groups.delete(groupKey);
|
||||
}
|
||||
|
||||
groups.set('default', new Map());
|
||||
});
|
||||
|
||||
@@ -41,6 +44,7 @@ describe('DataStore', () => {
|
||||
};
|
||||
expect(DataStore.createJoinVoiceChannelPayload(joinConfig)).toStrictEqual({
|
||||
op: GatewayOpcodes.VoiceStateUpdate,
|
||||
// eslint-disable-next-line id-length
|
||||
d: {
|
||||
guild_id: joinConfig.guildId,
|
||||
channel_id: joinConfig.channelId,
|
||||
@@ -60,7 +64,7 @@ describe('DataStore', () => {
|
||||
|
||||
expect([...DataStore.getVoiceConnections().values()]).toEqual([voiceConnectionDefault]);
|
||||
expect([...DataStore.getVoiceConnections('default').values()]).toEqual([voiceConnectionDefault]);
|
||||
expect([...DataStore.getVoiceConnections('abc').values()]).toEqual([voiceConnectionAbc]);
|
||||
expect([...DataStore.getVoiceConnections('abc')!.values()]).toEqual([voiceConnectionAbc]);
|
||||
|
||||
DataStore.untrackVoiceConnection(voiceConnectionDefault);
|
||||
expect(DataStore.getVoiceConnection('123')).toBeUndefined();
|
||||
@@ -73,6 +77,7 @@ describe('DataStore', () => {
|
||||
expect(DataStore.hasAudioPlayer(player)).toEqual(true);
|
||||
expect(DataStore.addAudioPlayer(player)).toEqual(player);
|
||||
DataStore.deleteAudioPlayer(player);
|
||||
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
||||
expect(DataStore.deleteAudioPlayer(player)).toBeUndefined();
|
||||
expect(DataStore.hasAudioPlayer(player)).toEqual(false);
|
||||
// Tests audio cycle with nextTime === -1
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import EventEmitter, { once } from 'node:events';
|
||||
import { SSRCMap, VoiceUserData } from '../src/receive/SSRCMap';
|
||||
import type EventEmitter from 'node:events';
|
||||
import { once } from 'node:events';
|
||||
import process from 'node:process';
|
||||
import { SSRCMap, type VoiceUserData } from '../src/receive/SSRCMap';
|
||||
|
||||
function onceOrThrow<T extends EventEmitter>(target: T, event: string, after: number) {
|
||||
async function onceOrThrow<T extends EventEmitter>(target: T, event: string, after: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
target.on(event, resolve);
|
||||
setTimeout(() => reject(new Error('Time up')), after);
|
||||
|
||||
@@ -3,6 +3,6 @@ import { methods } from '../src/util/Secretbox';
|
||||
jest.mock('tweetnacl');
|
||||
|
||||
test('Does not throw error with a package installed', () => {
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error: Unknown type
|
||||
expect(() => methods.open()).not.toThrowError();
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('SpeakingMap', () => {
|
||||
speaking.on('start', (userId) => void starts.push(userId));
|
||||
speaking.on('end', (userId) => void ends.push(userId));
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let index = 0; index < 10; index++) {
|
||||
speaking.onPacket(userId);
|
||||
setTimeout(noop, SpeakingMap.DELAY / 2);
|
||||
jest.advanceTimersToNextTimer();
|
||||
@@ -22,6 +22,7 @@ describe('SpeakingMap', () => {
|
||||
expect(starts).toEqual([userId]);
|
||||
expect(ends).toEqual([]);
|
||||
}
|
||||
|
||||
jest.advanceTimersToNextTimer();
|
||||
expect(ends).toEqual([userId]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Edge, findPipeline, StreamType, TransformerType } from '../src/audio/TransformerGraph';
|
||||
import { findPipeline, StreamType, TransformerType, type Edge } from '../src/audio/TransformerGraph';
|
||||
|
||||
const noConstraint = () => true;
|
||||
|
||||
@@ -12,6 +12,7 @@ function reducePath(pipeline: Edge[]) {
|
||||
for (const edge of pipeline.slice(1)) {
|
||||
streams.push(edge.from.type);
|
||||
}
|
||||
|
||||
streams.push(pipeline[pipeline.length - 1].to.type);
|
||||
return streams;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
@@ -8,13 +9,12 @@ import * as _DataStore from '../src/DataStore';
|
||||
import {
|
||||
createVoiceConnection,
|
||||
VoiceConnection,
|
||||
VoiceConnectionConnectingState,
|
||||
VoiceConnectionDisconnectReason,
|
||||
VoiceConnectionReadyState,
|
||||
VoiceConnectionSignallingState,
|
||||
VoiceConnectionStatus,
|
||||
type VoiceConnectionConnectingState,
|
||||
type VoiceConnectionReadyState,
|
||||
type VoiceConnectionSignallingState,
|
||||
} from '../src/VoiceConnection';
|
||||
|
||||
import * as _AudioPlayer from '../src/audio/AudioPlayer';
|
||||
import { PlayerSubscription as _PlayerSubscription } from '../src/audio/PlayerSubscription';
|
||||
import * as _Networking from '../src/networking/Networking';
|
||||
@@ -129,6 +129,7 @@ describe('createVoiceConnection', () => {
|
||||
|
||||
const stateSetter = jest.spyOn(existingVoiceConnection, 'state', 'set');
|
||||
|
||||
// @ts-expect-error: We're testing
|
||||
DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') =>
|
||||
guildId === existingJoinConfig.guildId && group === existingJoinConfig.group ? existingVoiceConnection : null,
|
||||
);
|
||||
@@ -167,6 +168,7 @@ describe('createVoiceConnection', () => {
|
||||
|
||||
const rejoinSpy = jest.spyOn(existingVoiceConnection, 'rejoin');
|
||||
|
||||
// @ts-expect-error: We're testing
|
||||
DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') =>
|
||||
guildId === existingJoinConfig.guildId && group === existingJoinConfig.group ? existingVoiceConnection : null,
|
||||
);
|
||||
@@ -198,6 +200,7 @@ describe('createVoiceConnection', () => {
|
||||
adapterCreator: existingAdapter.creator,
|
||||
});
|
||||
|
||||
// @ts-expect-error: We're testing
|
||||
DataStore.getVoiceConnection.mockImplementation((guildId, group = 'default') =>
|
||||
guildId === existingJoinConfig.guildId && group === existingJoinConfig.group ? existingVoiceConnection : null,
|
||||
);
|
||||
@@ -355,17 +358,17 @@ describe('VoiceConnection#onNetworkingClose', () => {
|
||||
voiceConnection.state = {
|
||||
status: VoiceConnectionStatus.Destroyed,
|
||||
};
|
||||
voiceConnection['onNetworkingClose'](1000);
|
||||
voiceConnection['onNetworkingClose'](1_000);
|
||||
expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed);
|
||||
expect(adapter.sendPayload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Disconnects for code 4014', () => {
|
||||
const { voiceConnection, adapter } = createFakeVoiceConnection();
|
||||
voiceConnection['onNetworkingClose'](4014);
|
||||
voiceConnection['onNetworkingClose'](4_014);
|
||||
expect(voiceConnection.state).toMatchObject({
|
||||
status: VoiceConnectionStatus.Disconnected,
|
||||
closeCode: 4014,
|
||||
closeCode: 4_014,
|
||||
});
|
||||
expect(adapter.sendPayload).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -376,7 +379,7 @@ describe('VoiceConnection#onNetworkingClose', () => {
|
||||
DataStore.createJoinVoiceChannelPayload.mockImplementation((config) =>
|
||||
config === joinConfig ? dummyPayload : undefined,
|
||||
);
|
||||
voiceConnection['onNetworkingClose'](1234);
|
||||
voiceConnection['onNetworkingClose'](1_234);
|
||||
expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Signalling);
|
||||
expect(adapter.sendPayload).toHaveBeenCalledWith(dummyPayload);
|
||||
expect(voiceConnection.rejoinAttempts).toEqual(1);
|
||||
@@ -389,7 +392,7 @@ describe('VoiceConnection#onNetworkingClose', () => {
|
||||
config === joinConfig ? dummyPayload : undefined,
|
||||
);
|
||||
adapter.sendPayload.mockReturnValue(false);
|
||||
voiceConnection['onNetworkingClose'](1234);
|
||||
voiceConnection['onNetworkingClose'](1_234);
|
||||
expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Disconnected);
|
||||
expect(adapter.sendPayload).toHaveBeenCalledWith(dummyPayload);
|
||||
expect(voiceConnection.rejoinAttempts).toEqual(1);
|
||||
@@ -552,7 +555,7 @@ describe('VoiceConnection#rejoin', () => {
|
||||
...(voiceConnection.state as VoiceConnectionSignallingState),
|
||||
status: VoiceConnectionStatus.Disconnected,
|
||||
reason: VoiceConnectionDisconnectReason.WebSocketClose,
|
||||
closeCode: 1000,
|
||||
closeCode: 1_000,
|
||||
};
|
||||
expect(voiceConnection.rejoin()).toEqual(true);
|
||||
expect(voiceConnection.rejoinAttempts).toEqual(1);
|
||||
@@ -584,7 +587,7 @@ describe('VoiceConnection#rejoin', () => {
|
||||
...(voiceConnection.state as VoiceConnectionSignallingState),
|
||||
status: VoiceConnectionStatus.Disconnected,
|
||||
reason: VoiceConnectionDisconnectReason.WebSocketClose,
|
||||
closeCode: 1000,
|
||||
closeCode: 1_000,
|
||||
};
|
||||
adapter.sendPayload.mockReturnValue(false);
|
||||
expect(voiceConnection.rejoin()).toEqual(false);
|
||||
@@ -751,7 +754,7 @@ describe('Adapter', () => {
|
||||
const { adapter, voiceConnection } = createFakeVoiceConnection();
|
||||
voiceConnection['addServerPacket'] = jest.fn();
|
||||
const dummy = Symbol('dummy') as any;
|
||||
adapter.libMethods.onVoiceServerUpdate(dummy);
|
||||
adapter.libMethods.onVoiceServerUpdate!(dummy);
|
||||
expect(voiceConnection['addServerPacket']).toHaveBeenCalledWith(dummy);
|
||||
});
|
||||
|
||||
@@ -759,13 +762,13 @@ describe('Adapter', () => {
|
||||
const { adapter, voiceConnection } = createFakeVoiceConnection();
|
||||
voiceConnection['addStatePacket'] = jest.fn();
|
||||
const dummy = Symbol('dummy') as any;
|
||||
adapter.libMethods.onVoiceStateUpdate(dummy);
|
||||
adapter.libMethods.onVoiceStateUpdate!(dummy);
|
||||
expect(voiceConnection['addStatePacket']).toHaveBeenCalledWith(dummy);
|
||||
});
|
||||
|
||||
test('destroy', () => {
|
||||
const { adapter, voiceConnection } = createFakeVoiceConnection();
|
||||
adapter.libMethods.destroy();
|
||||
adapter.libMethods.destroy!();
|
||||
expect(voiceConnection.state.status).toEqual(VoiceConnectionStatus.Destroyed);
|
||||
expect(adapter.sendPayload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/* eslint-disable id-length */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/dot-notation */
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { once } from 'node:events';
|
||||
import process from 'node:process';
|
||||
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
|
||||
import { RTP_PACKET_DESKTOP, RTP_PACKET_CHROME, RTP_PACKET_ANDROID } from '../__mocks__/rtp';
|
||||
import { VoiceConnection as _VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection';
|
||||
@@ -16,7 +19,8 @@ openSpy.mockImplementation((buffer) => buffer);
|
||||
|
||||
const VoiceConnection = _VoiceConnection as unknown as jest.Mocked<typeof _VoiceConnection>;
|
||||
|
||||
function nextTick() {
|
||||
async function nextTick() {
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
return new Promise((resolve) => process.nextTick(resolve));
|
||||
}
|
||||
|
||||
@@ -178,7 +182,7 @@ describe('VoiceReceiver', () => {
|
||||
|
||||
// Assert
|
||||
expect(nonce.equals(range(29, 32))).toEqual(true);
|
||||
expect(decrypted.equals(range(13, 28))).toEqual(true);
|
||||
expect(decrypted!.equals(range(13, 28))).toEqual(true);
|
||||
});
|
||||
|
||||
test('decrypt: xsalsa20_poly1305_suffix', () => {
|
||||
@@ -191,7 +195,7 @@ describe('VoiceReceiver', () => {
|
||||
|
||||
// Assert
|
||||
expect(nonce.equals(range(41, 64))).toEqual(true);
|
||||
expect(decrypted.equals(range(13, 40))).toEqual(true);
|
||||
expect(decrypted!.equals(range(13, 40))).toEqual(true);
|
||||
});
|
||||
|
||||
test('decrypt: xsalsa20_poly1305', () => {
|
||||
@@ -204,7 +208,7 @@ describe('VoiceReceiver', () => {
|
||||
|
||||
// Assert
|
||||
expect(nonce.equals(range(1, 12))).toEqual(true);
|
||||
expect(decrypted.equals(range(13, 64))).toEqual(true);
|
||||
expect(decrypted!.equals(range(13, 64))).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createSocket as _createSocket } from 'node:dgram';
|
||||
import EventEmitter, { once } from 'node:events';
|
||||
import { VoiceUDPSocket } from '../src/networking/VoiceUDPSocket';
|
||||
@@ -16,6 +17,7 @@ beforeEach(() => {
|
||||
|
||||
class FakeSocket extends EventEmitter {
|
||||
public send(buffer: Buffer, port: number, address: string) {}
|
||||
|
||||
public close() {
|
||||
this.emit('close');
|
||||
}
|
||||
@@ -29,7 +31,7 @@ const VALID_RESPONSE = Buffer.from([
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd3, 0x84,
|
||||
]);
|
||||
|
||||
function wait() {
|
||||
async function wait() {
|
||||
return new Promise((resolve) => {
|
||||
setImmediate(resolve);
|
||||
jest.advanceTimersToNextTimer();
|
||||
@@ -52,13 +54,13 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
|
||||
fake.emit('message', VALID_RESPONSE);
|
||||
});
|
||||
createSocket.mockImplementation((type) => fake as any);
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 });
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 });
|
||||
|
||||
expect(createSocket).toHaveBeenCalledWith('udp4');
|
||||
expect(fake.listenerCount('message')).toEqual(1);
|
||||
await expect(socket.performIPDiscovery(1234)).resolves.toEqual({
|
||||
await expect(socket.performIPDiscovery(1_234)).resolves.toEqual({
|
||||
ip: '91.90.123.93',
|
||||
port: 54148,
|
||||
port: 54_148,
|
||||
});
|
||||
// Ensure clean up occurs
|
||||
expect(fake.listenerCount('message')).toEqual(1);
|
||||
@@ -77,13 +79,13 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
|
||||
fake.emit('message', VALID_RESPONSE);
|
||||
});
|
||||
createSocket.mockImplementation(() => fake as any);
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 });
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 });
|
||||
|
||||
expect(createSocket).toHaveBeenCalledWith('udp4');
|
||||
expect(fake.listenerCount('message')).toEqual(1);
|
||||
await expect(socket.performIPDiscovery(1234)).resolves.toEqual({
|
||||
await expect(socket.performIPDiscovery(1_234)).resolves.toEqual({
|
||||
ip: '91.90.123.93',
|
||||
port: 54148,
|
||||
port: 54_148,
|
||||
});
|
||||
// Ensure clean up occurs
|
||||
expect(fake.listenerCount('message')).toEqual(1);
|
||||
@@ -96,10 +98,10 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
|
||||
fake.close();
|
||||
});
|
||||
createSocket.mockImplementation(() => fake as any);
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 });
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 });
|
||||
|
||||
expect(createSocket).toHaveBeenCalledWith('udp4');
|
||||
await expect(socket.performIPDiscovery(1234)).rejects.toThrowError();
|
||||
await expect(socket.performIPDiscovery(1_234)).rejects.toThrowError();
|
||||
});
|
||||
|
||||
test('Stays alive when messages are echoed back', async () => {
|
||||
@@ -109,13 +111,12 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
|
||||
fake.emit('message', buffer);
|
||||
});
|
||||
createSocket.mockImplementation(() => fake as any);
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 });
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 });
|
||||
|
||||
let closed = false;
|
||||
// @ts-expect-error
|
||||
socket.on('close', () => (closed = true));
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
for (let index = 0; index < 30; index++) {
|
||||
jest.advanceTimersToNextTimer();
|
||||
await wait();
|
||||
}
|
||||
@@ -127,13 +128,12 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
|
||||
const fake = new FakeSocket();
|
||||
fake.send = jest.fn();
|
||||
createSocket.mockImplementation(() => fake as any);
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 });
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 });
|
||||
|
||||
let closed = false;
|
||||
// @ts-expect-error
|
||||
socket.on('close', () => (closed = true));
|
||||
|
||||
for (let i = 0; i < 15; i++) {
|
||||
for (let index = 0; index < 15; index++) {
|
||||
jest.advanceTimersToNextTimer();
|
||||
await wait();
|
||||
}
|
||||
@@ -146,25 +146,27 @@ describe('VoiceUDPSocket#performIPDiscovery', () => {
|
||||
const fakeSend = jest.fn();
|
||||
fake.send = fakeSend;
|
||||
createSocket.mockImplementation(() => fake as any);
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25565 });
|
||||
socket = new VoiceUDPSocket({ ip: '1.2.3.4', port: 25_565 });
|
||||
|
||||
let closed = false;
|
||||
// @ts-expect-error
|
||||
|
||||
socket.on('close', () => (closed = true));
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let index = 0; index < 10; index++) {
|
||||
jest.advanceTimersToNextTimer();
|
||||
await wait();
|
||||
}
|
||||
|
||||
fakeSend.mockImplementation(async (buffer: Buffer) => {
|
||||
await wait();
|
||||
fake.emit('message', buffer);
|
||||
});
|
||||
expect(closed).toEqual(false);
|
||||
for (let i = 0; i < 30; i++) {
|
||||
for (let index = 0; index < 30; index++) {
|
||||
jest.advanceTimersToNextTimer();
|
||||
await wait();
|
||||
}
|
||||
|
||||
expect(closed).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import EventEmitter, { once } from 'node:events';
|
||||
import { type EventEmitter, once } from 'node:events';
|
||||
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
|
||||
import WS from 'jest-websocket-mock';
|
||||
import { VoiceWebSocket } from '../src/networking/VoiceWebSocket';
|
||||
@@ -9,13 +9,13 @@ beforeEach(() => {
|
||||
WS.clean();
|
||||
});
|
||||
|
||||
function onceIgnoreError<T extends EventEmitter>(target: T, event: string) {
|
||||
async function onceIgnoreError<T extends EventEmitter>(target: T, event: string) {
|
||||
return new Promise((resolve) => {
|
||||
target.on(event, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function onceOrThrow<T extends EventEmitter>(target: T, event: string, after: number) {
|
||||
async function onceOrThrow<T extends EventEmitter>(target: T, event: string, after: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
target.on(event, resolve);
|
||||
setTimeout(() => reject(new Error('Time up')), after);
|
||||
@@ -46,7 +46,7 @@ describe.skip('VoiceWebSocket: packet parsing', () => {
|
||||
server.send('asdf');
|
||||
await expect(rcv).rejects.toThrowError();
|
||||
|
||||
const dummy = { op: 1234 };
|
||||
const dummy = { op: 1_234 };
|
||||
rcv = once(ws, 'packet');
|
||||
server.send(JSON.stringify(dummy));
|
||||
await expect(rcv).resolves.toEqual([dummy]);
|
||||
@@ -94,17 +94,19 @@ describe.skip('VoiceWebSocket: heartbeating', () => {
|
||||
await server.connected;
|
||||
const rcv = onceOrThrow(ws, 'close', 750);
|
||||
ws.setHeartbeatInterval(50);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
for (let index = 0; index < 10; index++) {
|
||||
const packet: any = await server.nextMessage;
|
||||
expect(packet).toMatchObject({
|
||||
op: VoiceOpcodes.Heartbeat,
|
||||
});
|
||||
server.send({
|
||||
op: VoiceOpcodes.HeartbeatAck,
|
||||
// eslint-disable-next-line id-length
|
||||
d: packet.d,
|
||||
});
|
||||
expect(ws.ping).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
|
||||
ws.setHeartbeatInterval(-1);
|
||||
await expect(rcv).rejects.toThrowError();
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { Buffer } from 'node:buffer';
|
||||
import EventEmitter, { once } from 'node:events';
|
||||
import process from 'node:process';
|
||||
import { Readable } from 'node:stream';
|
||||
import { opus as _opus } from 'prism-media';
|
||||
import { StreamType } from '../src/audio';
|
||||
import { StreamType } from '../src/audio/index';
|
||||
import { demuxProbe } from '../src/util/demuxProbe';
|
||||
|
||||
jest.mock('prism-media');
|
||||
@@ -12,19 +14,21 @@ jest.mock('prism-media');
|
||||
const WebmDemuxer = _opus.WebmDemuxer as unknown as jest.Mock<_opus.WebmDemuxer>;
|
||||
const OggDemuxer = _opus.OggDemuxer as unknown as jest.Mock<_opus.OggDemuxer>;
|
||||
|
||||
function nextTick() {
|
||||
async function nextTick() {
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
return new Promise((resolve) => process.nextTick(resolve));
|
||||
}
|
||||
|
||||
async function* gen(n: number) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
yield Buffer.from([i]);
|
||||
async function* gen(num: number) {
|
||||
for (let index = 0; index < num; index++) {
|
||||
yield Buffer.from([index]);
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
|
||||
function range(n: number) {
|
||||
return Buffer.from(Array.from(Array(n).keys()));
|
||||
function range(num: number) {
|
||||
// eslint-disable-next-line unicorn/no-new-array
|
||||
return Buffer.from(Array.from(new Array(num).keys()));
|
||||
}
|
||||
|
||||
const validHead = Buffer.from([
|
||||
@@ -41,6 +45,7 @@ async function collectStream(stream: Readable): Promise<Buffer> {
|
||||
for await (const data of stream) {
|
||||
output = Buffer.concat([output, data]);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -107,10 +112,10 @@ describe('demuxProbe', () => {
|
||||
});
|
||||
|
||||
test('Gives up on larger streams', async () => {
|
||||
const stream = Readable.from(gen(8192), { objectMode: false });
|
||||
const stream = Readable.from(gen(8_192), { objectMode: false });
|
||||
const probe = await demuxProbe(stream);
|
||||
expect(probe.type).toEqual(StreamType.Arbitrary);
|
||||
await expect(collectStream(probe.stream)).resolves.toEqual(range(8192));
|
||||
await expect(collectStream(probe.stream)).resolves.toEqual(range(8_192));
|
||||
});
|
||||
|
||||
test('Propagates errors', async () => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import EventEmitter from 'node:events';
|
||||
import { VoiceConnection, VoiceConnectionStatus } from '../src/VoiceConnection';
|
||||
import process from 'node:process';
|
||||
import { VoiceConnectionStatus, type VoiceConnection } from '../src/VoiceConnection';
|
||||
import { entersState } from '../src/util/entersState';
|
||||
|
||||
function createFakeVoiceConnection(status = VoiceConnectionStatus.Signalling) {
|
||||
@@ -20,13 +21,13 @@ describe('entersState', () => {
|
||||
const vc = createFakeVoiceConnection();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
process.nextTick(() => vc.emit(VoiceConnectionStatus.Ready, null as any, null as any));
|
||||
const result = await entersState(vc, VoiceConnectionStatus.Ready, 1000);
|
||||
const result = await entersState(vc, VoiceConnectionStatus.Ready, 1_000);
|
||||
expect(result).toEqual(vc);
|
||||
});
|
||||
|
||||
test('Rejects once the timeout is exceeded', async () => {
|
||||
const vc = createFakeVoiceConnection();
|
||||
const promise = entersState(vc, VoiceConnectionStatus.Ready, 1000);
|
||||
const promise = entersState(vc, VoiceConnectionStatus.Ready, 1_000);
|
||||
jest.runAllTimers();
|
||||
await expect(promise).rejects.toThrowError();
|
||||
});
|
||||
@@ -51,6 +52,6 @@ describe('entersState', () => {
|
||||
|
||||
test('Resolves immediately when target already in desired state', async () => {
|
||||
const vc = createFakeVoiceConnection();
|
||||
await expect(entersState(vc, VoiceConnectionStatus.Signalling, 1000)).resolves.toEqual(vc);
|
||||
await expect(entersState(vc, VoiceConnectionStatus.Signalling, 1_000)).resolves.toEqual(vc);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,15 +67,9 @@
|
||||
"@microsoft/api-extractor": "^7.29.5",
|
||||
"@types/jest": "^28.1.8",
|
||||
"@types/node": "^16.11.56",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
||||
"@typescript-eslint/parser": "^5.36.1",
|
||||
"downlevel-dts": "^0.10.1",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-marine": "^9.4.1",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-tsdoc": "^0.2.16",
|
||||
"eslint-config-neon": "^0.1.23",
|
||||
"jest": "^29.0.1",
|
||||
"jest-websocket-mock": "^2.4.0",
|
||||
"mock-socket": "^9.1.5",
|
||||
|
||||
@@ -3,11 +3,11 @@ import type { VoiceConnection } from './VoiceConnection';
|
||||
import type { AudioPlayer } from './audio';
|
||||
|
||||
export interface JoinConfig {
|
||||
guildId: string;
|
||||
channelId: string | null;
|
||||
group: string;
|
||||
guildId: string;
|
||||
selfDeaf: boolean;
|
||||
selfMute: boolean;
|
||||
group: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,6 +19,7 @@ export interface JoinConfig {
|
||||
export function createJoinVoiceChannelPayload(config: JoinConfig) {
|
||||
return {
|
||||
op: GatewayOpcodes.VoiceStateUpdate,
|
||||
// eslint-disable-next-line id-length
|
||||
d: {
|
||||
guild_id: config.guildId,
|
||||
channel_id: config.channelId,
|
||||
@@ -54,7 +55,6 @@ export function getGroups() {
|
||||
* Retrieves all the voice connections under the 'default' group.
|
||||
*
|
||||
* @param group - The group to look up
|
||||
*
|
||||
* @returns The map of voice connections
|
||||
*/
|
||||
export function getVoiceConnections(group?: 'default'): Map<string, VoiceConnection>;
|
||||
@@ -63,7 +63,6 @@ export function getVoiceConnections(group?: 'default'): Map<string, VoiceConnect
|
||||
* Retrieves all the voice connections under the given group name.
|
||||
*
|
||||
* @param group - The group to look up
|
||||
*
|
||||
* @returns The map of voice connections
|
||||
*/
|
||||
export function getVoiceConnections(group: string): Map<string, VoiceConnection> | undefined;
|
||||
@@ -72,7 +71,6 @@ export function getVoiceConnections(group: string): Map<string, VoiceConnection>
|
||||
* Retrieves all the voice connections under the given group name. Defaults to the 'default' group.
|
||||
*
|
||||
* @param group - The group to look up
|
||||
*
|
||||
* @returns The map of voice connections
|
||||
*/
|
||||
export function getVoiceConnections(group = 'default') {
|
||||
@@ -84,7 +82,6 @@ export function getVoiceConnections(group = 'default') {
|
||||
*
|
||||
* @param guildId - The guild id of the voice connection
|
||||
* @param group - the group that the voice connection was registered with
|
||||
*
|
||||
* @returns The voice connection, if it exists
|
||||
*/
|
||||
export function getVoiceConnection(guildId: string, group = 'default') {
|
||||
@@ -122,8 +119,10 @@ function audioCycleStep() {
|
||||
nextTime += FRAME_LENGTH;
|
||||
const available = audioPlayers.filter((player) => player.checkPlayable());
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation
|
||||
available.forEach((player) => player['_stepDispatch']());
|
||||
for (const player of available) {
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation
|
||||
player['_stepDispatch']();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
prepareNextAudioFrame(available);
|
||||
@@ -140,6 +139,7 @@ function prepareNextAudioFrame(players: AudioPlayer[]) {
|
||||
if (nextTime !== -1) {
|
||||
audioCycleInterval = setTimeout(() => audioCycleStep(), nextTime - Date.now());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,6 @@ function prepareNextAudioFrame(players: AudioPlayer[]) {
|
||||
* Checks whether or not the given audio player is being driven by the data store clock.
|
||||
*
|
||||
* @param target - The target to test for
|
||||
*
|
||||
* @returns `true` if it is being tracked, `false` otherwise
|
||||
*/
|
||||
export function hasAudioPlayer(target: AudioPlayer) {
|
||||
@@ -173,6 +172,7 @@ export function addAudioPlayer(player: AudioPlayer) {
|
||||
nextTime = Date.now();
|
||||
setImmediate(() => audioCycleStep());
|
||||
}
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
/* eslint-disable consistent-return */
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||
import type { Buffer } from 'node:buffer';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispatchData } from 'discord-api-types/v10';
|
||||
import type { CreateVoiceConnectionOptions } from '.';
|
||||
import type { JoinConfig } from './DataStore';
|
||||
import {
|
||||
getVoiceConnection,
|
||||
createJoinVoiceChannelPayload,
|
||||
trackVoiceConnection,
|
||||
JoinConfig,
|
||||
untrackVoiceConnection,
|
||||
} from './DataStore';
|
||||
import type { AudioPlayer } from './audio/AudioPlayer';
|
||||
import type { PlayerSubscription } from './audio/PlayerSubscription';
|
||||
import type { VoiceWebSocket, VoiceUDPSocket } from './networking';
|
||||
import { Networking, NetworkingState, NetworkingStatusCode } from './networking/Networking';
|
||||
import { VoiceReceiver } from './receive';
|
||||
import { Networking, NetworkingStatusCode, type NetworkingState } from './networking/Networking';
|
||||
import { VoiceReceiver } from './receive/index';
|
||||
import type { DiscordGatewayAdapterImplementerMethods } from './util/adapter';
|
||||
import { noop } from './util/util';
|
||||
import type { CreateVoiceConnectionOptions } from './index';
|
||||
|
||||
/**
|
||||
* The various status codes a voice connection can hold at any one time.
|
||||
*/
|
||||
export enum VoiceConnectionStatus {
|
||||
/**
|
||||
* Sending a packet to the main Discord gateway to indicate we want to change our voice state.
|
||||
*/
|
||||
Signalling = 'signalling',
|
||||
|
||||
/**
|
||||
* The `VOICE_SERVER_UPDATE` and `VOICE_STATE_UPDATE` packets have been received, now attempting to establish a voice connection.
|
||||
*/
|
||||
Connecting = 'connecting',
|
||||
|
||||
/**
|
||||
* A voice connection has been established, and is ready to be used.
|
||||
* The voice connection has been destroyed and untracked, it cannot be reused.
|
||||
*/
|
||||
Ready = 'ready',
|
||||
Destroyed = 'destroyed',
|
||||
|
||||
/**
|
||||
* The voice connection has either been severed or not established.
|
||||
@@ -42,9 +41,14 @@ export enum VoiceConnectionStatus {
|
||||
Disconnected = 'disconnected',
|
||||
|
||||
/**
|
||||
* The voice connection has been destroyed and untracked, it cannot be reused.
|
||||
* A voice connection has been established, and is ready to be used.
|
||||
*/
|
||||
Destroyed = 'destroyed',
|
||||
Ready = 'ready',
|
||||
|
||||
/**
|
||||
* Sending a packet to the main Discord gateway to indicate we want to change our voice state.
|
||||
*/
|
||||
Signalling = 'signalling',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,9 +56,9 @@ export enum VoiceConnectionStatus {
|
||||
* VOICE_STATE_UPDATE packet from Discord, provided by the adapter.
|
||||
*/
|
||||
export interface VoiceConnectionSignallingState {
|
||||
adapter: DiscordGatewayAdapterImplementerMethods;
|
||||
status: VoiceConnectionStatus.Signalling;
|
||||
subscription?: PlayerSubscription | undefined;
|
||||
adapter: DiscordGatewayAdapterImplementerMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,9 +91,9 @@ export enum VoiceConnectionDisconnectReason {
|
||||
* it attempting to connect. You can manually attempt to reconnect using VoiceConnection#reconnect.
|
||||
*/
|
||||
export interface VoiceConnectionDisconnectedBaseState {
|
||||
adapter: DiscordGatewayAdapterImplementerMethods;
|
||||
status: VoiceConnectionStatus.Disconnected;
|
||||
subscription?: PlayerSubscription | undefined;
|
||||
adapter: DiscordGatewayAdapterImplementerMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,12 +109,12 @@ export interface VoiceConnectionDisconnectedOtherState extends VoiceConnectionDi
|
||||
* You can manually attempt to reconnect using VoiceConnection#reconnect.
|
||||
*/
|
||||
export interface VoiceConnectionDisconnectedWebSocketState extends VoiceConnectionDisconnectedBaseState {
|
||||
reason: VoiceConnectionDisconnectReason.WebSocketClose;
|
||||
|
||||
/**
|
||||
* The close code of the WebSocket connection to the Discord voice server.
|
||||
*/
|
||||
closeCode: number;
|
||||
|
||||
reason: VoiceConnectionDisconnectReason.WebSocketClose;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,10 +130,10 @@ export type VoiceConnectionDisconnectedState =
|
||||
* voice server.
|
||||
*/
|
||||
export interface VoiceConnectionConnectingState {
|
||||
status: VoiceConnectionStatus.Connecting;
|
||||
networking: Networking;
|
||||
subscription?: PlayerSubscription | undefined;
|
||||
adapter: DiscordGatewayAdapterImplementerMethods;
|
||||
networking: Networking;
|
||||
status: VoiceConnectionStatus.Connecting;
|
||||
subscription?: PlayerSubscription | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,10 +141,10 @@ export interface VoiceConnectionConnectingState {
|
||||
* voice server.
|
||||
*/
|
||||
export interface VoiceConnectionReadyState {
|
||||
status: VoiceConnectionStatus.Ready;
|
||||
networking: Networking;
|
||||
subscription?: PlayerSubscription | undefined;
|
||||
adapter: DiscordGatewayAdapterImplementerMethods;
|
||||
networking: Networking;
|
||||
status: VoiceConnectionStatus.Ready;
|
||||
subscription?: PlayerSubscription | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,30 +160,34 @@ export interface VoiceConnectionDestroyedState {
|
||||
* The various states that a voice connection can be in.
|
||||
*/
|
||||
export type VoiceConnectionState =
|
||||
| VoiceConnectionSignallingState
|
||||
| VoiceConnectionDisconnectedState
|
||||
| VoiceConnectionConnectingState
|
||||
| VoiceConnectionDestroyedState
|
||||
| VoiceConnectionDisconnectedState
|
||||
| VoiceConnectionReadyState
|
||||
| VoiceConnectionDestroyedState;
|
||||
| VoiceConnectionSignallingState;
|
||||
|
||||
export interface VoiceConnection extends EventEmitter {
|
||||
/**
|
||||
* Emitted when there is an error emitted from the voice connection
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on(event: 'error', listener: (error: Error) => void): this;
|
||||
/**
|
||||
* Emitted debugging information about the voice connection
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on(event: 'debug', listener: (message: string) => void): this;
|
||||
/**
|
||||
* Emitted when the state of the voice connection changes
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on(event: 'stateChange', listener: (oldState: VoiceConnectionState, newState: VoiceConnectionState) => void): this;
|
||||
/**
|
||||
* Emitted when the state of the voice connection changes to a specific status
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on<T extends VoiceConnectionStatus>(
|
||||
@@ -228,7 +236,7 @@ export class VoiceConnection extends EventEmitter {
|
||||
/**
|
||||
* The debug logger function, if debugging is enabled.
|
||||
*/
|
||||
private readonly debug: null | ((message: string) => void);
|
||||
private readonly debug: ((message: string) => void) | null;
|
||||
|
||||
/**
|
||||
* Creates a new voice connection.
|
||||
@@ -292,6 +300,7 @@ export class VoiceConnection extends EventEmitter {
|
||||
oldNetworking.off('stateChange', this.onNetworkingStateChange);
|
||||
oldNetworking.destroy();
|
||||
}
|
||||
|
||||
if (newNetworking) this.updateReceiveBindings(newNetworking.state, oldNetworking?.state);
|
||||
}
|
||||
|
||||
@@ -362,6 +371,7 @@ export class VoiceConnection extends EventEmitter {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -432,13 +442,12 @@ export class VoiceConnection extends EventEmitter {
|
||||
* 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
|
||||
*/
|
||||
private onNetworkingClose(code: number) {
|
||||
if (this.state.status === VoiceConnectionStatus.Destroyed) return;
|
||||
// If networking closes, try to connect to the voice channel again.
|
||||
if (code === 4014) {
|
||||
if (code === 4_014) {
|
||||
// Disconnected - networking is already destroyed here
|
||||
this.state = {
|
||||
...this.state,
|
||||
@@ -548,12 +557,15 @@ export class VoiceConnection extends EventEmitter {
|
||||
if (this.state.status === VoiceConnectionStatus.Destroyed) {
|
||||
throw new Error('Cannot destroy VoiceConnection - it has already been destroyed');
|
||||
}
|
||||
|
||||
if (getVoiceConnection(this.joinConfig.guildId, this.joinConfig.group) === this) {
|
||||
untrackVoiceConnection(this);
|
||||
}
|
||||
|
||||
if (adapterAvailable) {
|
||||
this.state.adapter.sendPayload(createJoinVoiceChannelPayload({ ...this.joinConfig, channelId: null }));
|
||||
}
|
||||
|
||||
this.state = {
|
||||
status: VoiceConnectionStatus.Destroyed,
|
||||
};
|
||||
@@ -571,6 +583,7 @@ export class VoiceConnection extends EventEmitter {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.joinConfig.channelId = null;
|
||||
if (!this.state.adapter.sendPayload(createJoinVoiceChannelPayload(this.joinConfig))) {
|
||||
this.state = {
|
||||
@@ -581,6 +594,7 @@ export class VoiceConnection extends EventEmitter {
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
adapter: this.state.adapter,
|
||||
reason: VoiceConnectionDisconnectReason.Manual,
|
||||
@@ -599,7 +613,7 @@ export class VoiceConnection extends EventEmitter {
|
||||
*
|
||||
* A state transition from Disconnected to Signalling will be observed when this is called.
|
||||
*/
|
||||
public rejoin(joinConfig?: Omit<JoinConfig, 'guildId' | 'group'>) {
|
||||
public rejoin(joinConfig?: Omit<JoinConfig, 'group' | 'guildId'>) {
|
||||
if (this.state.status === VoiceConnectionStatus.Destroyed) {
|
||||
return false;
|
||||
}
|
||||
@@ -615,6 +629,7 @@ export class VoiceConnection extends EventEmitter {
|
||||
status: VoiceConnectionStatus.Signalling,
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -635,6 +650,7 @@ export class VoiceConnection extends EventEmitter {
|
||||
*/
|
||||
public setSpeaking(enabled: boolean) {
|
||||
if (this.state.status !== VoiceConnectionStatus.Ready) return false;
|
||||
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
|
||||
return this.state.networking.setSpeaking(enabled);
|
||||
}
|
||||
|
||||
@@ -642,7 +658,6 @@ export class VoiceConnection extends EventEmitter {
|
||||
* 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
|
||||
*/
|
||||
public subscribe(player: AudioPlayer) {
|
||||
@@ -677,6 +692,7 @@ export class VoiceConnection extends EventEmitter {
|
||||
udp: this.state.networking.state.udp.ping,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ws: undefined,
|
||||
udp: undefined,
|
||||
@@ -721,19 +737,22 @@ export function createVoiceConnection(joinConfig: JoinConfig, options: CreateVoi
|
||||
reason: VoiceConnectionDisconnectReason.AdapterUnavailable,
|
||||
};
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
const voiceConnection = new VoiceConnection(joinConfig, options);
|
||||
trackVoiceConnection(voiceConnection);
|
||||
if (voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) {
|
||||
if (!voiceConnection.state.adapter.sendPayload(payload)) {
|
||||
voiceConnection.state = {
|
||||
...voiceConnection.state,
|
||||
status: VoiceConnectionStatus.Disconnected,
|
||||
reason: VoiceConnectionDisconnectReason.AdapterUnavailable,
|
||||
};
|
||||
}
|
||||
if (
|
||||
voiceConnection.state.status !== VoiceConnectionStatus.Destroyed &&
|
||||
!voiceConnection.state.adapter.sendPayload(payload)
|
||||
) {
|
||||
voiceConnection.state = {
|
||||
...voiceConnection.state,
|
||||
status: VoiceConnectionStatus.Disconnected,
|
||||
reason: VoiceConnectionDisconnectReason.AdapterUnavailable,
|
||||
};
|
||||
}
|
||||
|
||||
return voiceConnection;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/prefer-ts-expect-error, @typescript-eslint/method-signature-style */
|
||||
import { Buffer } from 'node:buffer';
|
||||
import EventEmitter from 'node:events';
|
||||
import { addAudioPlayer, deleteAudioPlayer } from '../DataStore';
|
||||
import { VoiceConnectionStatus, type VoiceConnection } from '../VoiceConnection';
|
||||
import { noop } from '../util/util';
|
||||
import { AudioPlayerError } from './AudioPlayerError';
|
||||
import type { AudioResource } from './AudioResource';
|
||||
import { PlayerSubscription } from './PlayerSubscription';
|
||||
import { addAudioPlayer, deleteAudioPlayer } from '../DataStore';
|
||||
import { VoiceConnection, VoiceConnectionStatus } from '../VoiceConnection';
|
||||
import { noop } from '../util/util';
|
||||
|
||||
// The Opus "silent" frame
|
||||
export const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]);
|
||||
@@ -33,15 +34,20 @@ export enum NoSubscriberBehavior {
|
||||
|
||||
export enum AudioPlayerStatus {
|
||||
/**
|
||||
* When there is currently no resource for the player to be playing.
|
||||
* When the player has paused itself. Only possible with the "pause" no subscriber behavior.
|
||||
*/
|
||||
Idle = 'idle',
|
||||
AutoPaused = 'autopaused',
|
||||
|
||||
/**
|
||||
* When the player is waiting for an audio resource to become readable before transitioning to Playing.
|
||||
*/
|
||||
Buffering = 'buffering',
|
||||
|
||||
/**
|
||||
* When there is currently no resource for the player to be playing.
|
||||
*/
|
||||
Idle = 'idle',
|
||||
|
||||
/**
|
||||
* When the player has been manually paused.
|
||||
*/
|
||||
@@ -51,22 +57,17 @@ export enum AudioPlayerStatus {
|
||||
* When the player is actively playing an audio resource.
|
||||
*/
|
||||
Playing = 'playing',
|
||||
|
||||
/**
|
||||
* When the player has paused itself. Only possible with the "pause" no subscriber behavior.
|
||||
*/
|
||||
AutoPaused = 'autopaused',
|
||||
}
|
||||
|
||||
/**
|
||||
* Options that can be passed when creating an audio player, used to specify its behavior.
|
||||
*/
|
||||
export interface CreateAudioPlayerOptions {
|
||||
debug?: boolean;
|
||||
behaviors?: {
|
||||
noSubscriber?: NoSubscriberBehavior;
|
||||
maxMissedFrames?: number;
|
||||
noSubscriber?: NoSubscriberBehavior;
|
||||
};
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,14 +83,14 @@ export interface AudioPlayerIdleState {
|
||||
* it will re-enter the Idle state.
|
||||
*/
|
||||
export interface AudioPlayerBufferingState {
|
||||
status: AudioPlayerStatus.Buffering;
|
||||
onFailureCallback: () => void;
|
||||
onReadableCallback: () => void;
|
||||
onStreamError: (error: Error) => void;
|
||||
/**
|
||||
* The resource that the AudioPlayer is waiting for
|
||||
*/
|
||||
resource: AudioResource;
|
||||
onReadableCallback: () => void;
|
||||
onFailureCallback: () => void;
|
||||
onStreamError: (error: Error) => void;
|
||||
status: AudioPlayerStatus.Buffering;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,11 +98,11 @@ export interface AudioPlayerBufferingState {
|
||||
* it will enter the Idle state.
|
||||
*/
|
||||
export interface AudioPlayerPlayingState {
|
||||
status: AudioPlayerStatus.Playing;
|
||||
/**
|
||||
* The number of consecutive times that the audio resource has been unable to provide an Opus frame.
|
||||
*/
|
||||
missedFrames: number;
|
||||
onStreamError: (error: Error) => void;
|
||||
|
||||
/**
|
||||
* The playback duration in milliseconds of the current audio resource. This includes filler silence packets
|
||||
@@ -114,7 +115,7 @@ export interface AudioPlayerPlayingState {
|
||||
*/
|
||||
resource: AudioResource;
|
||||
|
||||
onStreamError: (error: Error) => void;
|
||||
status: AudioPlayerStatus.Playing;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,12 +123,7 @@ export interface AudioPlayerPlayingState {
|
||||
* automatically by the AudioPlayer itself if there are no available subscribers.
|
||||
*/
|
||||
export interface AudioPlayerPausedState {
|
||||
status: AudioPlayerStatus.Paused | AudioPlayerStatus.AutoPaused;
|
||||
/**
|
||||
* How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing.
|
||||
*/
|
||||
silencePacketsRemaining: number;
|
||||
|
||||
onStreamError: (error: Error) => void;
|
||||
/**
|
||||
* The playback duration in milliseconds of the current audio resource. This includes filler silence packets
|
||||
* that have been played when the resource was buffering.
|
||||
@@ -139,41 +135,51 @@ export interface AudioPlayerPausedState {
|
||||
*/
|
||||
resource: AudioResource;
|
||||
|
||||
onStreamError: (error: Error) => void;
|
||||
/**
|
||||
* How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing.
|
||||
*/
|
||||
silencePacketsRemaining: number;
|
||||
|
||||
status: AudioPlayerStatus.AutoPaused | AudioPlayerStatus.Paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* The various states that the player can be in.
|
||||
*/
|
||||
export type AudioPlayerState =
|
||||
| AudioPlayerIdleState
|
||||
| AudioPlayerBufferingState
|
||||
| AudioPlayerPlayingState
|
||||
| AudioPlayerPausedState;
|
||||
| AudioPlayerIdleState
|
||||
| AudioPlayerPausedState
|
||||
| AudioPlayerPlayingState;
|
||||
|
||||
export interface AudioPlayer extends EventEmitter {
|
||||
/**
|
||||
* Emitted when there is an error emitted from the audio resource played by the audio player
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on(event: 'error', listener: (error: AudioPlayerError) => void): this;
|
||||
/**
|
||||
* Emitted debugging information about the audio player
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on(event: 'debug', listener: (message: string) => void): this;
|
||||
/**
|
||||
* Emitted when the state of the audio player changes
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on(event: 'stateChange', listener: (oldState: AudioPlayerState, newState: AudioPlayerState) => void): this;
|
||||
/**
|
||||
* Emitted when the audio player is subscribed to a voice connection
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on(event: 'subscribe' | 'unsubscribe', listener: (subscription: PlayerSubscription) => void): this;
|
||||
/**
|
||||
* Emitted when the status of state changes to a specific status
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on<T extends AudioPlayerStatus>(
|
||||
@@ -221,14 +227,14 @@ export class AudioPlayer extends EventEmitter {
|
||||
* The behavior that the player should follow when it enters certain situations.
|
||||
*/
|
||||
private readonly behaviors: {
|
||||
noSubscriber: NoSubscriberBehavior;
|
||||
maxMissedFrames: number;
|
||||
noSubscriber: NoSubscriberBehavior;
|
||||
};
|
||||
|
||||
/**
|
||||
* The debug logger function, if debugging is enabled.
|
||||
*/
|
||||
private readonly debug: null | ((message: string) => void);
|
||||
private readonly debug: ((message: string) => void) | null;
|
||||
|
||||
/**
|
||||
* Creates a new AudioPlayer.
|
||||
@@ -259,9 +265,7 @@ export class AudioPlayer extends EventEmitter {
|
||||
*
|
||||
* @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
|
||||
@@ -273,6 +277,7 @@ export class AudioPlayer extends EventEmitter {
|
||||
setImmediate(() => this.emit('subscribe', subscription));
|
||||
return subscription;
|
||||
}
|
||||
|
||||
return existingSubscription;
|
||||
}
|
||||
|
||||
@@ -281,9 +286,7 @@ export class AudioPlayer extends EventEmitter {
|
||||
*
|
||||
* @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
|
||||
@@ -295,6 +298,7 @@ export class AudioPlayer extends EventEmitter {
|
||||
subscription.connection.setSpeaking(false);
|
||||
this.emit('unsubscribe', subscription);
|
||||
}
|
||||
|
||||
return exists;
|
||||
}
|
||||
|
||||
@@ -355,6 +359,7 @@ export class AudioPlayer extends EventEmitter {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
this.emit(newState.status, oldState, this._state as any);
|
||||
}
|
||||
|
||||
this.debug?.(`state change:\nfrom ${stringifyState(oldState)}\nto ${stringifyState(newState)}`);
|
||||
}
|
||||
|
||||
@@ -368,9 +373,7 @@ export class AudioPlayer extends EventEmitter {
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
public play<T>(resource: AudioResource<T>) {
|
||||
@@ -382,8 +385,10 @@ export class AudioPlayer extends EventEmitter {
|
||||
if (resource.audioPlayer === this) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Resource is already being played by another audio player.');
|
||||
}
|
||||
|
||||
resource.audioPlayer = this;
|
||||
|
||||
// Attach error listeners to the stream that will propagate the error and then return to the Idle
|
||||
@@ -451,7 +456,6 @@ export class AudioPlayer extends EventEmitter {
|
||||
* 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`
|
||||
*/
|
||||
public pause(interpolateSilence = true) {
|
||||
@@ -484,7 +488,6 @@ export class AudioPlayer extends EventEmitter {
|
||||
* 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`
|
||||
*/
|
||||
public stop(force = false) {
|
||||
@@ -496,6 +499,7 @@ export class AudioPlayer extends EventEmitter {
|
||||
} else if (this.state.resource.silenceRemaining === -1) {
|
||||
this.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -515,6 +519,7 @@ export class AudioPlayer extends EventEmitter {
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -530,7 +535,9 @@ export class AudioPlayer extends EventEmitter {
|
||||
if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return;
|
||||
|
||||
// Dispatch any audio packets that were prepared in the previous cycle
|
||||
this.playable.forEach((connection) => connection.dispatchAudio());
|
||||
for (const connection of this.playable) {
|
||||
connection.dispatchAudio();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -568,6 +575,7 @@ export class AudioPlayer extends EventEmitter {
|
||||
this._signalStopSpeaking();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -612,7 +620,9 @@ export class AudioPlayer extends EventEmitter {
|
||||
* they are no longer speaking. Called once playback of a resource ends.
|
||||
*/
|
||||
private _signalStopSpeaking() {
|
||||
return this.subscribers.forEach(({ connection }) => connection.setSpeaking(false));
|
||||
for (const { connection } of this.subscribers) {
|
||||
connection.setSpeaking(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -625,10 +635,12 @@ export class AudioPlayer extends EventEmitter {
|
||||
private _preparePacket(
|
||||
packet: Buffer,
|
||||
receivers: VoiceConnection[],
|
||||
state: AudioPlayerPlayingState | AudioPlayerPausedState,
|
||||
state: AudioPlayerPausedState | AudioPlayerPlayingState,
|
||||
) {
|
||||
state.playbackDuration += 20;
|
||||
receivers.forEach((connection) => connection.prepareAudioPacket(packet));
|
||||
for (const connection of receivers) {
|
||||
connection.prepareAudioPacket(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export class AudioPlayerError extends Error {
|
||||
* The resource associated with the audio player at the time the error was thrown.
|
||||
*/
|
||||
public readonly resource: AudioResource;
|
||||
|
||||
public constructor(error: Error, resource: AudioResource) {
|
||||
super(error.message);
|
||||
this.resource = resource;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { pipeline, Readable } from 'node:stream';
|
||||
import type { Buffer } from 'node:buffer';
|
||||
import { pipeline, type Readable } from 'node:stream';
|
||||
import prism from 'prism-media';
|
||||
import { AudioPlayer, SILENCE_FRAME } from './AudioPlayer';
|
||||
import { Edge, findPipeline, StreamType, TransformerType } from './TransformerGraph';
|
||||
import { noop } from '../util/util';
|
||||
import { SILENCE_FRAME, type AudioPlayer } from './AudioPlayer';
|
||||
import { findPipeline, StreamType, TransformerType, type Edge } from './TransformerGraph';
|
||||
|
||||
/**
|
||||
* Options that are set when creating a new audio resource.
|
||||
@@ -10,6 +11,12 @@ import { noop } from '../util/util';
|
||||
* @typeParam T - the type for the metadata (if any) of the audio resource
|
||||
*/
|
||||
export interface CreateAudioResourceOptions<T> {
|
||||
/**
|
||||
* Whether or not inline volume should be enabled. If enabled, you will be able to change the volume
|
||||
* of the stream on-the-fly. However, this also increases the performance cost of playback. Defaults to `false`.
|
||||
*/
|
||||
inlineVolume?: boolean;
|
||||
|
||||
/**
|
||||
* The type of the input stream. Defaults to `StreamType.Arbitrary`.
|
||||
*/
|
||||
@@ -22,12 +29,6 @@ export interface CreateAudioResourceOptions<T> {
|
||||
*/
|
||||
metadata?: T;
|
||||
|
||||
/**
|
||||
* Whether or not inline volume should be enabled. If enabled, you will be able to change the volume
|
||||
* of the stream on-the-fly. However, this also increases the performance cost of playback. Defaults to `false`.
|
||||
*/
|
||||
inlineVolume?: boolean;
|
||||
|
||||
/**
|
||||
* The number of silence frames to append to the end of the resource's audio stream, to prevent interpolation glitches.
|
||||
* Defaults to 5.
|
||||
@@ -123,6 +124,7 @@ export class AudioResource<T = unknown> {
|
||||
if (this.silenceRemaining === -1) this.silenceRemaining = this.silencePaddingFrames;
|
||||
return this.silenceRemaining !== 0;
|
||||
}
|
||||
|
||||
return real;
|
||||
}
|
||||
|
||||
@@ -141,7 +143,6 @@ export class AudioResource<T = unknown> {
|
||||
* 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
|
||||
*/
|
||||
public read(): Buffer | null {
|
||||
@@ -151,10 +152,12 @@ export class AudioResource<T = unknown> {
|
||||
this.silenceRemaining--;
|
||||
return SILENCE_FRAME;
|
||||
}
|
||||
|
||||
const packet = this.playStream.read() as Buffer | null;
|
||||
if (packet) {
|
||||
this.playbackDuration += 20;
|
||||
}
|
||||
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
@@ -174,8 +177,8 @@ export const NO_CONSTRAINT = () => true;
|
||||
* @param stream - The stream to infer the type of
|
||||
*/
|
||||
export function inferStreamType(stream: Readable): {
|
||||
streamType: StreamType;
|
||||
hasVolume: boolean;
|
||||
streamType: StreamType;
|
||||
} {
|
||||
if (stream instanceof prism.opus.Encoder) {
|
||||
return { streamType: StreamType.Opus, hasVolume: false };
|
||||
@@ -188,6 +191,7 @@ export function inferStreamType(stream: Readable): {
|
||||
} else if (stream instanceof prism.opus.WebmDemuxer) {
|
||||
return { streamType: StreamType.Opus, hasVolume: false };
|
||||
}
|
||||
|
||||
return { streamType: StreamType.Arbitrary, hasVolume: false };
|
||||
}
|
||||
|
||||
@@ -200,14 +204,12 @@ export function inferStreamType(stream: Readable): {
|
||||
* If the input is not in the correct format, then a pipeline of transcoders and transformers will be created
|
||||
* to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg,
|
||||
* Opus transcoders, and Ogg/WebM demuxers.
|
||||
*
|
||||
* @param input - The resource to play
|
||||
* @param options - Configurable options for creating the resource
|
||||
*
|
||||
* @typeParam T - the type for the metadata (if any) of the audio resource
|
||||
*/
|
||||
export function createAudioResource<T>(
|
||||
input: string | Readable,
|
||||
input: Readable | string,
|
||||
options: CreateAudioResourceOptions<T> &
|
||||
Pick<
|
||||
T extends null | undefined ? CreateAudioResourceOptions<T> : Required<CreateAudioResourceOptions<T>>,
|
||||
@@ -224,14 +226,12 @@ export function createAudioResource<T>(
|
||||
* If the input is not in the correct format, then a pipeline of transcoders and transformers will be created
|
||||
* to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg,
|
||||
* Opus transcoders, and Ogg/WebM demuxers.
|
||||
*
|
||||
* @param input - The resource to play
|
||||
* @param options - Configurable options for creating the resource
|
||||
*
|
||||
* @typeParam T - the type for the metadata (if any) of the audio resource
|
||||
*/
|
||||
export function createAudioResource<T extends null | undefined>(
|
||||
input: string | Readable,
|
||||
input: Readable | string,
|
||||
options?: Omit<CreateAudioResourceOptions<T>, 'metadata'>,
|
||||
): AudioResource<null>;
|
||||
|
||||
@@ -244,14 +244,12 @@ export function createAudioResource<T extends null | undefined>(
|
||||
* If the input is not in the correct format, then a pipeline of transcoders and transformers will be created
|
||||
* to ensure that the resultant stream is in the correct format for playback. This could involve using FFmpeg,
|
||||
* Opus transcoders, and Ogg/WebM demuxers.
|
||||
*
|
||||
* @param input - The resource to play
|
||||
* @param options - Configurable options for creating the resource
|
||||
*
|
||||
* @typeParam T - the type for the metadata (if any) of the audio resource
|
||||
*/
|
||||
export function createAudioResource<T>(
|
||||
input: string | Readable,
|
||||
input: Readable | string,
|
||||
options: CreateAudioResourceOptions<T> = {},
|
||||
): AudioResource<T> {
|
||||
let inputType = options.inputType;
|
||||
@@ -273,6 +271,7 @@ export function createAudioResource<T>(
|
||||
// No adjustments required
|
||||
return new AudioResource<T>([], [input], (options.metadata ?? null) as T, options.silencePaddingFrames ?? 5);
|
||||
}
|
||||
|
||||
const streams = transformerPipeline.map((edge) => edge.transformer(input));
|
||||
if (typeof input !== 'string') streams.unshift(input);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/dot-notation */
|
||||
import type { AudioPlayer } from './AudioPlayer';
|
||||
import type { VoiceConnection } from '../VoiceConnection';
|
||||
import type { AudioPlayer } from './AudioPlayer';
|
||||
|
||||
/**
|
||||
* Represents a subscription of a voice connection to an audio player, allowing
|
||||
|
||||
@@ -34,33 +34,33 @@ const FFMPEG_OPUS_ARGUMENTS = [
|
||||
*/
|
||||
export enum StreamType {
|
||||
Arbitrary = 'arbitrary',
|
||||
Raw = 'raw',
|
||||
OggOpus = 'ogg/opus',
|
||||
WebmOpus = 'webm/opus',
|
||||
Opus = 'opus',
|
||||
Raw = 'raw',
|
||||
WebmOpus = 'webm/opus',
|
||||
}
|
||||
|
||||
/**
|
||||
* The different types of transformers that can exist within the pipeline.
|
||||
*/
|
||||
export enum TransformerType {
|
||||
FFmpegPCM = 'ffmpeg pcm',
|
||||
FFmpegOgg = 'ffmpeg ogg',
|
||||
OpusEncoder = 'opus encoder',
|
||||
OpusDecoder = 'opus decoder',
|
||||
OggOpusDemuxer = 'ogg/opus demuxer',
|
||||
WebmOpusDemuxer = 'webm/opus demuxer',
|
||||
FFmpegPCM = 'ffmpeg pcm',
|
||||
InlineVolume = 'volume transformer',
|
||||
OggOpusDemuxer = 'ogg/opus demuxer',
|
||||
OpusDecoder = 'opus decoder',
|
||||
OpusEncoder = 'opus encoder',
|
||||
WebmOpusDemuxer = 'webm/opus demuxer',
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a pathway from one stream type to another using a transformer.
|
||||
*/
|
||||
export interface Edge {
|
||||
cost: number;
|
||||
from: Node;
|
||||
to: Node;
|
||||
cost: number;
|
||||
transformer: (input: string | Readable) => Readable;
|
||||
transformer(input: Readable | string): Readable;
|
||||
type: TransformerType;
|
||||
}
|
||||
|
||||
@@ -113,14 +113,14 @@ getNode(StreamType.Raw).addEdge({
|
||||
type: TransformerType.OpusEncoder,
|
||||
to: getNode(StreamType.Opus),
|
||||
cost: 1.5,
|
||||
transformer: () => new prism.opus.Encoder({ rate: 48000, channels: 2, frameSize: 960 }),
|
||||
transformer: () => new prism.opus.Encoder({ rate: 48_000, channels: 2, frameSize: 960 }),
|
||||
});
|
||||
|
||||
getNode(StreamType.Opus).addEdge({
|
||||
type: TransformerType.OpusDecoder,
|
||||
to: getNode(StreamType.Raw),
|
||||
cost: 1.5,
|
||||
transformer: () => new prism.opus.Decoder({ rate: 48000, channels: 2, frameSize: 960 }),
|
||||
transformer: () => new prism.opus.Decoder({ rate: 48_000, channels: 2, frameSize: 960 }),
|
||||
});
|
||||
|
||||
getNode(StreamType.OggOpus).addEdge({
|
||||
@@ -163,6 +163,7 @@ function canEnableFFmpegOptimizations(): boolean {
|
||||
try {
|
||||
return prism.FFmpeg.getInfo().output.includes('--enable-libopus');
|
||||
} catch {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -188,11 +189,6 @@ if (canEnableFFmpegOptimizations()) {
|
||||
* Represents a step in the path from node A to node B.
|
||||
*/
|
||||
interface Step {
|
||||
/**
|
||||
* The next step.
|
||||
*/
|
||||
next?: Step;
|
||||
|
||||
/**
|
||||
* The cost of the steps after this step.
|
||||
*/
|
||||
@@ -202,6 +198,11 @@ interface Step {
|
||||
* The edge associated with this step.
|
||||
*/
|
||||
edge?: Edge;
|
||||
|
||||
/**
|
||||
* The next step.
|
||||
*/
|
||||
next?: Step;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,10 +224,10 @@ function findPath(
|
||||
if (from === goal && constraints(path)) {
|
||||
return { cost: 0 };
|
||||
} else if (depth === 0) {
|
||||
return { cost: Infinity };
|
||||
return { cost: Number.POSITIVE_INFINITY };
|
||||
}
|
||||
|
||||
let currentBest: Step | undefined = undefined;
|
||||
let currentBest: Step | undefined;
|
||||
for (const edge of from.edges) {
|
||||
if (currentBest && edge.cost > currentBest.cost) continue;
|
||||
const next = findPath(edge.to, constraints, goal, [...path, edge], depth - 1);
|
||||
@@ -235,7 +236,8 @@ function findPath(
|
||||
currentBest = { cost, edge, next };
|
||||
}
|
||||
}
|
||||
return currentBest ?? { cost: Infinity };
|
||||
|
||||
return currentBest ?? { cost: Number.POSITIVE_INFINITY };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,6 +252,7 @@ function constructPipeline(step: Step) {
|
||||
edges.push(current.edge);
|
||||
current = current.next;
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './joinVoiceChannel';
|
||||
export * from './audio';
|
||||
export * from './util';
|
||||
export * from './receive';
|
||||
export * from './audio/index';
|
||||
export * from './util/index';
|
||||
export * from './receive/index';
|
||||
|
||||
export {
|
||||
VoiceConnection,
|
||||
|
||||
@@ -6,13 +6,13 @@ import type { DiscordGatewayAdapterCreator } from './util/adapter';
|
||||
* The options that can be given when creating a voice connection.
|
||||
*/
|
||||
export interface CreateVoiceConnectionOptions {
|
||||
adapterCreator: DiscordGatewayAdapterCreator;
|
||||
|
||||
/**
|
||||
* If true, debug messages will be enabled for the voice connection and its
|
||||
* related components. Defaults to false.
|
||||
*/
|
||||
debug?: boolean | undefined;
|
||||
|
||||
adapterCreator: DiscordGatewayAdapterCreator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,6 +24,11 @@ export interface JoinVoiceChannelOptions {
|
||||
*/
|
||||
channelId: string;
|
||||
|
||||
/**
|
||||
* An optional group identifier for the voice connection.
|
||||
*/
|
||||
group?: string;
|
||||
|
||||
/**
|
||||
* The id of the guild that the voice channel belongs to.
|
||||
*/
|
||||
@@ -38,20 +43,14 @@ export interface JoinVoiceChannelOptions {
|
||||
* Whether to join the channel muted (defaults to true)
|
||||
*/
|
||||
selfMute?: boolean;
|
||||
|
||||
/**
|
||||
* An optional group identifier for the voice connection.
|
||||
*/
|
||||
group?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a VoiceConnection to a Discord voice channel.
|
||||
*
|
||||
* @param voiceChannel - the voice channel to connect to
|
||||
* @param options - the options for joining the voice channel
|
||||
*/
|
||||
export function joinVoiceChannel(options: JoinVoiceChannelOptions & CreateVoiceConnectionOptions) {
|
||||
export function joinVoiceChannel(options: CreateVoiceConnectionOptions & JoinVoiceChannelOptions) {
|
||||
const joinConfig: JoinConfig = {
|
||||
selfDeaf: true,
|
||||
selfMute: false,
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
/* eslint-disable id-length */
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
|
||||
import type { CloseEvent } from 'ws';
|
||||
import { VoiceUDPSocket } from './VoiceUDPSocket';
|
||||
import { VoiceWebSocket } from './VoiceWebSocket';
|
||||
import * as secretbox from '../util/Secretbox';
|
||||
import { noop } from '../util/util';
|
||||
import { VoiceUDPSocket } from './VoiceUDPSocket';
|
||||
import { VoiceWebSocket } from './VoiceWebSocket';
|
||||
|
||||
// The number of audio channels required by Discord
|
||||
const CHANNELS = 2;
|
||||
const TIMESTAMP_INC = (48000 / 100) * CHANNELS;
|
||||
const TIMESTAMP_INC = (48_000 / 100) * CHANNELS;
|
||||
const MAX_NONCE_SIZE = 2 ** 32 - 1;
|
||||
|
||||
export const SUPPORTED_ENCRYPTION_MODES = ['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305'];
|
||||
@@ -35,8 +39,8 @@ export enum NetworkingStatusCode {
|
||||
*/
|
||||
export interface NetworkingOpeningWsState {
|
||||
code: NetworkingStatusCode.OpeningWs;
|
||||
ws: VoiceWebSocket;
|
||||
connectionOptions: ConnectionOptions;
|
||||
ws: VoiceWebSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,8 +48,8 @@ export interface NetworkingOpeningWsState {
|
||||
*/
|
||||
export interface NetworkingIdentifyingState {
|
||||
code: NetworkingStatusCode.Identifying;
|
||||
ws: VoiceWebSocket;
|
||||
connectionOptions: ConnectionOptions;
|
||||
ws: VoiceWebSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,10 +58,10 @@ export interface NetworkingIdentifyingState {
|
||||
*/
|
||||
export interface NetworkingUdpHandshakingState {
|
||||
code: NetworkingStatusCode.UdpHandshaking;
|
||||
ws: VoiceWebSocket;
|
||||
udp: VoiceUDPSocket;
|
||||
connectionOptions: ConnectionOptions;
|
||||
connectionData: Pick<ConnectionData, 'ssrc'>;
|
||||
connectionOptions: ConnectionOptions;
|
||||
udp: VoiceUDPSocket;
|
||||
ws: VoiceWebSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,10 +69,10 @@ export interface NetworkingUdpHandshakingState {
|
||||
*/
|
||||
export interface NetworkingSelectingProtocolState {
|
||||
code: NetworkingStatusCode.SelectingProtocol;
|
||||
ws: VoiceWebSocket;
|
||||
udp: VoiceUDPSocket;
|
||||
connectionOptions: ConnectionOptions;
|
||||
connectionData: Pick<ConnectionData, 'ssrc'>;
|
||||
connectionOptions: ConnectionOptions;
|
||||
udp: VoiceUDPSocket;
|
||||
ws: VoiceWebSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,11 +81,11 @@ export interface NetworkingSelectingProtocolState {
|
||||
*/
|
||||
export interface NetworkingReadyState {
|
||||
code: NetworkingStatusCode.Ready;
|
||||
ws: VoiceWebSocket;
|
||||
udp: VoiceUDPSocket;
|
||||
connectionOptions: ConnectionOptions;
|
||||
connectionData: ConnectionData;
|
||||
connectionOptions: ConnectionOptions;
|
||||
preparedPacket?: Buffer | undefined;
|
||||
udp: VoiceUDPSocket;
|
||||
ws: VoiceWebSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,11 +94,11 @@ export interface NetworkingReadyState {
|
||||
*/
|
||||
export interface NetworkingResumingState {
|
||||
code: NetworkingStatusCode.Resuming;
|
||||
ws: VoiceWebSocket;
|
||||
udp: VoiceUDPSocket;
|
||||
connectionOptions: ConnectionOptions;
|
||||
connectionData: ConnectionData;
|
||||
connectionOptions: ConnectionOptions;
|
||||
preparedPacket?: Buffer | undefined;
|
||||
udp: VoiceUDPSocket;
|
||||
ws: VoiceWebSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,13 +113,13 @@ export interface NetworkingClosedState {
|
||||
* The various states that a networking instance can be in.
|
||||
*/
|
||||
export type NetworkingState =
|
||||
| NetworkingOpeningWsState
|
||||
| NetworkingClosedState
|
||||
| NetworkingIdentifyingState
|
||||
| NetworkingUdpHandshakingState
|
||||
| NetworkingSelectingProtocolState
|
||||
| NetworkingOpeningWsState
|
||||
| NetworkingReadyState
|
||||
| NetworkingResumingState
|
||||
| NetworkingClosedState;
|
||||
| NetworkingSelectingProtocolState
|
||||
| NetworkingUdpHandshakingState;
|
||||
|
||||
/**
|
||||
* Details required to connect to the Discord voice gateway. These details
|
||||
@@ -123,11 +127,11 @@ export type NetworkingState =
|
||||
* and VOICE_STATE_UPDATE packets.
|
||||
*/
|
||||
interface ConnectionOptions {
|
||||
endpoint: string;
|
||||
serverId: string;
|
||||
userId: string;
|
||||
sessionId: string;
|
||||
token: string;
|
||||
endpoint: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,15 +139,15 @@ interface ConnectionOptions {
|
||||
* the connection, timing information for playback of streams.
|
||||
*/
|
||||
export interface ConnectionData {
|
||||
ssrc: number;
|
||||
encryptionMode: string;
|
||||
secretKey: Uint8Array;
|
||||
sequence: number;
|
||||
timestamp: number;
|
||||
packetsPlayed: number;
|
||||
nonce: number;
|
||||
nonceBuffer: Buffer;
|
||||
packetsPlayed: number;
|
||||
secretKey: Uint8Array;
|
||||
sequence: number;
|
||||
speaking: boolean;
|
||||
ssrc: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,16 +190,17 @@ function chooseEncryptionMode(options: string[]): string {
|
||||
if (!option) {
|
||||
throw new Error(`No compatible encryption modes. Available include: ${options.join(', ')}`);
|
||||
}
|
||||
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random number that is in the range of n bits.
|
||||
*
|
||||
* @param n - The number of bits
|
||||
* @param numberOfBits - The number of bits
|
||||
*/
|
||||
function randomNBit(n: number) {
|
||||
return Math.floor(Math.random() * 2 ** n);
|
||||
function randomNBit(numberOfBits: number) {
|
||||
return Math.floor(Math.random() * 2 ** numberOfBits);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,7 +212,7 @@ export class Networking extends EventEmitter {
|
||||
/**
|
||||
* The debug logger function, if debugging is enabled.
|
||||
*/
|
||||
private readonly debug: null | ((message: string) => void);
|
||||
private readonly debug: ((message: string) => void) | null;
|
||||
|
||||
/**
|
||||
* Creates a new Networking instance.
|
||||
@@ -287,7 +292,6 @@ export class Networking extends EventEmitter {
|
||||
* Creates a new WebSocket to a Discord Voice gateway.
|
||||
*
|
||||
* @param endpoint - The endpoint to connect to
|
||||
* @param debug - Whether to enable debug logging
|
||||
*/
|
||||
private createWebSocket(endpoint: string) {
|
||||
const ws = new VoiceWebSocket(`wss://${endpoint}?v=4`, Boolean(this.debug));
|
||||
@@ -351,7 +355,7 @@ export class Networking extends EventEmitter {
|
||||
* @param code - The close code
|
||||
*/
|
||||
private onWsClose({ code }: CloseEvent) {
|
||||
const canResume = code === 4015 || code < 4000;
|
||||
const canResume = code === 4_015 || code < 4_000;
|
||||
if (canResume && this.state.code === NetworkingStatusCode.Ready) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
@@ -400,6 +404,7 @@ export class Networking extends EventEmitter {
|
||||
udp
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
.performIPDiscovery(ssrc)
|
||||
// eslint-disable-next-line promise/prefer-await-to-then
|
||||
.then((localConfig) => {
|
||||
if (this.state.code !== NetworkingStatusCode.UdpHandshaking) return;
|
||||
this.state.ws.sendPacket({
|
||||
@@ -419,6 +424,7 @@ export class Networking extends EventEmitter {
|
||||
code: NetworkingStatusCode.SelectingProtocol,
|
||||
};
|
||||
})
|
||||
// eslint-disable-next-line promise/prefer-await-to-then, promise/prefer-await-to-callbacks
|
||||
.catch((error: Error) => this.emit('error', error));
|
||||
|
||||
this.state = {
|
||||
@@ -489,15 +495,14 @@ export class Networking extends EventEmitter {
|
||||
* @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
|
||||
*/
|
||||
public prepareAudioPacket(opusPacket: Buffer) {
|
||||
const state = this.state;
|
||||
if (state.code !== NetworkingStatusCode.Ready) return;
|
||||
state.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData);
|
||||
// eslint-disable-next-line consistent-return
|
||||
return state.preparedPacket;
|
||||
}
|
||||
|
||||
@@ -513,6 +518,7 @@ export class Networking extends EventEmitter {
|
||||
state.preparedPacket = undefined;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -598,6 +604,7 @@ export class Networking extends EventEmitter {
|
||||
const random = secretbox.methods.random(24, connectionData.nonceBuffer);
|
||||
return [secretbox.methods.close(opusPacket, random, secretKey), random];
|
||||
}
|
||||
|
||||
return [secretbox.methods.close(opusPacket, nonce, secretKey)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||
import { createSocket, Socket } from 'node:dgram';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { createSocket, type Socket } from 'node:dgram';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { isIPv4 } from 'node:net';
|
||||
|
||||
@@ -13,8 +14,8 @@ export interface SocketConfig {
|
||||
}
|
||||
|
||||
interface KeepAlive {
|
||||
value: number;
|
||||
timestamp: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,7 +26,7 @@ interface KeepAlive {
|
||||
export function parseLocalPacket(message: Buffer): SocketConfig {
|
||||
const packet = Buffer.from(message);
|
||||
|
||||
const ip = packet.slice(8, packet.indexOf(0, 8)).toString('utf-8');
|
||||
const ip = packet.slice(8, packet.indexOf(0, 8)).toString('utf8');
|
||||
|
||||
if (!isIPv4(ip)) {
|
||||
throw new Error('Malformed IP address');
|
||||
@@ -100,7 +101,7 @@ export class VoiceUDPSocket extends EventEmitter {
|
||||
/**
|
||||
* The debug logger function, if debugging is enabled.
|
||||
*/
|
||||
private readonly debug: null | ((message: string) => void);
|
||||
private readonly debug: ((message: string) => void) | null;
|
||||
|
||||
/**
|
||||
* Creates a new VoiceUDPSocket.
|
||||
@@ -137,6 +138,7 @@ export class VoiceUDPSocket extends EventEmitter {
|
||||
// Delete all keep alives up to and including the received one
|
||||
this.keepAlives.splice(0, index);
|
||||
}
|
||||
|
||||
// Propagate the message
|
||||
this.emit('message', buffer);
|
||||
}
|
||||
@@ -169,7 +171,7 @@ export class VoiceUDPSocket extends EventEmitter {
|
||||
* @param buffer - The buffer to send
|
||||
*/
|
||||
public send(buffer: Buffer) {
|
||||
return this.socket.send(buffer, this.remote.port, this.remote.ip);
|
||||
this.socket.send(buffer, this.remote.port, this.remote.ip);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,6 +181,7 @@ export class VoiceUDPSocket extends EventEmitter {
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch {}
|
||||
|
||||
clearInterval(this.keepAliveInterval);
|
||||
}
|
||||
|
||||
@@ -187,7 +190,7 @@ export class VoiceUDPSocket extends EventEmitter {
|
||||
*
|
||||
* @param ssrc - The SSRC received from Discord
|
||||
*/
|
||||
public performIPDiscovery(ssrc: number): Promise<SocketConfig> {
|
||||
public async performIPDiscovery(ssrc: number): Promise<SocketConfig> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const listener = (message: Buffer) => {
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
|
||||
import WebSocket, { MessageEvent } from 'ws';
|
||||
import WebSocket, { type MessageEvent } from 'ws';
|
||||
|
||||
export interface VoiceWebSocket extends EventEmitter {
|
||||
on(event: 'error', listener: (error: Error) => void): this;
|
||||
@@ -56,7 +56,7 @@ export class VoiceWebSocket extends EventEmitter {
|
||||
/**
|
||||
* The debug logger function, if debugging is enabled.
|
||||
*/
|
||||
private readonly debug: null | ((message: string) => void);
|
||||
private readonly debug: ((message: string) => void) | null;
|
||||
|
||||
/**
|
||||
* The underlying WebSocket of this wrapper.
|
||||
@@ -71,11 +71,11 @@ export class VoiceWebSocket extends EventEmitter {
|
||||
public constructor(address: string, debug: boolean) {
|
||||
super();
|
||||
this.ws = new WebSocket(address);
|
||||
this.ws.onmessage = (e) => this.onMessage(e);
|
||||
this.ws.onopen = (e) => this.emit('open', e);
|
||||
this.ws.onmessage = (err) => this.onMessage(err);
|
||||
this.ws.onopen = (err) => this.emit('open', err);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
this.ws.onerror = (e: Error | WebSocket.ErrorEvent) => this.emit('error', e instanceof Error ? e : e.error);
|
||||
this.ws.onclose = (e) => this.emit('close', e);
|
||||
this.ws.onerror = (err: Error | WebSocket.ErrorEvent) => this.emit('error', err instanceof Error ? err : err.error);
|
||||
this.ws.onclose = (err) => this.emit('close', err);
|
||||
|
||||
this.lastHeartbeatAck = 0;
|
||||
this.lastHeartbeatSend = 0;
|
||||
@@ -90,10 +90,10 @@ export class VoiceWebSocket extends EventEmitter {
|
||||
try {
|
||||
this.debug?.('destroyed');
|
||||
this.setHeartbeatInterval(-1);
|
||||
this.ws.close(1000);
|
||||
this.ws.close(1_000);
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
this.emit('error', e);
|
||||
const err = error as Error;
|
||||
this.emit('error', err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ export class VoiceWebSocket extends EventEmitter {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
packet = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
this.emit('error', e);
|
||||
const err = error as Error;
|
||||
this.emit('error', err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,10 +137,11 @@ export class VoiceWebSocket extends EventEmitter {
|
||||
try {
|
||||
const stringified = JSON.stringify(packet);
|
||||
this.debug?.(`>> ${stringified}`);
|
||||
return this.ws.send(stringified);
|
||||
this.ws.send(stringified);
|
||||
return;
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
this.emit('error', e);
|
||||
const err = error as Error;
|
||||
this.emit('error', err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +152,9 @@ export class VoiceWebSocket extends EventEmitter {
|
||||
this.lastHeartbeatSend = Date.now();
|
||||
this.missedHeartbeats++;
|
||||
const nonce = this.lastHeartbeatSend;
|
||||
return this.sendPacket({
|
||||
this.sendPacket({
|
||||
op: VoiceOpcodes.Heartbeat,
|
||||
// eslint-disable-next-line id-length
|
||||
d: nonce,
|
||||
});
|
||||
}
|
||||
@@ -171,6 +173,7 @@ export class VoiceWebSocket extends EventEmitter {
|
||||
this.ws.close();
|
||||
this.setHeartbeatInterval(-1);
|
||||
}
|
||||
|
||||
this.sendHeartbeat();
|
||||
}, ms);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Readable, ReadableOptions } from 'node:stream';
|
||||
import type { Buffer } from 'node:buffer';
|
||||
import { Readable, type ReadableOptions } from 'node:stream';
|
||||
import { SILENCE_FRAME } from '../audio/AudioPlayer';
|
||||
|
||||
/**
|
||||
@@ -23,11 +24,11 @@ export enum EndBehaviorType {
|
||||
|
||||
export type EndBehavior =
|
||||
| {
|
||||
behavior: EndBehaviorType.Manual;
|
||||
behavior: EndBehaviorType.AfterInactivity | EndBehaviorType.AfterSilence;
|
||||
duration: number;
|
||||
}
|
||||
| {
|
||||
behavior: EndBehaviorType.AfterSilence | EndBehaviorType.AfterInactivity;
|
||||
duration: number;
|
||||
behavior: EndBehaviorType.Manual;
|
||||
};
|
||||
|
||||
export interface AudioReceiveStreamOptions extends ReadableOptions {
|
||||
@@ -64,14 +65,13 @@ export class AudioReceiveStream extends Readable {
|
||||
}
|
||||
|
||||
public override push(buffer: Buffer | null) {
|
||||
if (buffer) {
|
||||
if (
|
||||
this.end.behavior === EndBehaviorType.AfterInactivity ||
|
||||
if (
|
||||
buffer &&
|
||||
(this.end.behavior === EndBehaviorType.AfterInactivity ||
|
||||
(this.end.behavior === EndBehaviorType.AfterSilence &&
|
||||
(buffer.compare(SILENCE_FRAME) !== 0 || typeof this.endTimeout === 'undefined'))
|
||||
) {
|
||||
this.renewEndTimeout(this.end);
|
||||
}
|
||||
(buffer.compare(SILENCE_FRAME) !== 0 || typeof this.endTimeout === 'undefined')))
|
||||
) {
|
||||
this.renewEndTimeout(this.end);
|
||||
}
|
||||
|
||||
return super.push(buffer);
|
||||
@@ -81,6 +81,7 @@ export class AudioReceiveStream extends Readable {
|
||||
if (this.endTimeout) {
|
||||
clearTimeout(this.endTimeout);
|
||||
}
|
||||
|
||||
this.endTimeout = setTimeout(() => this.push(null), end.duration);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,16 @@ export interface VoiceUserData {
|
||||
*/
|
||||
audioSSRC: number;
|
||||
|
||||
/**
|
||||
* The Discord user id of the user.
|
||||
*/
|
||||
userId: string;
|
||||
|
||||
/**
|
||||
* The SSRC of the user's video stream (if one exists)
|
||||
* Cannot be 0. If undefined, the user has no video stream.
|
||||
*/
|
||||
videoSSRC?: number;
|
||||
|
||||
/**
|
||||
* The Discord user id of the user.
|
||||
*/
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface SSRCMap extends EventEmitter {
|
||||
@@ -83,7 +83,6 @@ export class SSRCMap extends EventEmitter {
|
||||
* 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
|
||||
*/
|
||||
public delete(target: number | string) {
|
||||
@@ -93,6 +92,7 @@ export class SSRCMap extends EventEmitter {
|
||||
this.map.delete(target);
|
||||
this.emit('delete', existing);
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ import { EventEmitter } from 'node:events';
|
||||
export interface SpeakingMap extends EventEmitter {
|
||||
/**
|
||||
* Emitted when a user starts speaking.
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on(event: 'start', listener: (userId: string) => void): this;
|
||||
|
||||
/**
|
||||
* Emitted when a user ends speaking.
|
||||
*
|
||||
* @eventProperty
|
||||
*/
|
||||
on(event: 'end', listener: (userId: string) => void): this;
|
||||
@@ -45,6 +47,7 @@ export class SpeakingMap extends EventEmitter {
|
||||
this.users.set(userId, Date.now());
|
||||
this.emit('start', userId);
|
||||
}
|
||||
|
||||
this.startTimeout(userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/* eslint-disable jsdoc/check-param-names */
|
||||
import { Buffer } from 'node:buffer';
|
||||
import { VoiceOpcodes } from 'discord-api-types/voice/v4';
|
||||
import {
|
||||
AudioReceiveStream,
|
||||
AudioReceiveStreamOptions,
|
||||
createDefaultAudioReceiveStreamOptions,
|
||||
} from './AudioReceiveStream';
|
||||
import { SSRCMap } from './SSRCMap';
|
||||
import { SpeakingMap } from './SpeakingMap';
|
||||
import type { VoiceConnection } from '../VoiceConnection';
|
||||
import type { ConnectionData } from '../networking/Networking';
|
||||
import { methods } from '../util/Secretbox';
|
||||
import {
|
||||
AudioReceiveStream,
|
||||
createDefaultAudioReceiveStreamOptions,
|
||||
type AudioReceiveStreamOptions,
|
||||
} from './AudioReceiveStream';
|
||||
import { SSRCMap } from './SSRCMap';
|
||||
import { SpeakingMap } from './SpeakingMap';
|
||||
|
||||
/**
|
||||
* Attaches to a VoiceConnection, allowing you to receive audio packets from other
|
||||
@@ -59,7 +61,6 @@ export class VoiceReceiver {
|
||||
* Called when a packet is received on the attached connection's WebSocket.
|
||||
*
|
||||
* @param packet - The received packet
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public onWsPacket(packet: any) {
|
||||
@@ -112,6 +113,7 @@ export class VoiceReceiver {
|
||||
// Open packet
|
||||
const decrypted = methods.open(buffer.slice(12, end), nonce, secretKey);
|
||||
if (!decrypted) return;
|
||||
// eslint-disable-next-line consistent-return
|
||||
return Buffer.from(decrypted);
|
||||
}
|
||||
|
||||
@@ -122,7 +124,6 @@ export class VoiceReceiver {
|
||||
* @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
|
||||
*/
|
||||
private parsePacket(buffer: Buffer, mode: string, nonce: Buffer, secretKey: Uint8Array) {
|
||||
@@ -135,6 +136,7 @@ export class VoiceReceiver {
|
||||
packet = packet.subarray(4 + 4 * headerExtensionLength);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return packet;
|
||||
}
|
||||
|
||||
@@ -142,7 +144,6 @@ export class VoiceReceiver {
|
||||
* Called when the UDP socket of the attached connection receives a message.
|
||||
*
|
||||
* @param msg - The received message
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public onUdpMessage(msg: Buffer) {
|
||||
@@ -176,7 +177,6 @@ export class VoiceReceiver {
|
||||
* 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
|
||||
*/
|
||||
public subscribe(userId: string, options?: Partial<AudioReceiveStreamOptions>) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
|
||||
interface Methods {
|
||||
open: (buffer: Buffer, nonce: Buffer, secretKey: Uint8Array) => Buffer | null;
|
||||
close: (opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array) => Buffer;
|
||||
random: (bytes: number, nonce: Buffer) => Buffer;
|
||||
close(opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array): Buffer;
|
||||
open(buffer: Buffer, nonce: Buffer, secretKey: Uint8Array): Buffer | null;
|
||||
random(bytes: number, nonce: Buffer): Buffer;
|
||||
}
|
||||
|
||||
const libs = {
|
||||
@@ -14,6 +16,7 @@ const libs = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
if (sodium.crypto_secretbox_open_easy(output, buffer, nonce, secretKey)) return output;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
close: (opusPacket: Buffer, nonce: Buffer, secretKey: Uint8Array) => {
|
||||
@@ -24,7 +27,7 @@ const libs = {
|
||||
return output;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
random: (n: number, buffer: Buffer = Buffer.allocUnsafe(n)) => {
|
||||
random: (num: number, buffer: Buffer = Buffer.allocUnsafe(num)) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
sodium.randombytes_buf(buffer);
|
||||
return buffer;
|
||||
@@ -36,7 +39,7 @@ const libs = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
close: sodium.api.crypto_secretbox_easy,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
random: (n: number, buffer: Buffer = Buffer.allocUnsafe(n)) => {
|
||||
random: (num: number, buffer: Buffer = Buffer.allocUnsafe(num)) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
sodium.api.randombytes_buf(buffer);
|
||||
return buffer;
|
||||
@@ -77,7 +80,7 @@ const methods: Methods = {
|
||||
void (async () => {
|
||||
for (const libName of Object.keys(libs) as (keyof typeof libs)[]) {
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
// eslint-disable-next-line unicorn/no-abusive-eslint-disable, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
||||
const lib = require(libName);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (libName === 'libsodium-wrappers' && lib.ready) await lib.ready;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
export function abortAfter(delay: number): [AbortController, AbortSignal] {
|
||||
const ac = new AbortController();
|
||||
const timeout = setTimeout(() => ac.abort(), delay);
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error: No type for timeout
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
ac.signal.addEventListener('abort', () => clearTimeout(timeout));
|
||||
return [ac, ac.signal];
|
||||
|
||||
@@ -5,41 +5,40 @@ import type { GatewayVoiceServerUpdateDispatchData, GatewayVoiceStateUpdateDispa
|
||||
* Discord gateway DiscordGatewayAdapters.
|
||||
*/
|
||||
export interface DiscordGatewayAdapterLibraryMethods {
|
||||
/**
|
||||
* Call this when the adapter can no longer be used (e.g. due to a disconnect from the main gateway)
|
||||
*/
|
||||
destroy(): void;
|
||||
/**
|
||||
* Call this when you receive a VOICE_SERVER_UPDATE payload that is relevant to the adapter.
|
||||
*
|
||||
* @param data - The inner data of the VOICE_SERVER_UPDATE payload
|
||||
*/
|
||||
onVoiceServerUpdate: (data: GatewayVoiceServerUpdateDispatchData) => void;
|
||||
onVoiceServerUpdate(data: GatewayVoiceServerUpdateDispatchData): void;
|
||||
/**
|
||||
* Call this when you receive a VOICE_STATE_UPDATE payload that is relevant to the adapter.
|
||||
*
|
||||
* @param data - The inner data of the VOICE_STATE_UPDATE payload
|
||||
*/
|
||||
onVoiceStateUpdate: (data: GatewayVoiceStateUpdateDispatchData) => void;
|
||||
/**
|
||||
* Call this when the adapter can no longer be used (e.g. due to a disconnect from the main gateway)
|
||||
*/
|
||||
destroy: () => void;
|
||||
onVoiceStateUpdate(data: GatewayVoiceStateUpdateDispatchData): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Methods that are provided by the implementer of a Discord gateway DiscordGatewayAdapter.
|
||||
*/
|
||||
export interface DiscordGatewayAdapterImplementerMethods {
|
||||
/**
|
||||
* Implement this method such that the given payload is sent to the main Discord gateway connection.
|
||||
*
|
||||
* @param payload - The payload to send to the main Discord gateway connection
|
||||
*
|
||||
* @returns `false` if the payload definitely failed to send - in this case, the voice connection disconnects
|
||||
*/
|
||||
sendPayload: (payload: any) => boolean;
|
||||
/**
|
||||
* This will be called by \@discordjs/voice when the adapter can safely be destroyed as it will no
|
||||
* longer be used.
|
||||
*/
|
||||
destroy: () => void;
|
||||
destroy(): void;
|
||||
/**
|
||||
* Implement this method such that the given payload is sent to the main Discord gateway connection.
|
||||
*
|
||||
* @param payload - The payload to send to the main Discord gateway connection
|
||||
* @returns `false` if the payload definitely failed to send - in this case, the voice connection disconnects
|
||||
*/
|
||||
sendPayload(payload: any): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { Buffer } from 'node:buffer';
|
||||
import process from 'node:process';
|
||||
import { Readable } from 'node:stream';
|
||||
import prism from 'prism-media';
|
||||
import { noop } from './util';
|
||||
import { StreamType } from '..';
|
||||
import { noop } from './util';
|
||||
|
||||
/**
|
||||
* Takes an Opus Head, and verifies whether the associated Opus audio is suitable to play in a Discord voice channel.
|
||||
*
|
||||
* @param opusHead - The Opus Head to validate
|
||||
*
|
||||
* @returns `true` if suitable to play in a Discord voice channel, otherwise `false`
|
||||
*/
|
||||
export function validateDiscordOpusHead(opusHead: Buffer): boolean {
|
||||
const channels = opusHead.readUInt8(9);
|
||||
const sampleRate = opusHead.readUInt32LE(12);
|
||||
return channels === 2 && sampleRate === 48000;
|
||||
return channels === 2 && sampleRate === 48_000;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,22 +39,28 @@ export interface ProbeInfo {
|
||||
* @param stream - The readable stream to probe
|
||||
* @param probeSize - The number of bytes to attempt to read before giving up on the probe
|
||||
* @param validator - The Opus Head validator function
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
export function demuxProbe(
|
||||
export async function demuxProbe(
|
||||
stream: Readable,
|
||||
probeSize = 1024,
|
||||
probeSize = 1_024,
|
||||
validator = validateDiscordOpusHead,
|
||||
): Promise<ProbeInfo> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Preconditions
|
||||
if (stream.readableObjectMode) return reject(new Error('Cannot probe a readable stream in object mode'));
|
||||
if (stream.readableEnded) return reject(new Error('Cannot probe a stream that has ended'));
|
||||
if (stream.readableObjectMode) {
|
||||
reject(new Error('Cannot probe a readable stream in object mode'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (stream.readableEnded) {
|
||||
reject(new Error('Cannot probe a stream that has ended'));
|
||||
return;
|
||||
}
|
||||
|
||||
let readBuffer = Buffer.alloc(0);
|
||||
|
||||
let resolved: StreamType | undefined = undefined;
|
||||
let resolved: StreamType | undefined;
|
||||
|
||||
const finish = (type: StreamType) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
@@ -73,6 +80,7 @@ export function demuxProbe(
|
||||
if (readBuffer.length > 0) {
|
||||
stream.push(readBuffer);
|
||||
}
|
||||
|
||||
resolve({
|
||||
stream,
|
||||
type,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import EventEmitter, { once } from 'node:events';
|
||||
import { abortAfter } from './abortAfter';
|
||||
import { type EventEmitter, once } from 'node:events';
|
||||
import type { VoiceConnection, VoiceConnectionStatus } from '../VoiceConnection';
|
||||
import type { AudioPlayer, AudioPlayerStatus } from '../audio/AudioPlayer';
|
||||
import { abortAfter } from './abortAfter';
|
||||
|
||||
/**
|
||||
* Allows a voice connection a specified amount of time to enter a given state, otherwise rejects with an error.
|
||||
@@ -13,7 +13,7 @@ import type { AudioPlayer, AudioPlayerStatus } from '../audio/AudioPlayer';
|
||||
export function entersState(
|
||||
target: VoiceConnection,
|
||||
status: VoiceConnectionStatus,
|
||||
timeoutOrSignal: number | AbortSignal,
|
||||
timeoutOrSignal: AbortSignal | number,
|
||||
): Promise<VoiceConnection>;
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,7 @@ export function entersState(
|
||||
export function entersState(
|
||||
target: AudioPlayer,
|
||||
status: AudioPlayerStatus,
|
||||
timeoutOrSignal: number | AbortSignal,
|
||||
timeoutOrSignal: AbortSignal | number,
|
||||
): Promise<AudioPlayer>;
|
||||
|
||||
/**
|
||||
@@ -36,10 +36,10 @@ export function entersState(
|
||||
* @param status - The status that the target should be in
|
||||
* @param timeoutOrSignal - The maximum time we are allowing for this to occur, or a signal that will abort the operation
|
||||
*/
|
||||
export async function entersState<T extends VoiceConnection | AudioPlayer>(
|
||||
export async function entersState<T extends AudioPlayer | VoiceConnection>(
|
||||
target: T,
|
||||
status: VoiceConnectionStatus | AudioPlayerStatus,
|
||||
timeoutOrSignal: number | AbortSignal,
|
||||
status: AudioPlayerStatus | VoiceConnectionStatus,
|
||||
timeoutOrSignal: AbortSignal | number,
|
||||
) {
|
||||
if (target.state.status !== status) {
|
||||
const [ac, signal] =
|
||||
@@ -50,5 +50,6 @@ export async function entersState<T extends VoiceConnection | AudioPlayer>(
|
||||
ac?.abort();
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ function findPackageJSON(
|
||||
if (pkg.name !== packageName) throw new Error('package.json does not match');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return pkg;
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return findPackageJSON(resolve(dir, '..'), packageName, depth - 1);
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ function version(name: string): string {
|
||||
: findPackageJSON(dirname(require.resolve(name)), name, 3);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return pkg?.version ?? 'not found';
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return 'not found';
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export function generateDependencyReport() {
|
||||
const info = prism.FFmpeg.getInfo();
|
||||
report.push(`- version: ${info.version}`);
|
||||
report.push(`- libopus: ${info.output.includes('--enable-libopus') ? 'yes' : 'no'}`);
|
||||
} catch (err) {
|
||||
} catch {
|
||||
report.push('- not found');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user