chapterjs 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/asset.d.ts +46 -0
- package/dist/assets/asset.js +86 -0
- package/dist/assets/public.d.ts +19 -0
- package/dist/assets/public.js +120 -0
- package/dist/cache/cache.d.ts +62 -0
- package/dist/cache/cache.js +80 -0
- package/dist/cache/store.d.ts +45 -0
- package/dist/cache/store.js +53 -0
- package/dist/cli/build.d.ts +41 -0
- package/dist/cli/build.js +206 -0
- package/dist/cli/cli.js +108 -25
- package/dist/cli/cluster.d.ts +70 -0
- package/dist/cli/cluster.js +213 -0
- package/dist/cli/dev.d.ts +18 -1
- package/dist/cli/dev.js +252 -198
- package/dist/cli/env.d.ts +31 -0
- package/dist/cli/env.js +93 -0
- package/dist/cli/log.d.ts +13 -0
- package/dist/cli/log.js +17 -0
- package/dist/cli/node-version.d.ts +8 -0
- package/dist/cli/node-version.js +13 -0
- package/dist/cli/preflight.d.ts +36 -0
- package/dist/cli/preflight.js +122 -0
- package/dist/cli/project.d.ts +122 -0
- package/dist/cli/project.js +452 -0
- package/dist/cli/start.d.ts +25 -0
- package/dist/cli/start.js +225 -0
- package/dist/commands/autocomplete.d.ts +13 -0
- package/dist/commands/autocomplete.js +159 -0
- package/dist/commands/command.d.ts +346 -0
- package/dist/commands/command.js +28 -0
- package/dist/commands/convention.d.ts +64 -0
- package/dist/commands/convention.js +464 -0
- package/dist/commands/register.d.ts +18 -0
- package/dist/commands/register.js +42 -0
- package/dist/commands/router.d.ts +20 -0
- package/dist/commands/router.js +228 -0
- package/dist/commands/tree.d.ts +43 -0
- package/dist/commands/tree.js +206 -0
- package/dist/commands/twins.d.ts +15 -0
- package/dist/commands/twins.js +78 -0
- package/dist/components/button.d.ts +79 -0
- package/dist/components/button.js +108 -0
- package/dist/components/component.d.ts +140 -0
- package/dist/components/component.js +39 -0
- package/dist/components/convention.d.ts +35 -0
- package/dist/components/convention.js +339 -0
- package/dist/components/custom-id.d.ts +72 -0
- package/dist/components/custom-id.js +128 -0
- package/dist/components/embed.d.ts +28 -0
- package/dist/components/embed.js +11 -0
- package/dist/components/file.d.ts +30 -0
- package/dist/components/file.js +52 -0
- package/dist/components/instance.d.ts +100 -0
- package/dist/components/instance.js +86 -0
- package/dist/components/layout.d.ts +103 -0
- package/dist/components/layout.js +279 -0
- package/dist/components/modal.d.ts +244 -0
- package/dist/components/modal.js +399 -0
- package/dist/components/options.d.ts +41 -0
- package/dist/components/options.js +101 -0
- package/dist/components/render.d.ts +12 -0
- package/dist/components/render.js +58 -0
- package/dist/components/router.d.ts +18 -0
- package/dist/components/router.js +256 -0
- package/dist/components/select.d.ts +144 -0
- package/dist/components/select.js +179 -0
- package/dist/core/bot.d.ts +99 -0
- package/dist/core/bot.js +174 -0
- package/dist/core/memory.d.ts +66 -0
- package/dist/core/memory.js +115 -0
- package/dist/discord/api.d.ts +112 -0
- package/dist/discord/api.js +115 -0
- package/dist/discord/cdn.d.ts +67 -0
- package/dist/discord/cdn.js +100 -0
- package/dist/discord/codes.d.ts +171 -0
- package/dist/discord/codes.js +421 -0
- package/dist/discord/endpoint.d.ts +56 -0
- package/dist/discord/endpoint.js +53 -0
- package/dist/discord/endpoints.d.ts +1529 -0
- package/dist/discord/endpoints.js +1139 -0
- package/dist/discord/formatting.d.ts +67 -0
- package/dist/discord/formatting.js +87 -0
- package/dist/discord/index.d.ts +10 -0
- package/dist/discord/index.js +13 -0
- package/dist/discord/intents.d.ts +48 -0
- package/dist/discord/intents.js +128 -0
- package/dist/discord/permissions.d.ts +94 -0
- package/dist/discord/permissions.js +185 -0
- package/dist/discord/snowflake.d.ts +21 -0
- package/dist/discord/snowflake.js +36 -0
- package/dist/discord/types/application-command.d.ts +332 -0
- package/dist/discord/types/application-command.js +48 -0
- package/dist/discord/types/application-role-connection-metadata.d.ts +42 -0
- package/dist/discord/types/application-role-connection-metadata.js +22 -0
- package/dist/discord/types/application.d.ts +208 -0
- package/dist/discord/types/application.js +48 -0
- package/dist/discord/types/audit-log.d.ts +255 -0
- package/dist/discord/types/audit-log.js +144 -0
- package/dist/discord/types/auto-moderation.d.ts +167 -0
- package/dist/discord/types/auto-moderation.js +52 -0
- package/dist/discord/types/channel.d.ts +557 -0
- package/dist/discord/types/channel.js +88 -0
- package/dist/discord/types/common.d.ts +54 -0
- package/dist/discord/types/common.js +38 -0
- package/dist/discord/types/component.d.ts +775 -0
- package/dist/discord/types/component.js +76 -0
- package/dist/discord/types/emoji.d.ts +74 -0
- package/dist/discord/types/emoji.js +1 -0
- package/dist/discord/types/entitlement.d.ts +84 -0
- package/dist/discord/types/entitlement.js +22 -0
- package/dist/discord/types/gateway-events.d.ts +1148 -0
- package/dist/discord/types/gateway-events.js +54 -0
- package/dist/discord/types/gateway.d.ts +38 -0
- package/dist/discord/types/gateway.js +1 -0
- package/dist/discord/types/guild-scheduled-event.d.ts +257 -0
- package/dist/discord/types/guild-scheduled-event.js +68 -0
- package/dist/discord/types/guild-template.d.ts +51 -0
- package/dist/discord/types/guild-template.js +1 -0
- package/dist/discord/types/guild.d.ts +910 -0
- package/dist/discord/types/guild.js +153 -0
- package/dist/discord/types/index.d.ts +31 -0
- package/dist/discord/types/index.js +31 -0
- package/dist/discord/types/interaction.d.ts +321 -0
- package/dist/discord/types/interaction.js +47 -0
- package/dist/discord/types/invite.d.ts +158 -0
- package/dist/discord/types/invite.js +39 -0
- package/dist/discord/types/message.d.ts +834 -0
- package/dist/discord/types/message.js +159 -0
- package/dist/discord/types/oauth2.d.ts +22 -0
- package/dist/discord/types/oauth2.js +1 -0
- package/dist/discord/types/permissions.d.ts +183 -0
- package/dist/discord/types/permissions.js +118 -0
- package/dist/discord/types/poll.d.ts +106 -0
- package/dist/discord/types/poll.js +8 -0
- package/dist/discord/types/sku.d.ts +47 -0
- package/dist/discord/types/sku.js +26 -0
- package/dist/discord/types/soundboard.d.ts +71 -0
- package/dist/discord/types/soundboard.js +1 -0
- package/dist/discord/types/stage-instance.d.ts +58 -0
- package/dist/discord/types/stage-instance.js +10 -0
- package/dist/discord/types/sticker.d.ts +115 -0
- package/dist/discord/types/sticker.js +20 -0
- package/dist/discord/types/subscription.d.ts +54 -0
- package/dist/discord/types/subscription.js +12 -0
- package/dist/discord/types/team.d.ts +55 -0
- package/dist/discord/types/team.js +21 -0
- package/dist/discord/types/user.d.ts +273 -0
- package/dist/discord/types/user.js +83 -0
- package/dist/discord/types/voice.d.ts +72 -0
- package/dist/discord/types/voice.js +1 -0
- package/dist/discord/types/webhook.d.ts +184 -0
- package/dist/discord/types/webhook.js +12 -0
- package/dist/events/convention.d.ts +25 -0
- package/dist/events/convention.js +97 -0
- package/dist/events/event.d.ts +8 -0
- package/dist/events/event.js +20 -0
- package/dist/events/registry.d.ts +706 -0
- package/dist/events/registry.js +839 -0
- package/dist/events/router.d.ts +45 -0
- package/dist/events/router.js +133 -0
- package/dist/events/types.d.ts +7 -0
- package/dist/events/types.js +48 -0
- package/dist/gateway/errors.d.ts +18 -0
- package/dist/gateway/errors.js +47 -0
- package/dist/gateway/gateway.d.ts +64 -0
- package/dist/gateway/gateway.js +117 -0
- package/dist/gateway/identify-queue.d.ts +25 -0
- package/dist/gateway/identify-queue.js +29 -0
- package/dist/gateway/shard.d.ts +94 -0
- package/dist/gateway/shard.js +414 -0
- package/dist/gateway/state.d.ts +11 -0
- package/dist/gateway/state.js +184 -0
- package/dist/index.d.ts +55 -18
- package/dist/index.js +38 -12
- package/dist/interactions/cooldown.d.ts +33 -0
- package/dist/interactions/cooldown.js +74 -0
- package/dist/interactions/dispatch.d.ts +91 -0
- package/dist/interactions/dispatch.js +110 -0
- package/dist/loader/generated.d.ts +37 -0
- package/dist/loader/generated.js +114 -0
- package/dist/loader/hot.d.ts +13 -0
- package/dist/loader/hot.js +85 -0
- package/dist/loader/loader.d.ts +72 -0
- package/dist/loader/loader.js +135 -0
- package/dist/loader/locate.d.ts +10 -0
- package/dist/loader/locate.js +36 -0
- package/dist/loader/watch.d.ts +17 -0
- package/dist/loader/watch.js +66 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.js +6 -0
- package/dist/messages/commands.d.ts +23 -0
- package/dist/messages/commands.js +123 -0
- package/dist/messages/convention.d.ts +89 -0
- package/dist/messages/convention.js +347 -0
- package/dist/messages/messages.d.ts +219 -0
- package/dist/messages/messages.js +30 -0
- package/dist/messages/phrases.d.ts +23 -0
- package/dist/messages/phrases.js +44 -0
- package/dist/messages/translate.d.ts +100 -0
- package/dist/messages/translate.js +249 -0
- package/dist/presence/convention.d.ts +26 -0
- package/dist/presence/convention.js +133 -0
- package/dist/presence/presence.d.ts +57 -0
- package/dist/presence/presence.js +27 -0
- package/dist/rest/errors.d.ts +55 -0
- package/dist/rest/errors.js +92 -0
- package/dist/rest/rate-limiter.d.ts +46 -0
- package/dist/rest/rate-limiter.js +153 -0
- package/dist/rest/rest.d.ts +63 -0
- package/dist/rest/rest.js +194 -0
- package/dist/structures/base.d.ts +48 -0
- package/dist/structures/base.js +84 -0
- package/dist/structures/channel.d.ts +380 -0
- package/dist/structures/channel.js +627 -0
- package/dist/structures/context.d.ts +30 -0
- package/dist/structures/context.js +7 -0
- package/dist/structures/emoji.d.ts +46 -0
- package/dist/structures/emoji.js +65 -0
- package/dist/structures/entities.d.ts +62 -0
- package/dist/structures/entities.js +226 -0
- package/dist/structures/guild.d.ts +348 -0
- package/dist/structures/guild.js +466 -0
- package/dist/structures/interaction.d.ts +197 -0
- package/dist/structures/interaction.js +396 -0
- package/dist/structures/invite.d.ts +38 -0
- package/dist/structures/invite.js +57 -0
- package/dist/structures/known.d.ts +28 -0
- package/dist/structures/known.js +58 -0
- package/dist/structures/member.d.ts +140 -0
- package/dist/structures/member.js +260 -0
- package/dist/structures/message.d.ts +197 -0
- package/dist/structures/message.js +268 -0
- package/dist/structures/payload.d.ts +91 -0
- package/dist/structures/payload.js +184 -0
- package/dist/structures/role.d.ts +69 -0
- package/dist/structures/role.js +112 -0
- package/dist/structures/user.d.ts +59 -0
- package/dist/structures/user.js +94 -0
- package/dist/structures/webhook.d.ts +46 -0
- package/dist/structures/webhook.js +79 -0
- package/dist/tasks/convention.d.ts +26 -0
- package/dist/tasks/convention.js +78 -0
- package/dist/tasks/schedule.d.ts +54 -0
- package/dist/tasks/schedule.js +236 -0
- package/dist/tasks/scheduler.d.ts +33 -0
- package/dist/tasks/scheduler.js +107 -0
- package/dist/tasks/task.d.ts +76 -0
- package/dist/tasks/task.js +28 -0
- package/dist/util/case.d.ts +13 -0
- package/dist/util/case.js +54 -0
- package/dist/util/duration.d.ts +8 -0
- package/dist/util/duration.js +29 -0
- package/dist/util/websocket.d.ts +9 -0
- package/dist/util/websocket.js +31 -0
- package/dist/voice/connection.d.ts +90 -0
- package/dist/voice/connection.js +494 -0
- package/dist/voice/dave.d.ts +109 -0
- package/dist/voice/dave.js +245 -0
- package/dist/voice/opus.d.ts +19 -0
- package/dist/voice/opus.js +273 -0
- package/dist/voice/session.d.ts +55 -0
- package/dist/voice/session.js +405 -0
- package/dist/voice/source.d.ts +22 -0
- package/dist/voice/source.js +195 -0
- package/dist/voice/transport.d.ts +43 -0
- package/dist/voice/transport.js +140 -0
- package/dist/voice/usage.d.ts +4 -0
- package/dist/voice/usage.js +33 -0
- package/package.json +26 -7
- package/vendor/dave/LICENSE +231 -0
- package/vendor/dave/libdave.d.mts +99 -0
- package/vendor/dave/libdave.mjs +2 -0
- package/vendor/dave/libdave.wasm +0 -0
- package/dist/components.d.ts +0 -97
- package/dist/components.js +0 -260
- package/dist/context.d.ts +0 -72
- package/dist/context.js +0 -273
- package/dist/discord/client.d.ts +0 -35
- package/dist/discord/client.js +0 -66
- package/dist/discord/constants.d.ts +0 -49
- package/dist/discord/constants.js +0 -61
- package/dist/discord/events.d.ts +0 -129
- package/dist/discord/events.js +0 -102
- package/dist/discord/gateway.d.ts +0 -61
- package/dist/discord/gateway.js +0 -320
- package/dist/discord/payloads.d.ts +0 -275
- package/dist/discord/payloads.js +0 -3
- package/dist/discord/presence.d.ts +0 -42
- package/dist/discord/presence.js +0 -51
- package/dist/discord/rest.d.ts +0 -49
- package/dist/discord/rest.js +0 -271
- package/dist/discord/types.d.ts +0 -334
- package/dist/discord/types.js +0 -4
- package/dist/events.d.ts +0 -94
- package/dist/events.js +0 -255
- package/dist/presence.d.ts +0 -92
- package/dist/presence.js +0 -250
- package/dist/structures/Channel.d.ts +0 -110
- package/dist/structures/Channel.js +0 -171
- package/dist/structures/Guild.d.ts +0 -70
- package/dist/structures/Guild.js +0 -105
- package/dist/structures/Interaction.d.ts +0 -113
- package/dist/structures/Interaction.js +0 -274
- package/dist/structures/Member.d.ts +0 -70
- package/dist/structures/Member.js +0 -124
- package/dist/structures/Message.d.ts +0 -87
- package/dist/structures/Message.js +0 -182
- package/dist/structures/Reaction.d.ts +0 -38
- package/dist/structures/Reaction.js +0 -64
- package/dist/structures/Role.d.ts +0 -43
- package/dist/structures/Role.js +0 -77
- package/dist/structures/User.d.ts +0 -41
- package/dist/structures/User.js +0 -79
- package/dist/structures/utils.d.ts +0 -72
- package/dist/structures/utils.js +0 -59
- package/dist/utils/errors.d.ts +0 -5
- package/dist/utils/errors.js +0 -28
- package/dist/utils/modules.d.ts +0 -39
- package/dist/utils/modules.js +0 -156
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// One session with a voice server: its WebSocket (voice gateway v8), its UDP
|
|
2
|
+
// socket, the transport key and the DAVE state. A session lives as long as
|
|
3
|
+
// its token: moving to another channel or losing it makes a new one, while
|
|
4
|
+
// the voice connection users hold stays the same object.
|
|
5
|
+
// https://docs.discord.com/developers/topics/voice-connections
|
|
6
|
+
import { createSocket } from 'node:dgram';
|
|
7
|
+
import { VoiceCloseCode, VoiceOpcode } from '../discord/codes.js';
|
|
8
|
+
import { onSocketEnd } from '../util/websocket.js';
|
|
9
|
+
import { DaveSession } from './dave.js';
|
|
10
|
+
import { pickMode, rtpHeader, TRANSPORT_MODES, TransportEncryptor, } from './transport.js';
|
|
11
|
+
export const VOICE_GATEWAY_VERSION = 8;
|
|
12
|
+
/** The close codes after which Discord says not to reconnect. */
|
|
13
|
+
const ENDED = [
|
|
14
|
+
VoiceCloseCode.Disconnected,
|
|
15
|
+
VoiceCloseCode.DisconnectedRateLimited,
|
|
16
|
+
VoiceCloseCode.DisconnectedCallTerminated,
|
|
17
|
+
];
|
|
18
|
+
/** The session is gone: a new one is needed, from the main gateway. */
|
|
19
|
+
const LOST = [
|
|
20
|
+
VoiceCloseCode.SessionNoLongerValid,
|
|
21
|
+
VoiceCloseCode.SessionTimeout,
|
|
22
|
+
VoiceCloseCode.ServerNotFound,
|
|
23
|
+
];
|
|
24
|
+
/** What these codes mean for the developer. */
|
|
25
|
+
const REFUSED = {
|
|
26
|
+
[VoiceCloseCode.UnknownOpcode]: 'it received an opcode it does not know',
|
|
27
|
+
[VoiceCloseCode.FailedToDecodePayload]: 'it could not read what it received',
|
|
28
|
+
[VoiceCloseCode.NotAuthenticated]: 'it was sent something before Identify',
|
|
29
|
+
[VoiceCloseCode.AuthenticationFailed]: 'the voice token was refused',
|
|
30
|
+
[VoiceCloseCode.AlreadyAuthenticated]: 'it was identified twice',
|
|
31
|
+
[VoiceCloseCode.UnknownProtocol]: 'it does not know the protocol asked',
|
|
32
|
+
[VoiceCloseCode.UnknownEncryptionMode]: 'it does not know the encryption mode asked',
|
|
33
|
+
[VoiceCloseCode.E2eeDaveProtocolRequired]: 'the channel requires end-to-end encryption (DAVE)',
|
|
34
|
+
[VoiceCloseCode.BadRequest]: 'it received a malformed request',
|
|
35
|
+
};
|
|
36
|
+
/** Five frames of silence, sent before stopping. */
|
|
37
|
+
const SILENCE = Buffer.from([0xf8, 0xff, 0xfe]);
|
|
38
|
+
export class VoiceSession {
|
|
39
|
+
server;
|
|
40
|
+
#options;
|
|
41
|
+
dave;
|
|
42
|
+
#ws = null;
|
|
43
|
+
#udp = null;
|
|
44
|
+
#remote = null;
|
|
45
|
+
#ssrc = 0;
|
|
46
|
+
#encryptor = null;
|
|
47
|
+
#sequence = Math.floor(Math.random() * 0xffff);
|
|
48
|
+
#timestamp = Math.floor(Math.random() * 0xffffffff);
|
|
49
|
+
#seqAck = -1;
|
|
50
|
+
#heartbeat = null;
|
|
51
|
+
#acked = true;
|
|
52
|
+
#resuming = false;
|
|
53
|
+
#closed = false;
|
|
54
|
+
#speaking = false;
|
|
55
|
+
#ready = null;
|
|
56
|
+
#retries = 0;
|
|
57
|
+
/** Packets handed to the socket and not sent yet. */
|
|
58
|
+
#sending = 0;
|
|
59
|
+
#afterSends = null;
|
|
60
|
+
constructor(server, options) {
|
|
61
|
+
this.server = server;
|
|
62
|
+
this.#options = options;
|
|
63
|
+
this.dave = new DaveSession(options.dave, {
|
|
64
|
+
userId: server.userId,
|
|
65
|
+
channelId: server.channelId,
|
|
66
|
+
warn: options.warn,
|
|
67
|
+
transport: {
|
|
68
|
+
json: (op, data) => this.#send(op, data),
|
|
69
|
+
binary: (op, payload) => this.#sendBinary(op, payload),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Resolves once the session can send audio. */
|
|
74
|
+
connect() {
|
|
75
|
+
const timeout = this.#options.timeout ?? 15_000;
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const timer = setTimeout(() => reject(new Error(`The voice server did not answer within ${timeout / 1000} s.`)), timeout);
|
|
78
|
+
this.#ready = {
|
|
79
|
+
resolve: () => {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
resolve();
|
|
82
|
+
},
|
|
83
|
+
reject: error => {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
reject(error);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
this.#open();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Sends one Opus packet that plays `samples` samples. */
|
|
92
|
+
sendOpus(packet, samples) {
|
|
93
|
+
const encryptor = this.#encryptor;
|
|
94
|
+
if (!encryptor || !this.#udp || !this.#remote || this.#closed)
|
|
95
|
+
return;
|
|
96
|
+
if (!this.dave.canSend) {
|
|
97
|
+
// The call is end-to-end encrypted and the bot has no key yet: what
|
|
98
|
+
// it would send could not be heard. Time goes on all the same.
|
|
99
|
+
this.#timestamp = (this.#timestamp + samples) >>> 0;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!this.#speaking)
|
|
103
|
+
this.setSpeaking(true);
|
|
104
|
+
const frame = this.dave.encrypt(packet);
|
|
105
|
+
const header = rtpHeader(this.#sequence, this.#timestamp, this.#ssrc);
|
|
106
|
+
this.#sequence = (this.#sequence + 1) & 0xffff;
|
|
107
|
+
this.#timestamp = (this.#timestamp + samples) >>> 0;
|
|
108
|
+
this.#sending++;
|
|
109
|
+
this.#udp.send(encryptor.seal(header, frame), this.#remote.port, this.#remote.ip, () => {
|
|
110
|
+
this.#sending--;
|
|
111
|
+
if (this.#sending === 0)
|
|
112
|
+
this.#afterSends?.();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** The five frames of silence Discord asks for before a pause. */
|
|
116
|
+
sendSilence() {
|
|
117
|
+
for (let i = 0; i < 5; i++)
|
|
118
|
+
this.sendOpus(SILENCE, 960);
|
|
119
|
+
}
|
|
120
|
+
/** Speaking (5): needed before audio, and shown in Discord. */
|
|
121
|
+
setSpeaking(speaking) {
|
|
122
|
+
if (speaking === this.#speaking)
|
|
123
|
+
return;
|
|
124
|
+
this.#speaking = speaking;
|
|
125
|
+
this.#send(VoiceOpcode.Speaking, {
|
|
126
|
+
speaking: speaking ? 1 : 0,
|
|
127
|
+
delay: 0,
|
|
128
|
+
ssrc: this.#ssrc,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Closes the session for good. */
|
|
132
|
+
close() {
|
|
133
|
+
if (this.#closed)
|
|
134
|
+
return;
|
|
135
|
+
this.#closed = true;
|
|
136
|
+
this.#stopHeartbeat();
|
|
137
|
+
this.#ws?.close(1000);
|
|
138
|
+
this.#ws = null;
|
|
139
|
+
// What was handed to the socket (the silence that ends a song) still
|
|
140
|
+
// leaves before it closes.
|
|
141
|
+
const udp = this.#udp;
|
|
142
|
+
this.#udp = null;
|
|
143
|
+
if (udp && this.#sending > 0)
|
|
144
|
+
this.#afterSends = () => udp.close();
|
|
145
|
+
else
|
|
146
|
+
udp?.close();
|
|
147
|
+
this.dave.close();
|
|
148
|
+
this.#ready?.reject(new Error('The voice connection was closed.'));
|
|
149
|
+
this.#ready = null;
|
|
150
|
+
}
|
|
151
|
+
#open() {
|
|
152
|
+
const scheme = this.#options.secure === false ? 'ws' : 'wss';
|
|
153
|
+
const ws = new WebSocket(`${scheme}://${this.server.endpoint}/?v=${VOICE_GATEWAY_VERSION}`);
|
|
154
|
+
ws.binaryType = 'arraybuffer';
|
|
155
|
+
this.#ws = ws;
|
|
156
|
+
ws.addEventListener('message', event => {
|
|
157
|
+
if (ws !== this.#ws)
|
|
158
|
+
return;
|
|
159
|
+
try {
|
|
160
|
+
if (typeof event.data === 'string')
|
|
161
|
+
this.#onJson(event.data);
|
|
162
|
+
else
|
|
163
|
+
this.#onBinary(new Uint8Array(event.data));
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
this.#options.warn(`a message of the voice server could not be read: ${error.message}`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
onSocketEnd(ws, code => {
|
|
170
|
+
if (ws === this.#ws)
|
|
171
|
+
this.#onClose(code);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
#onJson(text) {
|
|
175
|
+
const { op, d, seq } = JSON.parse(text);
|
|
176
|
+
if (typeof seq === 'number')
|
|
177
|
+
this.#seqAck = seq;
|
|
178
|
+
const data = d ?? {};
|
|
179
|
+
switch (op) {
|
|
180
|
+
case VoiceOpcode.Hello:
|
|
181
|
+
this.#startHeartbeat(data.heartbeat_interval);
|
|
182
|
+
if (this.#resuming) {
|
|
183
|
+
this.#send(VoiceOpcode.Resume, {
|
|
184
|
+
server_id: this.server.guildId,
|
|
185
|
+
session_id: this.server.sessionId,
|
|
186
|
+
token: this.server.token,
|
|
187
|
+
seq_ack: this.#seqAck,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
this.#send(VoiceOpcode.Identify, {
|
|
192
|
+
server_id: this.server.guildId,
|
|
193
|
+
user_id: this.server.userId,
|
|
194
|
+
session_id: this.server.sessionId,
|
|
195
|
+
token: this.server.token,
|
|
196
|
+
max_dave_protocol_version: this.dave.maxVersion,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
case VoiceOpcode.Ready:
|
|
201
|
+
void this.#selectProtocol(data);
|
|
202
|
+
break;
|
|
203
|
+
case VoiceOpcode.SessionDescription: {
|
|
204
|
+
const { mode, secret_key: key, dave_protocol_version: version, } = data;
|
|
205
|
+
const chosen = pickMode([mode]);
|
|
206
|
+
if (!chosen) {
|
|
207
|
+
this.#fail(new Error(`The voice server chose an encryption mode chapterjs does not know: ${mode}.`));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
this.#encryptor = new TransportEncryptor(chosen, Uint8Array.from(key));
|
|
211
|
+
this.dave.start(version ?? 0);
|
|
212
|
+
this.#retries = 0;
|
|
213
|
+
this.#ready?.resolve();
|
|
214
|
+
this.#ready = null;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case VoiceOpcode.HeartbeatAck:
|
|
218
|
+
this.#acked = true;
|
|
219
|
+
break;
|
|
220
|
+
case VoiceOpcode.Resumed:
|
|
221
|
+
this.#resuming = false;
|
|
222
|
+
this.#retries = 0;
|
|
223
|
+
break;
|
|
224
|
+
case VoiceOpcode.ClientsConnect:
|
|
225
|
+
this.dave.clientsConnect(data.user_ids ?? []);
|
|
226
|
+
break;
|
|
227
|
+
case VoiceOpcode.ClientDisconnect:
|
|
228
|
+
this.dave.clientDisconnect(data.user_id);
|
|
229
|
+
break;
|
|
230
|
+
case VoiceOpcode.DavePrepareTransition:
|
|
231
|
+
this.dave.prepareTransition(data.transition_id, data.protocol_version);
|
|
232
|
+
break;
|
|
233
|
+
case VoiceOpcode.DaveExecuteTransition:
|
|
234
|
+
this.dave.executeTransition(data.transition_id);
|
|
235
|
+
break;
|
|
236
|
+
case VoiceOpcode.DavePrepareEpoch:
|
|
237
|
+
this.dave.prepareEpoch(data.epoch, data.protocol_version);
|
|
238
|
+
break;
|
|
239
|
+
default:
|
|
240
|
+
// Speaking of others, video, and what the bot does not use.
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Binary messages: a sequence number, an opcode, then the payload. */
|
|
245
|
+
#onBinary(bytes) {
|
|
246
|
+
if (bytes.length < 3)
|
|
247
|
+
return;
|
|
248
|
+
this.#seqAck = (bytes[0] << 8) | bytes[1];
|
|
249
|
+
const op = bytes[2];
|
|
250
|
+
const payload = bytes.subarray(3);
|
|
251
|
+
const transition = () => (payload[0] << 8) | payload[1];
|
|
252
|
+
switch (op) {
|
|
253
|
+
case VoiceOpcode.DaveMlsExternalSender:
|
|
254
|
+
this.dave.externalSender(payload);
|
|
255
|
+
break;
|
|
256
|
+
case VoiceOpcode.DaveMlsProposals:
|
|
257
|
+
this.dave.proposals(payload);
|
|
258
|
+
break;
|
|
259
|
+
case VoiceOpcode.DaveMlsAnnounceCommitTransition:
|
|
260
|
+
this.dave.announceCommit(transition(), payload.subarray(2));
|
|
261
|
+
break;
|
|
262
|
+
case VoiceOpcode.DaveMlsWelcome:
|
|
263
|
+
this.dave.welcome(transition(), payload.subarray(2));
|
|
264
|
+
break;
|
|
265
|
+
default:
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async #selectProtocol(ready) {
|
|
270
|
+
const mode = pickMode(ready.modes);
|
|
271
|
+
if (!mode) {
|
|
272
|
+
this.#fail(new Error(`The voice server offers none of the encryption modes chapterjs speaks (${TRANSPORT_MODES.join(', ')}): it offered ${ready.modes.join(', ')}.`));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.#ssrc = ready.ssrc;
|
|
276
|
+
this.dave.setSsrc(ready.ssrc);
|
|
277
|
+
this.#remote = { ip: ready.ip, port: ready.port };
|
|
278
|
+
try {
|
|
279
|
+
const self = await this.#discover(ready);
|
|
280
|
+
this.#send(VoiceOpcode.SelectProtocol, {
|
|
281
|
+
protocol: 'udp',
|
|
282
|
+
data: { address: self.ip, port: self.port, mode },
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
this.#fail(error);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* IP Discovery: asks the voice server which address and port it sees the
|
|
291
|
+
* bot from, through the socket audio will go through.
|
|
292
|
+
* https://docs.discord.com/developers/topics/voice-connections#ip-discovery
|
|
293
|
+
*/
|
|
294
|
+
#discover(ready) {
|
|
295
|
+
this.#udp?.close();
|
|
296
|
+
const udp = createSocket('udp4');
|
|
297
|
+
this.#udp = udp;
|
|
298
|
+
return new Promise((resolve, reject) => {
|
|
299
|
+
const timer = setTimeout(() => {
|
|
300
|
+
udp.off('message', onMessage);
|
|
301
|
+
reject(new Error(`The voice server did not answer over UDP (${ready.ip}:${ready.port}): a firewall may block it.`));
|
|
302
|
+
}, 5000);
|
|
303
|
+
const onMessage = (message) => {
|
|
304
|
+
if (message.length < 74 || message.readUInt16BE(0) !== 2)
|
|
305
|
+
return;
|
|
306
|
+
clearTimeout(timer);
|
|
307
|
+
udp.off('message', onMessage);
|
|
308
|
+
const end = message.indexOf(0, 8);
|
|
309
|
+
resolve({
|
|
310
|
+
ip: message.toString('utf8', 8, end > 8 && end < 72 ? end : 72),
|
|
311
|
+
port: message.readUInt16BE(72),
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
udp.on('message', onMessage);
|
|
315
|
+
udp.on('error', error => {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
reject(error);
|
|
318
|
+
});
|
|
319
|
+
const request = Buffer.alloc(74);
|
|
320
|
+
request.writeUInt16BE(1, 0);
|
|
321
|
+
request.writeUInt16BE(70, 2);
|
|
322
|
+
request.writeUInt32BE(ready.ssrc >>> 0, 4);
|
|
323
|
+
udp.send(request, ready.port, ready.ip);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
#onClose(code) {
|
|
327
|
+
this.#stopHeartbeat();
|
|
328
|
+
this.#ws = null;
|
|
329
|
+
if (this.#closed)
|
|
330
|
+
return;
|
|
331
|
+
if (ENDED.includes(code))
|
|
332
|
+
return this.#end({ type: 'ended', code });
|
|
333
|
+
if (LOST.includes(code))
|
|
334
|
+
return this.#end({ type: 'lost', code });
|
|
335
|
+
const reason = REFUSED[code];
|
|
336
|
+
if (reason)
|
|
337
|
+
return this.#end({ type: 'refused', code, reason });
|
|
338
|
+
// Anything else (a network error, a crash of the voice server, a
|
|
339
|
+
// missing heartbeat ACK): resume, waiting longer each time.
|
|
340
|
+
if (this.#ready || !this.#encryptor) {
|
|
341
|
+
// Not even ready yet: there is nothing to resume.
|
|
342
|
+
return this.#end({ type: 'lost', code });
|
|
343
|
+
}
|
|
344
|
+
this.#resuming = true;
|
|
345
|
+
const delay = Math.min(1000 * 2 ** this.#retries, 30_000);
|
|
346
|
+
this.#retries++;
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
if (!this.#closed)
|
|
349
|
+
this.#open();
|
|
350
|
+
}, delay).unref();
|
|
351
|
+
}
|
|
352
|
+
#end(end) {
|
|
353
|
+
const ready = this.#ready;
|
|
354
|
+
this.close();
|
|
355
|
+
if (ready) {
|
|
356
|
+
ready.reject(new Error(end.type === 'refused'
|
|
357
|
+
? `The voice server refused the bot (${end.code}): ${end.reason}.`
|
|
358
|
+
: `The voice server closed the connection (${end.code}).`));
|
|
359
|
+
}
|
|
360
|
+
this.#options.onEnd(end);
|
|
361
|
+
}
|
|
362
|
+
#fail(error) {
|
|
363
|
+
const ready = this.#ready;
|
|
364
|
+
this.#ready = null;
|
|
365
|
+
this.close();
|
|
366
|
+
ready?.reject(error);
|
|
367
|
+
if (!ready)
|
|
368
|
+
this.#options.onEnd({ type: 'lost', code: 0 });
|
|
369
|
+
}
|
|
370
|
+
#startHeartbeat(interval) {
|
|
371
|
+
this.#stopHeartbeat();
|
|
372
|
+
this.#acked = true;
|
|
373
|
+
this.#heartbeat = setInterval(() => {
|
|
374
|
+
if (!this.#acked) {
|
|
375
|
+
// No answer to the last one: the connection is dead.
|
|
376
|
+
this.#ws?.close(4000);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this.#acked = false;
|
|
380
|
+
this.#send(VoiceOpcode.Heartbeat, {
|
|
381
|
+
t: Date.now(),
|
|
382
|
+
seq_ack: this.#seqAck,
|
|
383
|
+
});
|
|
384
|
+
}, interval);
|
|
385
|
+
this.#heartbeat.unref();
|
|
386
|
+
}
|
|
387
|
+
#stopHeartbeat() {
|
|
388
|
+
if (this.#heartbeat)
|
|
389
|
+
clearInterval(this.#heartbeat);
|
|
390
|
+
this.#heartbeat = null;
|
|
391
|
+
}
|
|
392
|
+
#send(op, d) {
|
|
393
|
+
if (this.#ws?.readyState === WebSocket.OPEN) {
|
|
394
|
+
this.#ws.send(JSON.stringify({ op, d }));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
#sendBinary(op, payload) {
|
|
398
|
+
if (this.#ws?.readyState !== WebSocket.OPEN)
|
|
399
|
+
return;
|
|
400
|
+
const message = new Uint8Array(payload.length + 1);
|
|
401
|
+
message[0] = op;
|
|
402
|
+
message.set(payload, 1);
|
|
403
|
+
this.#ws.send(message);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AssetFile } from '../assets/asset.js';
|
|
2
|
+
import type { Attachment } from '../structures/message.js';
|
|
3
|
+
/**
|
|
4
|
+
* What `play()` takes: a file of `public/` (`asset('music/intro.ogg')`), a
|
|
5
|
+
* file sent on Discord (the `attachment` option of a command, or one of
|
|
6
|
+
* `message.attachments`), or a link starting with `https://`.
|
|
7
|
+
*/
|
|
8
|
+
export type AudioSource = AssetFile | Attachment | string;
|
|
9
|
+
/** Opus packets being read, and how to stop reading them. */
|
|
10
|
+
export interface OpusStream {
|
|
11
|
+
readonly packets: AsyncIterable<Buffer>;
|
|
12
|
+
/** What is played, as the developer named it. */
|
|
13
|
+
readonly name: string;
|
|
14
|
+
close(): void;
|
|
15
|
+
}
|
|
16
|
+
/** The container of a file, from its first bytes. */
|
|
17
|
+
export declare function sniff(head: Buffer): 'ogg' | 'webm' | null;
|
|
18
|
+
/**
|
|
19
|
+
* Opens what is to be played. Fails before anything is sent when the file
|
|
20
|
+
* is missing, or when ffmpeg is needed and is not installed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function openAudio(source: AudioSource): Promise<OpusStream>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// What `play()` takes, turned into Opus packets: a file of `public/`, a
|
|
2
|
+
// link or a file sent on Discord that is already Opus is read as is;
|
|
3
|
+
// anything else is converted by ffmpeg, when it is installed.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { createReadStream } from 'node:fs';
|
|
6
|
+
import { open } from 'node:fs/promises';
|
|
7
|
+
import { assetPath, isAssetFile, missingAsset, } from '../assets/asset.js';
|
|
8
|
+
import { oggOpusPackets, webmOpusPackets } from './opus.js';
|
|
9
|
+
/** How much of a file tells what it holds. */
|
|
10
|
+
const HEAD = 4096;
|
|
11
|
+
/** The container of a file, from its first bytes. */
|
|
12
|
+
export function sniff(head) {
|
|
13
|
+
if (head.toString('latin1', 0, 4) === 'OggS' &&
|
|
14
|
+
head.includes('OpusHead', 0, 'latin1')) {
|
|
15
|
+
return 'ogg';
|
|
16
|
+
}
|
|
17
|
+
if (head.readUInt32BE(0) === 0x1a45dfa3 && head.includes('A_OPUS')) {
|
|
18
|
+
return 'webm';
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Opens what is to be played. Fails before anything is sent when the file
|
|
24
|
+
* is missing, or when ffmpeg is needed and is not installed.
|
|
25
|
+
*/
|
|
26
|
+
export async function openAudio(source) {
|
|
27
|
+
if (isAssetFile(source)) {
|
|
28
|
+
const path = assetPath(source);
|
|
29
|
+
const name = `public/${source.path}`;
|
|
30
|
+
let head;
|
|
31
|
+
try {
|
|
32
|
+
const handle = await open(path, 'r');
|
|
33
|
+
try {
|
|
34
|
+
const buffer = Buffer.alloc(4096);
|
|
35
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
36
|
+
head = buffer.subarray(0, bytesRead);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await handle.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw missingAsset(source, error);
|
|
44
|
+
}
|
|
45
|
+
const container = head.length >= 4 ? sniff(head) : null;
|
|
46
|
+
if (container) {
|
|
47
|
+
const stream = createReadStream(path);
|
|
48
|
+
// Reading reports what goes wrong; a stream never read must not.
|
|
49
|
+
stream.on('error', () => { });
|
|
50
|
+
const demux = container === 'ogg' ? oggOpusPackets : webmOpusPackets;
|
|
51
|
+
return {
|
|
52
|
+
name,
|
|
53
|
+
packets: demux(stream),
|
|
54
|
+
close: () => stream.destroy(),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return ffmpeg(path, name);
|
|
58
|
+
}
|
|
59
|
+
if (isAttachment(source))
|
|
60
|
+
return openLink(source.url, source.filename);
|
|
61
|
+
if (typeof source === 'string' && /^https?:\/\//i.test(source)) {
|
|
62
|
+
return openLink(source, source);
|
|
63
|
+
}
|
|
64
|
+
throw new TypeError(`play() takes a file of public/, like play(asset('music/intro.ogg')), a file sent on Discord, or a link starting with https://. Got ${JSON.stringify(source) ?? String(source)}.`);
|
|
65
|
+
}
|
|
66
|
+
/** A file sent on Discord, as a command option or a message gives it. */
|
|
67
|
+
function isAttachment(value) {
|
|
68
|
+
return (typeof value === 'object' &&
|
|
69
|
+
value !== null &&
|
|
70
|
+
typeof value.url === 'string' &&
|
|
71
|
+
typeof value.filename === 'string');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A link: downloaded as it plays. Its first bytes say whether it is Opus,
|
|
75
|
+
* read as is, or something ffmpeg converts.
|
|
76
|
+
*/
|
|
77
|
+
async function openLink(url, name) {
|
|
78
|
+
let response;
|
|
79
|
+
try {
|
|
80
|
+
response = await fetch(url);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw new Error(`${name} could not be downloaded: ${error.message}.`);
|
|
84
|
+
}
|
|
85
|
+
if (!response.ok || !response.body) {
|
|
86
|
+
throw new Error(`${name} could not be downloaded: the server answered ${response.status}.`);
|
|
87
|
+
}
|
|
88
|
+
// A page (YouTube, a site): what plays is not at this address.
|
|
89
|
+
if (response.headers.get('content-type')?.startsWith('text/html')) {
|
|
90
|
+
void response.body.cancel().catch(() => { });
|
|
91
|
+
throw new Error(`${name} is a web page, not a sound: give the link of an audio file or of a stream, or send the file.`);
|
|
92
|
+
}
|
|
93
|
+
const reader = response.body.getReader();
|
|
94
|
+
const start = [];
|
|
95
|
+
let length = 0;
|
|
96
|
+
let ended = false;
|
|
97
|
+
while (length < HEAD) {
|
|
98
|
+
const next = await reader.read();
|
|
99
|
+
if (next.done) {
|
|
100
|
+
ended = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
start.push(next.value);
|
|
104
|
+
length += next.value.length;
|
|
105
|
+
}
|
|
106
|
+
const head = Buffer.concat(start);
|
|
107
|
+
const container = head.length >= 4 ? sniff(head) : null;
|
|
108
|
+
if (!container) {
|
|
109
|
+
// ffmpeg downloads it itself.
|
|
110
|
+
void reader.cancel().catch(() => { });
|
|
111
|
+
return ffmpeg(url, name);
|
|
112
|
+
}
|
|
113
|
+
let closed = false;
|
|
114
|
+
const body = async function* () {
|
|
115
|
+
yield head;
|
|
116
|
+
if (ended)
|
|
117
|
+
return;
|
|
118
|
+
while (!closed) {
|
|
119
|
+
const next = await reader.read();
|
|
120
|
+
if (next.done)
|
|
121
|
+
return;
|
|
122
|
+
yield next.value;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const demux = container === 'ogg' ? oggOpusPackets : webmOpusPackets;
|
|
126
|
+
return {
|
|
127
|
+
name,
|
|
128
|
+
packets: demux(body()),
|
|
129
|
+
close: () => {
|
|
130
|
+
closed = true;
|
|
131
|
+
void reader.cancel().catch(() => { });
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Converts anything ffmpeg reads into Ogg Opus, 48 kHz stereo in frames
|
|
137
|
+
* of 20 ms: what Discord expects.
|
|
138
|
+
*/
|
|
139
|
+
async function ffmpeg(input, name) {
|
|
140
|
+
const child = spawn('ffmpeg', [
|
|
141
|
+
'-hide_banner',
|
|
142
|
+
'-loglevel',
|
|
143
|
+
'error',
|
|
144
|
+
'-i',
|
|
145
|
+
input,
|
|
146
|
+
'-vn',
|
|
147
|
+
'-c:a',
|
|
148
|
+
'libopus',
|
|
149
|
+
'-ar',
|
|
150
|
+
'48000',
|
|
151
|
+
'-ac',
|
|
152
|
+
'2',
|
|
153
|
+
'-b:a',
|
|
154
|
+
'128k',
|
|
155
|
+
'-frame_duration',
|
|
156
|
+
'20',
|
|
157
|
+
'-f',
|
|
158
|
+
'ogg',
|
|
159
|
+
'pipe:1',
|
|
160
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
161
|
+
await new Promise((resolve, reject) => {
|
|
162
|
+
child.once('spawn', resolve);
|
|
163
|
+
child.once('error', error => {
|
|
164
|
+
reject(error.code === 'ENOENT'
|
|
165
|
+
? new Error(`${name} is not an Opus file (.ogg or .webm), and ffmpeg is not installed to convert it. Install ffmpeg (https://ffmpeg.org/download.html), or convert the file to .ogg Opus.`)
|
|
166
|
+
: error);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
let errors = '';
|
|
170
|
+
child.stderr.setEncoding('utf8');
|
|
171
|
+
child.stderr.on('data', (text) => {
|
|
172
|
+
errors = (errors + text).slice(-2000);
|
|
173
|
+
});
|
|
174
|
+
const exited = new Promise(resolve => child.once('close', code => resolve(code)));
|
|
175
|
+
const packets = (async function* () {
|
|
176
|
+
let count = 0;
|
|
177
|
+
for await (const packet of oggOpusPackets(child.stdout)) {
|
|
178
|
+
count++;
|
|
179
|
+
yield packet;
|
|
180
|
+
}
|
|
181
|
+
const code = await exited;
|
|
182
|
+
if (code !== 0 && code !== null && count === 0) {
|
|
183
|
+
const reason = errors.trim().split('\n').at(-1) ?? `exit code ${code}`;
|
|
184
|
+
throw new Error(`ffmpeg could not read ${name}: ${reason}`);
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
return {
|
|
188
|
+
name,
|
|
189
|
+
packets,
|
|
190
|
+
close: () => {
|
|
191
|
+
if (child.exitCode === null)
|
|
192
|
+
child.kill();
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The transport encryption modes, best first. */
|
|
2
|
+
export declare const TRANSPORT_MODES: readonly ['aead_aes256_gcm_rtpsize', 'aead_xchacha20_poly1305_rtpsize'];
|
|
3
|
+
export type TransportMode = (typeof TRANSPORT_MODES)[number];
|
|
4
|
+
/**
|
|
5
|
+
* The mode to use among the ones the voice server offers: AES256-GCM when
|
|
6
|
+
* it is there (Discord asks to prefer it), XChaCha20-Poly1305 otherwise
|
|
7
|
+
* (always offered); `null` when neither is.
|
|
8
|
+
* https://docs.discord.com/developers/topics/voice-connections#transport-encryption-modes
|
|
9
|
+
*/
|
|
10
|
+
export declare function pickMode(offered: readonly string[]): TransportMode | null;
|
|
11
|
+
/** Audio sampled at 48 kHz: what an RTP timestamp counts. */
|
|
12
|
+
export declare const SAMPLE_RATE = 48000;
|
|
13
|
+
/**
|
|
14
|
+
* The RTP header of a voice packet: version 2, payload type 0x78, then
|
|
15
|
+
* sequence, timestamp and SSRC, big endian.
|
|
16
|
+
* https://docs.discord.com/developers/topics/voice-connections#transport-encryption-modes-voice-packet-structure
|
|
17
|
+
*/
|
|
18
|
+
export declare function rtpHeader(sequence: number, timestamp: number, ssrc: number): Buffer;
|
|
19
|
+
/**
|
|
20
|
+
* HChaCha20: derives the key XChaCha20 uses for a 24-byte nonce, from the
|
|
21
|
+
* key and the first 16 bytes of that nonce.
|
|
22
|
+
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.2
|
|
23
|
+
*/
|
|
24
|
+
export declare function hchacha20(key: Uint8Array, nonce16: Uint8Array): Buffer;
|
|
25
|
+
/**
|
|
26
|
+
* XChaCha20-Poly1305: ChaCha20-Poly1305 (which Node has) with the key
|
|
27
|
+
* HChaCha20 derives and the last 8 bytes of the 24-byte nonce.
|
|
28
|
+
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#section-2.3
|
|
29
|
+
*/
|
|
30
|
+
export declare function xchacha20poly1305Seal(key: Uint8Array, nonce24: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Buffer;
|
|
31
|
+
/**
|
|
32
|
+
* Encrypts packets for one voice session. In the `rtpsize` modes the RTP
|
|
33
|
+
* header stays clear and authenticated, and a 32-bit counter, appended to
|
|
34
|
+
* the packet, is the nonce: the first 4 bytes of a nonce that is zero
|
|
35
|
+
* otherwise.
|
|
36
|
+
*/
|
|
37
|
+
export declare class TransportEncryptor {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(mode: TransportMode, key: Uint8Array);
|
|
40
|
+
get mode(): TransportMode;
|
|
41
|
+
/** A whole packet: the clear header, the sealed audio, the counter. */
|
|
42
|
+
seal(header: Buffer, payload: Uint8Array): Buffer;
|
|
43
|
+
}
|