@m7mdxzx1/discord-video-strem 0.0.1

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.
Files changed (89) hide show
  1. package/README.md +294 -0
  2. package/dist/client/GatewayEvents.d.ts +27 -0
  3. package/dist/client/GatewayEvents.js +1 -0
  4. package/dist/client/GatewayOpCodes.d.ts +40 -0
  5. package/dist/client/GatewayOpCodes.js +41 -0
  6. package/dist/client/Streamer.d.ts +41 -0
  7. package/dist/client/Streamer.js +189 -0
  8. package/dist/client/encryptor/TransportEncryptor.d.ts +16 -0
  9. package/dist/client/encryptor/TransportEncryptor.js +38 -0
  10. package/dist/client/index.d.ts +4 -0
  11. package/dist/client/index.js +4 -0
  12. package/dist/client/packet/AudioPacketizer.d.ts +8 -0
  13. package/dist/client/packet/AudioPacketizer.js +22 -0
  14. package/dist/client/packet/BaseMediaPacketizer.d.ts +109 -0
  15. package/dist/client/packet/BaseMediaPacketizer.js +243 -0
  16. package/dist/client/packet/VideoPacketizerAnnexB.d.ts +132 -0
  17. package/dist/client/packet/VideoPacketizerAnnexB.js +231 -0
  18. package/dist/client/packet/VideoPacketizerVP8.d.ts +15 -0
  19. package/dist/client/packet/VideoPacketizerVP8.js +56 -0
  20. package/dist/client/packet/index.d.ts +4 -0
  21. package/dist/client/packet/index.js +4 -0
  22. package/dist/client/processing/AnnexBHelper.d.ts +93 -0
  23. package/dist/client/processing/AnnexBHelper.js +132 -0
  24. package/dist/client/voice/BaseMediaConnection.d.ts +118 -0
  25. package/dist/client/voice/BaseMediaConnection.js +319 -0
  26. package/dist/client/voice/MediaUdp.d.ts +26 -0
  27. package/dist/client/voice/MediaUdp.js +140 -0
  28. package/dist/client/voice/StreamConnection.d.ts +10 -0
  29. package/dist/client/voice/StreamConnection.js +30 -0
  30. package/dist/client/voice/VoiceConnection.d.ts +7 -0
  31. package/dist/client/voice/VoiceConnection.js +10 -0
  32. package/dist/client/voice/VoiceMessageTypes.d.ts +136 -0
  33. package/dist/client/voice/VoiceMessageTypes.js +1 -0
  34. package/dist/client/voice/VoiceOpCodes.d.ts +21 -0
  35. package/dist/client/voice/VoiceOpCodes.js +22 -0
  36. package/dist/client/voice/index.d.ts +5 -0
  37. package/dist/client/voice/index.js +5 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.js +5 -0
  40. package/dist/media/AudioStream.d.ts +25 -0
  41. package/dist/media/AudioStream.js +63 -0
  42. package/dist/media/BaseMediaStream.d.ts +31 -0
  43. package/dist/media/BaseMediaStream.js +145 -0
  44. package/dist/media/LibavCodecId.d.ts +541 -0
  45. package/dist/media/LibavCodecId.js +552 -0
  46. package/dist/media/LibavDecoder.d.ts +5 -0
  47. package/dist/media/LibavDecoder.js +63 -0
  48. package/dist/media/LibavDemuxer.d.ts +23 -0
  49. package/dist/media/LibavDemuxer.js +295 -0
  50. package/dist/media/VideoStream.d.ts +7 -0
  51. package/dist/media/VideoStream.js +10 -0
  52. package/dist/media/index.d.ts +1 -0
  53. package/dist/media/index.js +1 -0
  54. package/dist/media/newApi.d.ts +126 -0
  55. package/dist/media/newApi.js +387 -0
  56. package/dist/media/utils.d.ts +1 -0
  57. package/dist/media/utils.js +4 -0
  58. package/dist/utils.d.ts +28 -0
  59. package/dist/utils.js +54 -0
  60. package/package.json +69 -0
  61. package/src/client/GatewayEvents.ts +41 -0
  62. package/src/client/GatewayOpCodes.ts +40 -0
  63. package/src/client/Streamer.ts +279 -0
  64. package/src/client/encryptor/TransportEncryptor.ts +62 -0
  65. package/src/client/index.ts +4 -0
  66. package/src/client/packet/AudioPacketizer.ts +28 -0
  67. package/src/client/packet/BaseMediaPacketizer.ts +307 -0
  68. package/src/client/packet/VideoPacketizerAnnexB.ts +263 -0
  69. package/src/client/packet/VideoPacketizerVP8.ts +73 -0
  70. package/src/client/packet/index.ts +4 -0
  71. package/src/client/processing/AnnexBHelper.ts +142 -0
  72. package/src/client/voice/BaseMediaConnection.ts +407 -0
  73. package/src/client/voice/MediaUdp.ts +171 -0
  74. package/src/client/voice/StreamConnection.ts +33 -0
  75. package/src/client/voice/VoiceConnection.ts +15 -0
  76. package/src/client/voice/VoiceMessageTypes.ts +164 -0
  77. package/src/client/voice/VoiceOpCodes.ts +21 -0
  78. package/src/client/voice/index.ts +5 -0
  79. package/src/index.ts +5 -0
  80. package/src/media/AudioStream.ts +81 -0
  81. package/src/media/BaseMediaStream.ts +173 -0
  82. package/src/media/LibavCodecId.ts +566 -0
  83. package/src/media/LibavDecoder.ts +82 -0
  84. package/src/media/LibavDemuxer.ts +348 -0
  85. package/src/media/VideoStream.ts +15 -0
  86. package/src/media/index.ts +1 -0
  87. package/src/media/newApi.ts +618 -0
  88. package/src/media/utils.ts +6 -0
  89. package/src/utils.ts +77 -0
