@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.
- package/README.md +294 -0
- package/dist/client/GatewayEvents.d.ts +27 -0
- package/dist/client/GatewayEvents.js +1 -0
- package/dist/client/GatewayOpCodes.d.ts +40 -0
- package/dist/client/GatewayOpCodes.js +41 -0
- package/dist/client/Streamer.d.ts +41 -0
- package/dist/client/Streamer.js +189 -0
- package/dist/client/encryptor/TransportEncryptor.d.ts +16 -0
- package/dist/client/encryptor/TransportEncryptor.js +38 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +4 -0
- package/dist/client/packet/AudioPacketizer.d.ts +8 -0
- package/dist/client/packet/AudioPacketizer.js +22 -0
- package/dist/client/packet/BaseMediaPacketizer.d.ts +109 -0
- package/dist/client/packet/BaseMediaPacketizer.js +243 -0
- package/dist/client/packet/VideoPacketizerAnnexB.d.ts +132 -0
- package/dist/client/packet/VideoPacketizerAnnexB.js +231 -0
- package/dist/client/packet/VideoPacketizerVP8.d.ts +15 -0
- package/dist/client/packet/VideoPacketizerVP8.js +56 -0
- package/dist/client/packet/index.d.ts +4 -0
- package/dist/client/packet/index.js +4 -0
- package/dist/client/processing/AnnexBHelper.d.ts +93 -0
- package/dist/client/processing/AnnexBHelper.js +132 -0
- package/dist/client/voice/BaseMediaConnection.d.ts +118 -0
- package/dist/client/voice/BaseMediaConnection.js +319 -0
- package/dist/client/voice/MediaUdp.d.ts +26 -0
- package/dist/client/voice/MediaUdp.js +140 -0
- package/dist/client/voice/StreamConnection.d.ts +10 -0
- package/dist/client/voice/StreamConnection.js +30 -0
- package/dist/client/voice/VoiceConnection.d.ts +7 -0
- package/dist/client/voice/VoiceConnection.js +10 -0
- package/dist/client/voice/VoiceMessageTypes.d.ts +136 -0
- package/dist/client/voice/VoiceMessageTypes.js +1 -0
- package/dist/client/voice/VoiceOpCodes.d.ts +21 -0
- package/dist/client/voice/VoiceOpCodes.js +22 -0
- package/dist/client/voice/index.d.ts +5 -0
- package/dist/client/voice/index.js +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/media/AudioStream.d.ts +25 -0
- package/dist/media/AudioStream.js +63 -0
- package/dist/media/BaseMediaStream.d.ts +31 -0
- package/dist/media/BaseMediaStream.js +145 -0
- package/dist/media/LibavCodecId.d.ts +541 -0
- package/dist/media/LibavCodecId.js +552 -0
- package/dist/media/LibavDecoder.d.ts +5 -0
- package/dist/media/LibavDecoder.js +63 -0
- package/dist/media/LibavDemuxer.d.ts +23 -0
- package/dist/media/LibavDemuxer.js +295 -0
- package/dist/media/VideoStream.d.ts +7 -0
- package/dist/media/VideoStream.js +10 -0
- package/dist/media/index.d.ts +1 -0
- package/dist/media/index.js +1 -0
- package/dist/media/newApi.d.ts +126 -0
- package/dist/media/newApi.js +387 -0
- package/dist/media/utils.d.ts +1 -0
- package/dist/media/utils.js +4 -0
- package/dist/utils.d.ts +28 -0
- package/dist/utils.js +54 -0
- package/package.json +69 -0
- package/src/client/GatewayEvents.ts +41 -0
- package/src/client/GatewayOpCodes.ts +40 -0
- package/src/client/Streamer.ts +279 -0
- package/src/client/encryptor/TransportEncryptor.ts +62 -0
- package/src/client/index.ts +4 -0
- package/src/client/packet/AudioPacketizer.ts +28 -0
- package/src/client/packet/BaseMediaPacketizer.ts +307 -0
- package/src/client/packet/VideoPacketizerAnnexB.ts +263 -0
- package/src/client/packet/VideoPacketizerVP8.ts +73 -0
- package/src/client/packet/index.ts +4 -0
- package/src/client/processing/AnnexBHelper.ts +142 -0
- package/src/client/voice/BaseMediaConnection.ts +407 -0
- package/src/client/voice/MediaUdp.ts +171 -0
- package/src/client/voice/StreamConnection.ts +33 -0
- package/src/client/voice/VoiceConnection.ts +15 -0
- package/src/client/voice/VoiceMessageTypes.ts +164 -0
- package/src/client/voice/VoiceOpCodes.ts +21 -0
- package/src/client/voice/index.ts +5 -0
- package/src/index.ts +5 -0
- package/src/media/AudioStream.ts +81 -0
- package/src/media/BaseMediaStream.ts +173 -0
- package/src/media/LibavCodecId.ts +566 -0
- package/src/media/LibavDecoder.ts +82 -0
- package/src/media/LibavDemuxer.ts +348 -0
- package/src/media/VideoStream.ts +15 -0
- package/src/media/index.ts +1 -0
- package/src/media/newApi.ts +618 -0
- package/src/media/utils.ts +6 -0
- 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
|
+
}
|