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