@@ -0,0 +1,407 @@
1
+ import { VoiceOpCodes } from "./VoiceOpCodes.js";
2
+ import { MediaUdp } from "./MediaUdp.js";
3
+ import {
4
+ AES256TransportEncryptor,
5
+ Chacha20TransportEncryptor,
6
+ type TransportEncryptor
7
+ } from "../encryptor/TransportEncryptor.js";
8
+ import { STREAMS_SIMULCAST, SupportedEncryptionModes } from "../../utils.js";
9
+ import WebSocket from 'ws';
10
+ import EventEmitter from "node:events";
11
+ import type { Message, GatewayRequest, GatewayResponse } from "./VoiceMessageTypes.js";
12
+ import type { Streamer } from "../Streamer.js";
13
+
14
+ type VoiceConnectionStatus =
15
+ {
16
+ hasSession: boolean;
17
+ hasToken: boolean;
18
+ started: boolean;
19
+ resuming: boolean;
20
+ }
21
+
22
+ type WebRtcParameters = {
23
+ address: string,
24
+ port: number,
25
+ audioSsrc: number,
26
+ videoSsrc: number,
27
+ rtxSsrc: number
28
+ supportedEncryptionModes: SupportedEncryptionModes[]
29
+ }
30
+
31
+ type ValueOf<T> =
32
+ T extends (infer U)[] ? U :
33
+ T extends Record<string, infer U> ? U :
34
+ never
35
+
36
+ export type VideoAttributes = {
37
+ width: number,
38
+ height: number,
39
+ fps: number
40
+ }
41
+
42
+ export const CodecPayloadType = {
43
+ "opus": {
44
+ name: "opus", type: "audio", priority: 1000, payload_type: 120
45
+ },
46
+ "H264": {
47
+ name: "H264", type: "video", priority: 1000, payload_type: 101, rtx_payload_type: 102, encode: true, decode: true
48
+ },
49
+ "H265": {
50
+ name: "H265", type: "video", priority: 1000, payload_type: 103, rtx_payload_type: 104, encode: true, decode: true
51
+ },
52
+ "VP8": {
53
+ name: "VP8", type: "video", priority: 1000, payload_type: 105, rtx_payload_type: 106, encode: true, decode: true
54
+ },
55
+ "VP9": {
56
+ name: "VP9", type: "video", priority: 1000, payload_type: 107, rtx_payload_type: 108, encode: true, decode: true
57
+ },
58
+ "AV1": {
59
+ name: "AV1", type: "video", priority: 1000, payload_type: 109, rtx_payload_type: 110, encode: true, decode: true
60
+ }
61
+ } as const;
62
+
63
+ export abstract class BaseMediaConnection extends EventEmitter {
64
+ private interval: NodeJS.Timeout | null = null;
65
+ public udp: MediaUdp;
66
+ public guildId: string | null = null;
67
+ public channelId: string;
68
+ public botId: string;
69
+ public ws: WebSocket | null = null;
70
+ public ready: (udp: MediaUdp) => void;
71
+ public status: VoiceConnectionStatus;
72
+ public server: string | null = null; //websocket url
73
+ public token: string | null = null;
74
+ public session_id: string | null = null;
75
+
76
+ public webRtcParams: WebRtcParameters | null = null;
77
+ private _streamer: Streamer;
78
+ private _transportEncryptor?: TransportEncryptor;
79
+ private _sequenceNumber = -1;
80
+
81
+ constructor(
82
+ streamer: Streamer,
83
+ guildId: string | null,
84
+ botId: string,
85
+ channelId: string,
86
+ callback: (udp: MediaUdp) => void
87
+ ) {
88
+ super();
89
+ this._streamer = streamer;
90
+ this.status = {
91
+ hasSession: false,
92
+ hasToken: false,
93
+ started: false,
94
+ resuming: false
95
+ }
96
+
97
+ // make udp client
98
+ this.udp = new MediaUdp(this);
99
+
100
+ this.guildId = guildId;
101
+ this.channelId = channelId;
102
+ this.botId = botId;
103
+ this.ready = callback;
104
+ }
105
+
106
+ public abstract get serverId(): string | null;
107
+
108
+ public get type(): "guild" | "call" {
109
+ return this.guildId ? "guild" : "call";
110
+ }
111
+
112
+ public get transportEncryptor() {
113
+ return this._transportEncryptor;
114
+ }
115
+
116
+ public get streamer() {
117
+ return this._streamer;
118
+ }
119
+
120
+ stop(): void {
121
+ this.interval && clearInterval(this.interval);
122
+ this.status.started = false;
123
+ this.ws?.close();
124
+ this.udp?.stop();
125
+ }
126
+
127
+ setSession(session_id: string): void {
128
+ this.session_id = session_id;
129
+
130
+ this.status.hasSession = true;
131
+ this.start();
132
+ }
133
+
134
+ setTokens(server: string, token: string): void {
135
+ this.token = token;
136
+ this.server = server;
137
+
138
+ this.status.hasToken = true;
139
+ this.start();
140
+ }
141
+
142
+ start(): void {
143
+ /*
144
+ ** Connection can only start once both
145
+ ** session description and tokens have been gathered
146
+ */
147
+ if (this.status.hasSession && this.status.hasToken) {
148
+ if (this.status.started)
149
+ return
150
+ this.status.started = true;
151
+
152
+ this.ws = new WebSocket(`wss://${this.server}/?v=8`, {
153
+ followRedirects: true
154
+ });
155
+ this.ws.on("open", () => {
156
+ if(this.status.resuming) {
157
+ this.status.resuming = false;
158
+ this.resume();
159
+ } else {
160
+ this.identify();
161
+ }
162
+ })
163
+ this.ws.on("error", (err) => {
164
+ console.error(err);
165
+ })
166
+ this.ws.on("close", (code) => {
167
+ const wasStarted = this.status.started;
168
+
169
+ this.status.started = false;
170
+ this.udp.ready = false;
171
+
172
+ const canResume = code === 4_015 || code < 4_000;
173
+
174
+ if (canResume && wasStarted) {
175
+ this.status.resuming = true;
176
+ this.start();
177
+ }
178
+ })
179
+ this.setupEvents();
180
+ }
181
+ }
182
+
183
+ handleReady(d: Message.Ready): void {
184
+ // we hardcoded the STREAMS_SIMULCAST, which will always be array of 1
185
+ const stream = d.streams[0];
186
+ this.webRtcParams = {
187
+ address: d.ip,
188
+ port: d.port,
189
+ audioSsrc: d.ssrc,
190
+ videoSsrc: stream.ssrc,
191
+ rtxSsrc: stream.rtx_ssrc,
192
+ supportedEncryptionModes: d.modes
193
+ }
194
+ }
195
+
196
+ handleProtocolAck(d: Message.SelectProtocolAck): void {
197
+ const secretKey = Buffer.from(d.secret_key);
198
+ switch (d.mode)
199
+ {
200
+ case SupportedEncryptionModes.AES256:
201
+ this._transportEncryptor = new AES256TransportEncryptor(secretKey);
202
+ break;
203
+ case SupportedEncryptionModes.XCHACHA20:
204
+ this._transportEncryptor = new Chacha20TransportEncryptor(secretKey);
205
+ break;
206
+ }
207
+ this.emit("select_protocol_ack");
208
+ }
209
+
210
+ setupEvents(): void {
211
+ this.ws?.on('message', (data, isBinary) => {
212
+ if (isBinary)
213
+ return;
214
+ const { op, d, seq } = JSON.parse(data.toString()) as GatewayResponse;
215
+ if (seq)
216
+ this._sequenceNumber = seq;
217
+
218
+ if (op === VoiceOpCodes.READY) { // ready
219
+ this.handleReady(d);
220
+ this.sendVoice().then(() => this.ready(this.udp));
221
+ this.setVideoAttributes(false);
222
+ }
223
+ else if (op >= 4000) {
224
+ console.error(`Error ${this.constructor.name} connection`, d);
225
+ }
226
+ else if (op === VoiceOpCodes.HELLO) {
227
+ this.setupHeartbeat(d.heartbeat_interval);
228
+ }
229
+ else if (op === VoiceOpCodes.SELECT_PROTOCOL_ACK) { // session description
230
+ this.handleProtocolAck(d);
231
+ }
232
+ else if (op === VoiceOpCodes.SPEAKING) {
233
+ // ignore speaking updates
234
+ }
235
+ else if (op === VoiceOpCodes.HEARTBEAT_ACK) {
236
+ // ignore heartbeat acknowledgements
237
+ }
238
+ else if (op === VoiceOpCodes.RESUMED) {
239
+ this.status.started = true;
240
+ this.udp.ready = true;
241
+ }
242
+ else {
243
+ //console.log("unhandled voice event", {op, d});
244
+ }
245
+ });
246
+ }
247
+
248
+ setupHeartbeat(interval: number): void {
249
+ if (this.interval) {
250
+ clearInterval(this.interval);
251
+ }
252
+ this.interval = setInterval(() => {
253
+ this.sendOpcode(VoiceOpCodes.HEARTBEAT, {
254
+ t: Date.now(),
255
+ seq_ack: this._sequenceNumber
256
+ });
257
+ }, interval);
258
+ }
259
+
260
+ sendOpcode<T extends GatewayRequest>(code: T["op"], data: T["d"]): void {
261
+ this.ws?.send(JSON.stringify({
262
+ op: code,
263
+ d: data
264
+ }));
265
+ }
266
+
267
+ /*
268
+ ** identifies with media server with credentials
269
+ */
270
+ identify(): void {
271
+ if (!this.serverId)
272
+ throw new Error("Server ID is null or empty");
273
+ if (!this.session_id)
274
+ throw new Error("Session ID is null or empty");
275
+ if (!this.token)
276
+ throw new Error("Token is null or empty");
277
+ this.sendOpcode(VoiceOpCodes.IDENTIFY, {
278
+ server_id: this.serverId,
279
+ user_id: this.botId,
280
+ session_id: this.session_id,
281
+ token: this.token,
282
+ video: true,
283
+ streams: STREAMS_SIMULCAST
284
+ });
285
+ }
286
+
287
+ resume(): void {
288
+ if (!this.serverId)
289
+ throw new Error("Server ID is null or empty");
290
+ if (!this.session_id)
291
+ throw new Error("Session ID is null or empty");
292
+ if (!this.token)
293
+ throw new Error("Token is null or empty");
294
+ this.sendOpcode(VoiceOpCodes.RESUME, {
295
+ server_id: this.serverId,
296
+ session_id: this.session_id,
297
+ token: this.token,
298
+ seq_ack: this._sequenceNumber
299
+ });
300
+ }
301
+
302
+ /*
303
+ ** Sets protocols and ip data used for video and audio.
304
+ ** Uses vp8 for video
305
+ ** Uses opus for audio
306
+ */
307
+ private setProtocols(): Promise<void> {
308
+ const { ip, port } = this.udp;
309
+ if (!ip || !port)
310
+ throw new Error("IP or port is undefined (this shouldn't happen!!!)");
311
+ // select encryption mode
312
+ // From Discord docs:
313
+ // You must support aead_xchacha20_poly1305_rtpsize. You should prefer to use aead_aes256_gcm_rtpsize when it is available.
314
+ let encryptionMode: SupportedEncryptionModes;
315
+ if (!this.webRtcParams)
316
+ throw new Error("WebRTC connection not ready");
317
+ if (
318
+ this.webRtcParams.supportedEncryptionModes.includes(SupportedEncryptionModes.AES256) &&
319
+ !this._streamer.opts.forceChacha20Encryption
320
+ ) {
321
+ encryptionMode = SupportedEncryptionModes.AES256
322
+ } else {
323
+ encryptionMode = SupportedEncryptionModes.XCHACHA20
324
+ }
325
+ return new Promise((resolve) => {
326
+ this.sendOpcode(VoiceOpCodes.SELECT_PROTOCOL, {
327
+ protocol: "udp",
328
+ codecs: Object.values(CodecPayloadType) as ValueOf<typeof CodecPayloadType>[],
329
+ data: {
330
+ address: ip,
331
+ port: port,
332
+ mode: encryptionMode
333
+ }
334
+ });
335
+ this.once("select_protocol_ack", () => resolve());
336
+ })
337
+ }
338
+
339
+ /*
340
+ * Sets video attributes (width, height, frame rate).
341
+ * enabled -> video on or off
342
+ * attr -> video attributes
343
+ * video and rtx sources are set to ssrc + 1 and ssrc + 2
344
+ */
345
+ public setVideoAttributes(enabled: false): void
346
+ public setVideoAttributes(enabled: true, attr: VideoAttributes): void
347
+ public setVideoAttributes(enabled: boolean, attr?: VideoAttributes): void {
348
+ if (!this.webRtcParams)
349
+ throw new Error("WebRTC connection not ready");
350
+ const { audioSsrc, videoSsrc, rtxSsrc } = this.webRtcParams;
351
+ if (!enabled) {
352
+ this.sendOpcode(VoiceOpCodes.VIDEO, {
353
+ audio_ssrc: audioSsrc,
354
+ video_ssrc: 0,
355
+ rtx_ssrc: 0,
356
+ streams: []
357
+ })
358
+ } else {
359
+ if (!attr)
360
+ throw new Error("Need to specify video attributes")
361
+ this.sendOpcode(VoiceOpCodes.VIDEO, {
362
+ audio_ssrc: audioSsrc,
363
+ video_ssrc: videoSsrc,
364
+ rtx_ssrc: rtxSsrc,
365
+ streams: [
366
+ {
367
+ type:"video",
368
+ rid:"100",
369
+ ssrc: videoSsrc,
370
+ active: true,
371
+ quality: 100,
372
+ rtx_ssrc: rtxSsrc,
373
+ // hardcode the max bitrate because we don't really know anyway
374
+ max_bitrate: 10000 * 1000,
375
+ max_framerate: enabled ? attr.fps : 0,
376
+ max_resolution: {
377
+ type: "fixed",
378
+ width: attr.width,
379
+ height: attr.height
380
+ }
381
+ }
382
+ ]
383
+ });
384
+ }
385
+ }
386
+
387
+ /*
388
+ ** Set speaking status
389
+ ** speaking -> speaking status on or off
390
+ */
391
+ public setSpeaking(speaking: boolean): void {
392
+ if (!this.webRtcParams)
393
+ throw new Error("WebRTC connection not ready");
394
+ this.sendOpcode(VoiceOpCodes.SPEAKING, {
395
+ delay: 0,
396
+ speaking: speaking ? 1 : 0,
397
+ ssrc: this.webRtcParams.audioSsrc
398
+ });
399
+ }
400
+
401
+ /*
402
+ ** Start media connection
403
+ */
404
+ public sendVoice(): Promise<void> {
405
+ return this.udp.createUdp().then(() => this.setProtocols());
406
+ }
407
+ }
@@ -0,0 +1,171 @@
1
+ import udpCon from 'node:dgram';
2
+ import { isIP } from 'node:net';
3
+ import { AudioPacketizer } from '../packet/AudioPacketizer.js';
4
+ import {
5
+ VideoPacketizerH264,
6
+ VideoPacketizerH265
7
+ } from '../packet/VideoPacketizerAnnexB.js';
8
+ import { VideoPacketizerVP8 } from '../packet/VideoPacketizerVP8.js';
9
+ import { normalizeVideoCodec } from '../../utils.js';
10
+ import type { BaseMediaPacketizer } from '../packet/BaseMediaPacketizer.js';
11
+ import type { BaseMediaConnection } from './BaseMediaConnection.js';
12
+
13
+ // credit to discord.js
14
+ function parseLocalPacket(message: Buffer) {
15
+ const packet = Buffer.from(message);
16
+
17
+ const ip = packet.subarray(8, packet.indexOf(0, 8)).toString('utf8');
18
+
19
+ if (!isIP(ip)) {
20
+ throw new Error('Malformed IP address');
21
+ }
22
+
23
+ const port = packet.readUInt16BE(packet.length - 2);
24
+
25
+ return { ip, port };
26
+ }
27
+
28
+ export class MediaUdp {
29
+ private _mediaConnection: BaseMediaConnection;
30
+ private _socket: udpCon.Socket | null = null;
31
+ private _ready = false;
32
+ private _audioPacketizer?: BaseMediaPacketizer;
33
+ private _videoPacketizer?: BaseMediaPacketizer;
34
+ private _ip?: string;
35
+ private _port?: number;
36
+
37
+ constructor(voiceConnection: BaseMediaConnection) {
38
+ this._mediaConnection = voiceConnection;
39
+ }
40
+
41
+ public get audioPacketizer(): BaseMediaPacketizer | undefined {
42
+ return this._audioPacketizer;
43
+ }
44
+
45
+ public get videoPacketizer(): BaseMediaPacketizer | undefined {
46
+ // This will never be undefined anyway, so it's safe
47
+ return this._videoPacketizer;
48
+ }
49
+
50
+ public get mediaConnection(): BaseMediaConnection {
51
+ return this._mediaConnection;
52
+ }
53
+
54
+ public get ip()
55
+ {
56
+ return this._ip;
57
+ }
58
+
59
+ public get port()
60
+ {
61
+ return this._port;
62
+ }
63
+
64
+ public async sendAudioFrame(frame: Buffer, frametime: number): Promise<void> {
65
+ if(!this.ready) return;
66
+ await this.audioPacketizer?.sendFrame(frame, frametime);
67
+ }
68
+
69
+ public async sendVideoFrame(frame: Buffer, frametime: number): Promise<void> {
70
+ if(!this.ready) return;
71
+ await this.videoPacketizer?.sendFrame(frame, frametime);
72
+ }
73
+
74
+ public setPacketizer(videoCodec: string): void {
75
+ if (!this.mediaConnection.webRtcParams)
76
+ throw new Error("WebRTC connection not ready");
77
+ const { audioSsrc, videoSsrc } = this.mediaConnection.webRtcParams;
78
+ this._audioPacketizer = new AudioPacketizer(this, audioSsrc);
79
+ switch (normalizeVideoCodec(videoCodec))
80
+ {
81
+ case "H264":
82
+ this._videoPacketizer = new VideoPacketizerH264(this, videoSsrc);
83
+ break;
84
+ case "H265":
85
+ this._videoPacketizer = new VideoPacketizerH265(this, videoSsrc);
86
+ break;
87
+ case "VP8":
88
+ this._videoPacketizer = new VideoPacketizerVP8(this, videoSsrc);
89
+ break;
90
+ default:
91
+ throw new Error(`Packetizer not implemented for ${videoCodec}`)
92
+ }
93
+ }
94
+
95
+ public sendPacket(packet: Buffer): Promise<void> {
96
+ if (!this.mediaConnection.webRtcParams)
97
+ throw new Error("WebRTC connection not ready");
98
+ const { address, port } = this.mediaConnection.webRtcParams;
99
+ return new Promise<void>((resolve, reject) => {
100
+ try {
101
+ this._socket?.send(packet, 0, packet.length, port, address, (error, bytes) => {
102
+ if (error) {
103
+ console.log("ERROR", error);
104
+ reject(error);
105
+ }
106
+ resolve();
107
+ });
108
+ } catch(e) {reject(e)}
109
+ });
110
+ }
111
+
112
+ handleIncoming(buf: unknown): void {
113
+ //console.log("RECEIVED PACKET", buf);
114
+ }
115
+
116
+ public get ready(): boolean {
117
+ return this._ready;
118
+ }
119
+
120
+ public set ready(val: boolean) {
121
+ this._ready = val;
122
+ }
123
+
124
+ public stop(): void {
125
+ try {
126
+ this.ready = false;
127
+ this._socket?.disconnect();
128
+ }catch(e) {}
129
+ }
130
+
131
+ public createUdp(): Promise<void> {
132
+ if (!this.mediaConnection.webRtcParams)
133
+ throw new Error("WebRTC connection not ready");
134
+ const { audioSsrc, address, port } = this.mediaConnection.webRtcParams;
135
+ return new Promise<void>((resolve, reject) => {
136
+ this._socket = udpCon.createSocket('udp4');
137
+
138
+ this._socket.on('error', (error: Error) => {
139
+ console.error("Error connecting to media udp server", error);
140
+ reject(error);
141
+ });
142
+
143
+ this._socket.once('message', (message) => {
144
+ if (message.readUInt16BE(0) !== 2) {
145
+ reject('wrong handshake packet for udp')
146
+ }
147
+ try {
148
+ const packet = parseLocalPacket(message);
149
+ this._ip = packet.ip;
150
+ this._port = packet.port;
151
+ this._ready = true;
152
+ } catch(e) { reject(e) }
153
+
154
+ resolve();
155
+ this._socket?.on('message', this.handleIncoming);
156
+ });
157
+
158
+ const blank = Buffer.alloc(74);
159
+
160
+ blank.writeUInt16BE(1, 0);
161
+ blank.writeUInt16BE(70, 2);
162
+ blank.writeUInt32BE(audioSsrc, 4);
163
+
164
+ this._socket.send(blank, 0, blank.length, port, address, (error, bytes) => {
165
+ if (error) {
166
+ reject(error)
167
+ }
168
+ });
169
+ });
170
+ }
171
+ }
@@ -0,0 +1,33 @@
1
+ import { VoiceOpCodes } from "../voice/VoiceOpCodes.js";
2
+ import { BaseMediaConnection } from "./BaseMediaConnection.js";
3
+
4
+ export class StreamConnection extends BaseMediaConnection
5
+ {
6
+ private _streamKey: string | null = null;
7
+ private _serverId: string | null = null;
8
+
9
+ public override setSpeaking(speaking: boolean): void {
10
+ if (!this.webRtcParams)
11
+ throw new Error("WebRTC connection not ready");
12
+ this.sendOpcode(VoiceOpCodes.SPEAKING, {
13
+ delay: 0,
14
+ speaking: speaking ? 2 : 0,
15
+ ssrc: this.webRtcParams.audioSsrc
16
+ });
17
+ }
18
+
19
+ public override get serverId(): string | null {
20
+ return this._serverId;
21
+ }
22
+
23
+ public set serverId(id: string) {
24
+ this._serverId = id;
25
+ }
26
+
27
+ public get streamKey(): string | null {
28
+ return this._streamKey;
29
+ }
30
+ public set streamKey(value: string) {
31
+ this._streamKey = value;
32
+ }
33
+ }
@@ -0,0 +1,15 @@
1
+ import type { StreamConnection } from './StreamConnection.js';
2
+ import { BaseMediaConnection } from './BaseMediaConnection.js';
3
+
4
+ export class VoiceConnection extends BaseMediaConnection {
5
+ public streamConnection?: StreamConnection;
6
+
7
+ public override get serverId(): string {
8
+ return this.guildId ?? this.channelId; // for guild vc it is the guild id, for dm voice it is the channel id
9
+ }
10
+
11
+ public override stop(): void {
12
+ super.stop();
13
+ this.streamConnection?.stop();
14
+ }
15
+ }