djs-selfbot-v13 3.7.30 → 3.7.33
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/package.json +1 -1
- package/src/client/Client.js +74 -1
- package/src/client/voice/ClientVoiceManager.js +84 -17
- package/src/client/voice/StreamSession.js +168 -0
- package/src/client/voice/VoiceConnection.js +265 -19
- package/src/client/voice/dispatcher/AnnexBDispatcher.js +64 -11
- package/src/client/voice/dispatcher/BaseDispatcher.js +13 -9
- package/src/client/voice/dispatcher/VideoDispatcher.js +33 -0
- package/src/client/voice/networking/DAVESession.js +234 -0
- package/src/client/voice/networking/VoiceWebSocket.js +284 -24
- package/src/client/voice/player/MediaPlayer.js +3 -1
- package/src/client/voice/player/processing/AnnexBBitstreamReaderWriter.js +137 -0
- package/src/client/voice/player/processing/SPSVUIRewriter.js +203 -0
- package/src/client/voice/receiver/PacketHandler.js +11 -4
- package/src/errors/Messages.js +6 -1
- package/src/managers/BackupManager.js +1 -1
- package/src/{util → managers}/backup/index.js +1 -1
- package/src/{util → managers}/backup/load.js +1 -1
- package/src/util/Constants.js +12 -0
- package/typings/index.d.ts +29 -0
- /package/src/{util → managers}/backup/create.js +0 -0
- /package/src/{util → managers}/backup/util.js +0 -0
|
@@ -16,6 +16,14 @@ const { Opcodes, VoiceOpcodes, VoiceStatus, Events } = require('../../util/Const
|
|
|
16
16
|
const Speaking = require('../../util/Speaking');
|
|
17
17
|
const Util = require('../../util/Util');
|
|
18
18
|
|
|
19
|
+
function normalizeVoiceEndpoint(endpoint, { preservePort = false } = {}) {
|
|
20
|
+
const match = endpoint?.match(/^([^:]+)(?::(\d+))?/);
|
|
21
|
+
if (!match) return endpoint;
|
|
22
|
+
const [, host, port] = match;
|
|
23
|
+
if (!preservePort || !port || port === '443' || port === '80') return host;
|
|
24
|
+
return `${host}:${port}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
// Workaround for Discord now requiring silence to be sent before being able to receive audio
|
|
20
28
|
class SingleSilence extends Silence {
|
|
21
29
|
_read() {
|
|
@@ -166,6 +174,18 @@ class VoiceConnection extends EventEmitter {
|
|
|
166
174
|
* @type {Collection<Snowflake, StreamConnectionReadonly>}
|
|
167
175
|
*/
|
|
168
176
|
this.streamWatchConnection = new Collection();
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* DAVE end-to-end encryption session
|
|
180
|
+
* @type {?DAVESession}
|
|
181
|
+
*/
|
|
182
|
+
this.dave = null;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Connected client user IDs for DAVE MLS
|
|
186
|
+
* @type {Set<Snowflake>}
|
|
187
|
+
*/
|
|
188
|
+
this.connectedClients = new Set();
|
|
169
189
|
}
|
|
170
190
|
|
|
171
191
|
/**
|
|
@@ -228,6 +248,20 @@ class VoiceConnection extends EventEmitter {
|
|
|
228
248
|
return this;
|
|
229
249
|
}
|
|
230
250
|
|
|
251
|
+
/**
|
|
252
|
+
* SSRC values assigned by the voice server.
|
|
253
|
+
* @returns {{ audioSsrc: number, videoSsrc: number, rtxSsrc: number }}
|
|
254
|
+
*/
|
|
255
|
+
getStreamSsrcs() {
|
|
256
|
+
const stream = this.authentication.streams?.[0];
|
|
257
|
+
const audioSsrc = this.authentication.ssrc;
|
|
258
|
+
return {
|
|
259
|
+
audioSsrc,
|
|
260
|
+
videoSsrc: stream?.ssrc ?? audioSsrc + 1,
|
|
261
|
+
rtxSsrc: stream?.rtx_ssrc ?? audioSsrc + 2,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
231
265
|
/**
|
|
232
266
|
* Sets video status
|
|
233
267
|
* @param {boolean} value Video on or off
|
|
@@ -236,6 +270,10 @@ class VoiceConnection extends EventEmitter {
|
|
|
236
270
|
if (value === this.videoStatus) return;
|
|
237
271
|
if (this.status !== VoiceStatus.CONNECTED) return;
|
|
238
272
|
this.videoStatus = value;
|
|
273
|
+
const attrs = this.videoAttributes ?? {};
|
|
274
|
+
const fps = attrs.fps ?? 30;
|
|
275
|
+
const height = attrs.height ?? 720;
|
|
276
|
+
const width = attrs.width ?? Math.round((height * 16) / 9);
|
|
239
277
|
if (!value) {
|
|
240
278
|
this.sockets.ws
|
|
241
279
|
.sendPacket({
|
|
@@ -251,27 +289,28 @@ class VoiceConnection extends EventEmitter {
|
|
|
251
289
|
this.emit('debug', e);
|
|
252
290
|
});
|
|
253
291
|
} else {
|
|
292
|
+
const { audioSsrc, videoSsrc, rtxSsrc } = this.getStreamSsrcs();
|
|
254
293
|
this.sockets.ws
|
|
255
294
|
.sendPacket({
|
|
256
295
|
op: VoiceOpcodes.SOURCES,
|
|
257
296
|
d: {
|
|
258
|
-
audio_ssrc:
|
|
259
|
-
video_ssrc:
|
|
260
|
-
rtx_ssrc:
|
|
297
|
+
audio_ssrc: audioSsrc,
|
|
298
|
+
video_ssrc: videoSsrc,
|
|
299
|
+
rtx_ssrc: rtxSsrc,
|
|
261
300
|
streams: [
|
|
262
301
|
{
|
|
263
302
|
type: 'video',
|
|
264
303
|
rid: '100',
|
|
265
|
-
ssrc:
|
|
304
|
+
ssrc: videoSsrc,
|
|
266
305
|
active: true,
|
|
267
306
|
quality: 100,
|
|
268
|
-
rtx_ssrc:
|
|
269
|
-
max_bitrate:
|
|
270
|
-
max_framerate:
|
|
307
|
+
rtx_ssrc: rtxSsrc,
|
|
308
|
+
max_bitrate: 10_000_000,
|
|
309
|
+
max_framerate: fps,
|
|
271
310
|
max_resolution: {
|
|
272
|
-
type: '
|
|
273
|
-
width
|
|
274
|
-
height
|
|
311
|
+
type: 'fixed',
|
|
312
|
+
width,
|
|
313
|
+
height,
|
|
275
314
|
},
|
|
276
315
|
},
|
|
277
316
|
],
|
|
@@ -305,11 +344,14 @@ class VoiceConnection extends EventEmitter {
|
|
|
305
344
|
self_mute: this.voice ? this.voice.selfMute : false,
|
|
306
345
|
self_deaf: this.voice ? this.voice.selfDeaf : false,
|
|
307
346
|
self_video: this.voice ? this.voice.selfVideo : false,
|
|
308
|
-
flags: 2,
|
|
309
347
|
},
|
|
310
348
|
options,
|
|
311
349
|
);
|
|
312
350
|
|
|
351
|
+
if (options.self_video) {
|
|
352
|
+
options.flags = 2;
|
|
353
|
+
}
|
|
354
|
+
|
|
313
355
|
this.emit('debug', `Sending voice state update: ${JSON.stringify(options)}`);
|
|
314
356
|
|
|
315
357
|
return this.channel.client.ws.broadcast({
|
|
@@ -371,6 +413,21 @@ class VoiceConnection extends EventEmitter {
|
|
|
371
413
|
this.checkAuthenticated();
|
|
372
414
|
} else if (sessionId !== this.authentication.sessionId) {
|
|
373
415
|
this.authentication.sessionId = sessionId;
|
|
416
|
+
if (
|
|
417
|
+
this.constructor.name !== 'StreamConnection' &&
|
|
418
|
+
[VoiceStatus.CONNECTED, VoiceStatus.CONNECTING].includes(this.status)
|
|
419
|
+
) {
|
|
420
|
+
this.status = VoiceStatus.RECONNECTING;
|
|
421
|
+
if (this.sockets.ws) {
|
|
422
|
+
this.sockets.ws.shutdown();
|
|
423
|
+
this.sockets.ws = null;
|
|
424
|
+
}
|
|
425
|
+
if (this.sockets.udp) {
|
|
426
|
+
this.sockets.udp.shutdown();
|
|
427
|
+
this.sockets.udp = null;
|
|
428
|
+
}
|
|
429
|
+
this.checkAuthenticated();
|
|
430
|
+
}
|
|
374
431
|
/**
|
|
375
432
|
* Emitted when a new session ID is received.
|
|
376
433
|
* @event VoiceConnection#newSession
|
|
@@ -386,8 +443,9 @@ class VoiceConnection extends EventEmitter {
|
|
|
386
443
|
*/
|
|
387
444
|
checkAuthenticated() {
|
|
388
445
|
const { token, endpoint, sessionId } = this.authentication;
|
|
446
|
+
const needsServerId = this.constructor.name === 'StreamConnection';
|
|
389
447
|
this.emit('debug', `Authenticated with sessionId ${sessionId}`);
|
|
390
|
-
if (token && endpoint && sessionId) {
|
|
448
|
+
if (token && endpoint && sessionId && (!needsServerId || this.serverId)) {
|
|
391
449
|
this.status = VoiceStatus.CONNECTING;
|
|
392
450
|
/**
|
|
393
451
|
* Emitted when we successfully initiate a voice connection.
|
|
@@ -441,7 +499,7 @@ class VoiceConnection extends EventEmitter {
|
|
|
441
499
|
*/
|
|
442
500
|
authenticate(options = {}) {
|
|
443
501
|
this.sendVoiceStateUpdate(options);
|
|
444
|
-
this.connectTimeout = setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'),
|
|
502
|
+
this.connectTimeout = setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 30_000).unref();
|
|
445
503
|
}
|
|
446
504
|
|
|
447
505
|
/**
|
|
@@ -500,6 +558,16 @@ class VoiceConnection extends EventEmitter {
|
|
|
500
558
|
cleanup() {
|
|
501
559
|
this.player.destroy();
|
|
502
560
|
this.speaking = new Speaking().freeze();
|
|
561
|
+
if (this.dave) {
|
|
562
|
+
const isSharedDave =
|
|
563
|
+
this.constructor.name === 'StreamConnection' && this.voiceConnection?.dave === this.dave;
|
|
564
|
+
if (!isSharedDave) {
|
|
565
|
+
this.dave.destroy();
|
|
566
|
+
this.dave.removeAllListeners();
|
|
567
|
+
}
|
|
568
|
+
this.dave = null;
|
|
569
|
+
}
|
|
570
|
+
this.connectedClients.clear();
|
|
503
571
|
const { ws, udp } = this.sockets;
|
|
504
572
|
|
|
505
573
|
this.emit('debug', 'Connection clean up');
|
|
@@ -571,11 +639,21 @@ class VoiceConnection extends EventEmitter {
|
|
|
571
639
|
* @param {Object} data The received data
|
|
572
640
|
* @private
|
|
573
641
|
*/
|
|
642
|
+
isDaveReady() {
|
|
643
|
+
return (
|
|
644
|
+
!this.authentication.dave_protocol_version ||
|
|
645
|
+
(this.dave?.lastTransitionId === 0 && Boolean(this.dave?.session?.ready))
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
574
649
|
onSessionDescription(data) {
|
|
575
650
|
Object.assign(this.authentication, data);
|
|
576
651
|
this.status = VoiceStatus.CONNECTED;
|
|
652
|
+
clearTimeout(this.connectTimeout);
|
|
653
|
+
let daveFallback;
|
|
577
654
|
const ready = () => {
|
|
578
655
|
clearTimeout(this.connectTimeout);
|
|
656
|
+
clearTimeout(daveFallback);
|
|
579
657
|
this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`);
|
|
580
658
|
/**
|
|
581
659
|
* Emitted once the connection is ready, when a promise to join a voice channel resolves,
|
|
@@ -584,10 +662,32 @@ class VoiceConnection extends EventEmitter {
|
|
|
584
662
|
*/
|
|
585
663
|
this.emit('ready');
|
|
586
664
|
};
|
|
587
|
-
|
|
665
|
+
const waitForDave = () => {
|
|
666
|
+
if (this.isDaveReady()) {
|
|
667
|
+
ready();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const onTransitioned = () => {
|
|
671
|
+
if (this.isDaveReady()) {
|
|
672
|
+
this.sockets.ws?.removeListener('transitioned', onTransitioned);
|
|
673
|
+
ready();
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
this.sockets.ws?.on('transitioned', onTransitioned);
|
|
677
|
+
daveFallback = setTimeout(() => {
|
|
678
|
+
this.sockets.ws?.removeListener('transitioned', onTransitioned);
|
|
679
|
+
this.emit('debug', 'DAVE handshake incomplete, continuing with transport encryption only');
|
|
680
|
+
ready();
|
|
681
|
+
}, 15_000).unref();
|
|
682
|
+
};
|
|
683
|
+
if (data.dave_protocol_version) {
|
|
684
|
+
if (!this.dispatcher && !this.videoDispatcher) {
|
|
685
|
+
this.playAudio(new SingleSilence(), { type: 'opus', volume: false });
|
|
686
|
+
}
|
|
687
|
+
waitForDave();
|
|
688
|
+
} else if (this.dispatcher || this.videoDispatcher) {
|
|
588
689
|
ready();
|
|
589
690
|
} else {
|
|
590
|
-
// This serves to provide support for voice receive, sending audio is required to receive it.
|
|
591
691
|
const dispatcher = this.playAudio(new SingleSilence(), { type: 'opus', volume: false });
|
|
592
692
|
dispatcher.once('finish', ready);
|
|
593
693
|
}
|
|
@@ -672,12 +772,97 @@ class VoiceConnection extends EventEmitter {
|
|
|
672
772
|
* Create new connection to screenshare stream
|
|
673
773
|
* @returns {Promise<StreamConnection>}
|
|
674
774
|
*/
|
|
775
|
+
waitForDaveReady() {
|
|
776
|
+
if (this.isDaveReady() || this.status === VoiceStatus.CONNECTED) return Promise.resolve();
|
|
777
|
+
return new Promise((resolve, reject) => {
|
|
778
|
+
const timeout = setTimeout(() => reject(new Error('VOICE_CONNECTION_TIMEOUT')), 30_000).unref();
|
|
779
|
+
const onReady = () => {
|
|
780
|
+
if (!this.isDaveReady()) return;
|
|
781
|
+
clearTimeout(timeout);
|
|
782
|
+
this.sockets.ws?.removeListener('transitioned', onReady);
|
|
783
|
+
resolve();
|
|
784
|
+
};
|
|
785
|
+
this.sockets.ws?.on('transitioned', onReady);
|
|
786
|
+
onReady();
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
stopExistingStream() {
|
|
791
|
+
return new Promise(resolve => {
|
|
792
|
+
const hadStreamConnection = Boolean(this.streamConnection);
|
|
793
|
+
if (this.streamConnection) {
|
|
794
|
+
this.streamConnection.disconnect();
|
|
795
|
+
this.streamConnection = null;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const streamKey = `${['DM', 'GROUP_DM'].includes(this.channel.type) ? 'call' : `guild:${this.channel.guild.id}`}:${
|
|
799
|
+
this.channel.id
|
|
800
|
+
}:${this.client.user.id}`;
|
|
801
|
+
|
|
802
|
+
const voiceState = this.channel.guild?.voiceStates.cache.get(this.client.user.id);
|
|
803
|
+
const wasStreaming = this.client.user?.voice?.streaming || voiceState?.streaming;
|
|
804
|
+
|
|
805
|
+
if (!wasStreaming && !hadStreamConnection) {
|
|
806
|
+
setTimeout(resolve, 200).unref();
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const sendDelete = () => {
|
|
811
|
+
this.channel.client.ws.broadcast({
|
|
812
|
+
op: Opcodes.STREAM_DELETE,
|
|
813
|
+
d: { stream_key: streamKey },
|
|
814
|
+
});
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
sendDelete();
|
|
818
|
+
if (wasStreaming) setTimeout(sendDelete, 400).unref();
|
|
819
|
+
|
|
820
|
+
let settled = false;
|
|
821
|
+
const cleanup = () => {
|
|
822
|
+
this.client.off(Events.VOICE_STATE_UPDATE, onState);
|
|
823
|
+
this.off('ready', onVoiceReady);
|
|
824
|
+
};
|
|
825
|
+
const finish = () => {
|
|
826
|
+
if (settled) return;
|
|
827
|
+
if (this.status !== VoiceStatus.CONNECTED) return;
|
|
828
|
+
settled = true;
|
|
829
|
+
cleanup();
|
|
830
|
+
setTimeout(resolve, 500).unref();
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const onState = (_oldState, newState) => {
|
|
834
|
+
if (newState.id !== this.client.user.id || newState.streaming) return;
|
|
835
|
+
finish();
|
|
836
|
+
};
|
|
837
|
+
const onVoiceReady = () => finish();
|
|
838
|
+
|
|
839
|
+
this.client.on(Events.VOICE_STATE_UPDATE, onState);
|
|
840
|
+
this.on('ready', onVoiceReady);
|
|
841
|
+
|
|
842
|
+
if (!wasStreaming) {
|
|
843
|
+
setTimeout(resolve, 1500).unref();
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
setTimeout(() => finish(), 2000).unref();
|
|
848
|
+
setTimeout(() => {
|
|
849
|
+
if (settled) return;
|
|
850
|
+
settled = true;
|
|
851
|
+
cleanup();
|
|
852
|
+
resolve();
|
|
853
|
+
}, 12_000).unref();
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
675
857
|
createStreamConnection() {
|
|
676
858
|
// eslint-disable-next-line consistent-return
|
|
677
859
|
return new Promise((resolve, reject) => {
|
|
678
860
|
if (this.streamConnection) {
|
|
679
861
|
return resolve(this.streamConnection);
|
|
680
862
|
} else {
|
|
863
|
+
this.waitForDaveReady()
|
|
864
|
+
.then(() => this.stopExistingStream())
|
|
865
|
+
.then(() => {
|
|
681
866
|
const connection = (this.streamConnection = new StreamConnection(this.voiceManager, this.channel, this));
|
|
682
867
|
connection.setVideoCodec(this.videoCodec); // Sync :?
|
|
683
868
|
// Setup event...
|
|
@@ -697,7 +882,7 @@ class VoiceConnection extends EventEmitter {
|
|
|
697
882
|
// Current user stream
|
|
698
883
|
switch (event) {
|
|
699
884
|
case 'STREAM_CREATE': {
|
|
700
|
-
this.streamConnection.setSessionId(this.authentication.sessionId);
|
|
885
|
+
this.streamConnection.setSessionId(data.session_id || this.authentication.sessionId);
|
|
701
886
|
this.streamConnection.serverId = data.rtc_server_id;
|
|
702
887
|
break;
|
|
703
888
|
}
|
|
@@ -706,6 +891,13 @@ class VoiceConnection extends EventEmitter {
|
|
|
706
891
|
break;
|
|
707
892
|
}
|
|
708
893
|
case 'STREAM_DELETE': {
|
|
894
|
+
if (
|
|
895
|
+
[VoiceStatus.CONNECTING, VoiceStatus.AUTHENTICATING, VoiceStatus.RECONNECTING].includes(
|
|
896
|
+
this.streamConnection.status,
|
|
897
|
+
)
|
|
898
|
+
) {
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
709
901
|
this.streamConnection.disconnect();
|
|
710
902
|
break;
|
|
711
903
|
}
|
|
@@ -743,7 +935,7 @@ class VoiceConnection extends EventEmitter {
|
|
|
743
935
|
}
|
|
744
936
|
|
|
745
937
|
connection.sendSignalScreenshare();
|
|
746
|
-
connection.
|
|
938
|
+
connection.connectTimeout = setTimeout(() => connection.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 30_000).unref();
|
|
747
939
|
|
|
748
940
|
connection.on('debug', msg =>
|
|
749
941
|
this.channel.client.emit(
|
|
@@ -760,6 +952,7 @@ class VoiceConnection extends EventEmitter {
|
|
|
760
952
|
|
|
761
953
|
connection.once('authenticated', () => {
|
|
762
954
|
connection.once('ready', () => {
|
|
955
|
+
connection.sendScreenshareState(false);
|
|
763
956
|
resolve(connection);
|
|
764
957
|
connection.removeListener('error', reject);
|
|
765
958
|
});
|
|
@@ -767,6 +960,8 @@ class VoiceConnection extends EventEmitter {
|
|
|
767
960
|
this.streamConnection = null;
|
|
768
961
|
});
|
|
769
962
|
});
|
|
963
|
+
})
|
|
964
|
+
.catch(reject);
|
|
770
965
|
}
|
|
771
966
|
});
|
|
772
967
|
}
|
|
@@ -811,7 +1006,7 @@ class VoiceConnection extends EventEmitter {
|
|
|
811
1006
|
// Current user stream
|
|
812
1007
|
switch (event) {
|
|
813
1008
|
case 'STREAM_CREATE': {
|
|
814
|
-
this.streamConnection.setSessionId(this.authentication.sessionId);
|
|
1009
|
+
this.streamConnection.setSessionId(data.session_id || this.authentication.sessionId);
|
|
815
1010
|
this.streamConnection.serverId = data.rtc_server_id;
|
|
816
1011
|
break;
|
|
817
1012
|
}
|
|
@@ -820,6 +1015,13 @@ class VoiceConnection extends EventEmitter {
|
|
|
820
1015
|
break;
|
|
821
1016
|
}
|
|
822
1017
|
case 'STREAM_DELETE': {
|
|
1018
|
+
if (
|
|
1019
|
+
[VoiceStatus.CONNECTING, VoiceStatus.AUTHENTICATING, VoiceStatus.RECONNECTING].includes(
|
|
1020
|
+
this.streamConnection.status,
|
|
1021
|
+
)
|
|
1022
|
+
) {
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
823
1025
|
this.streamConnection.disconnect();
|
|
824
1026
|
break;
|
|
825
1027
|
}
|
|
@@ -963,6 +1165,49 @@ class StreamConnection extends VoiceConnection {
|
|
|
963
1165
|
return Promise.resolve(this);
|
|
964
1166
|
}
|
|
965
1167
|
|
|
1168
|
+
setTokenAndEndpoint(token, endpoint) {
|
|
1169
|
+
this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`);
|
|
1170
|
+
if (!endpoint) return;
|
|
1171
|
+
|
|
1172
|
+
if (!token) {
|
|
1173
|
+
this.authenticateFailed('VOICE_TOKEN_ABSENT');
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
endpoint = normalizeVoiceEndpoint(endpoint, { preservePort: true });
|
|
1178
|
+
this.emit('debug', `Endpoint resolved as ${endpoint}`);
|
|
1179
|
+
|
|
1180
|
+
if (!endpoint) {
|
|
1181
|
+
this.authenticateFailed('VOICE_INVALID_ENDPOINT');
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const connect = () => {
|
|
1186
|
+
if (this.status === VoiceStatus.AUTHENTICATING) {
|
|
1187
|
+
this.authentication.token = token;
|
|
1188
|
+
this.authentication.endpoint = endpoint;
|
|
1189
|
+
this.checkAuthenticated();
|
|
1190
|
+
} else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) {
|
|
1191
|
+
this.reconnect(token, endpoint);
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
if (this.status === VoiceStatus.AUTHENTICATING) {
|
|
1196
|
+
setTimeout(connect, 750).unref();
|
|
1197
|
+
} else {
|
|
1198
|
+
connect();
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
onSessionDescription(data) {
|
|
1203
|
+
Object.assign(this.authentication, data);
|
|
1204
|
+
this.status = VoiceStatus.CONNECTED;
|
|
1205
|
+
this._sessionRetries = 0;
|
|
1206
|
+
clearTimeout(this.connectTimeout);
|
|
1207
|
+
this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`);
|
|
1208
|
+
this.emit('ready');
|
|
1209
|
+
}
|
|
1210
|
+
|
|
966
1211
|
joinStreamConnection() {
|
|
967
1212
|
throw new Error('STREAM_CANNOT_JOIN');
|
|
968
1213
|
}
|
|
@@ -1005,10 +1250,11 @@ class StreamConnection extends VoiceConnection {
|
|
|
1005
1250
|
preferred_region: null,
|
|
1006
1251
|
};
|
|
1007
1252
|
this.emit('debug', `Signal Stream: ${JSON.stringify(data)}`);
|
|
1008
|
-
|
|
1253
|
+
this.channel.client.ws.broadcast({
|
|
1009
1254
|
op: Opcodes.STREAM_CREATE,
|
|
1010
1255
|
d: data,
|
|
1011
1256
|
});
|
|
1257
|
+
this.sendScreenshareState(false);
|
|
1012
1258
|
}
|
|
1013
1259
|
|
|
1014
1260
|
/**
|
|
@@ -20,7 +20,8 @@ Please use the @dank074/discord-video-stream library for the best support.
|
|
|
20
20
|
const { Buffer } = require('buffer');
|
|
21
21
|
const VideoDispatcher = require('./VideoDispatcher');
|
|
22
22
|
const Util = require('../../../util/Util');
|
|
23
|
-
const { H264Helpers, H265Helpers } = require('../player/processing/AnnexBNalSplitter');
|
|
23
|
+
const { H264Helpers, H265Helpers, H264NalUnitTypes, H265NalUnitTypes } = require('../player/processing/AnnexBNalSplitter');
|
|
24
|
+
const { rewriteSPSVUI } = require('../player/processing/SPSVUIRewriter');
|
|
24
25
|
|
|
25
26
|
class AnnexBDispatcher extends VideoDispatcher {
|
|
26
27
|
constructor(player, highWaterMark = 12, streams, fps, nalFunctions, payloadType) {
|
|
@@ -28,8 +29,65 @@ class AnnexBDispatcher extends VideoDispatcher {
|
|
|
28
29
|
this._nalFunctions = nalFunctions;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
_rewriteH264SPS(accessUnit) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
let offset = 0;
|
|
35
|
+
while (offset < accessUnit.length) {
|
|
36
|
+
const naluSize = accessUnit.readUInt32BE(offset);
|
|
37
|
+
offset += 4;
|
|
38
|
+
let nalu = accessUnit.subarray(offset, offset + naluSize);
|
|
39
|
+
if (this._nalFunctions === H264Helpers && H264Helpers.getUnitType(nalu) === 7) {
|
|
40
|
+
try {
|
|
41
|
+
nalu = rewriteSPSVUI(nalu);
|
|
42
|
+
} catch {
|
|
43
|
+
// keep original SPS on rewrite failure
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const header = Buffer.alloc(4);
|
|
47
|
+
header.writeUInt32BE(nalu.length);
|
|
48
|
+
parts.push(header, nalu);
|
|
49
|
+
offset += naluSize;
|
|
50
|
+
}
|
|
51
|
+
return Buffer.concat(parts);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_accessUnitIsKeyframe(accessUnit) {
|
|
55
|
+
let offset = 0;
|
|
56
|
+
while (offset < accessUnit.length) {
|
|
57
|
+
const naluSize = accessUnit.readUInt32BE(offset);
|
|
58
|
+
offset += 4;
|
|
59
|
+
const nalu = accessUnit.subarray(offset, offset + naluSize);
|
|
60
|
+
const unitType = this._nalFunctions.getUnitType(nalu);
|
|
61
|
+
if (this._nalFunctions === H264Helpers) {
|
|
62
|
+
if (unitType === H264NalUnitTypes.CodedSliceIdr) return true;
|
|
63
|
+
} else if (
|
|
64
|
+
unitType === H265NalUnitTypes.IDR_W_RADL ||
|
|
65
|
+
unitType === H265NalUnitTypes.IDR_N_LP ||
|
|
66
|
+
unitType === H265NalUnitTypes.CRA_NUT
|
|
67
|
+
) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
offset += naluSize;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_sendVideoPacket(naluPayload, isLastPacket, isKeyframe) {
|
|
76
|
+
const includeContentType = isKeyframe && isLastPacket && this.player.isScreenSharing;
|
|
77
|
+
this._playChunk(
|
|
78
|
+
Buffer.concat([this.createPayloadExtension(includeContentType), naluPayload]),
|
|
79
|
+
isLastPacket,
|
|
80
|
+
{ isKeyframe },
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
31
84
|
_codecCallback(frame) {
|
|
32
|
-
let accessUnit = frame;
|
|
85
|
+
let accessUnit = this._nalFunctions === H264Helpers ? this._rewriteH264SPS(frame) : frame;
|
|
86
|
+
const isKeyframe = this._accessUnitIsKeyframe(accessUnit);
|
|
87
|
+
const dave = this.player.voiceConnection.dave;
|
|
88
|
+
if (dave?.session?.ready) {
|
|
89
|
+
accessUnit = dave.encryptVideo(accessUnit, this.player.voiceConnection.videoCodec);
|
|
90
|
+
}
|
|
33
91
|
let offset = 0;
|
|
34
92
|
|
|
35
93
|
// Extract NALUs from the access unit
|
|
@@ -39,24 +97,19 @@ class AnnexBDispatcher extends VideoDispatcher {
|
|
|
39
97
|
const nalu = accessUnit.subarray(offset, offset + naluSize);
|
|
40
98
|
const isLastNal = offset + naluSize >= accessUnit.length;
|
|
41
99
|
if (nalu.length <= this.mtu) {
|
|
42
|
-
|
|
43
|
-
this._playChunk(Buffer.concat([this.createPayloadExtension(), nalu]), isLastNal);
|
|
100
|
+
this._sendVideoPacket(nalu, isLastNal, isKeyframe);
|
|
44
101
|
} else {
|
|
45
102
|
const [naluHeader, naluData] = this._nalFunctions.splitHeader(nalu);
|
|
46
103
|
const dataFragments = this.partitionMtu(naluData);
|
|
47
|
-
// Send as Fragmentation Unit A (FU-A):
|
|
48
104
|
for (let fragmentIndex = 0; fragmentIndex < dataFragments.length; fragmentIndex++) {
|
|
49
105
|
const data = dataFragments[fragmentIndex];
|
|
50
106
|
const isFirstPacket = fragmentIndex === 0;
|
|
51
107
|
const isFinalPacket = fragmentIndex === dataFragments.length - 1;
|
|
52
108
|
|
|
53
|
-
this.
|
|
54
|
-
Buffer.concat([
|
|
55
|
-
this.createPayloadExtension(),
|
|
56
|
-
this.makeFragmentationUnitHeader(isFirstPacket, isFinalPacket, naluHeader),
|
|
57
|
-
data,
|
|
58
|
-
]),
|
|
109
|
+
this._sendVideoPacket(
|
|
110
|
+
Buffer.concat([this.makeFragmentationUnitHeader(isFirstPacket, isFinalPacket, naluHeader), data]),
|
|
59
111
|
isLastNal && isFinalPacket,
|
|
112
|
+
isKeyframe,
|
|
60
113
|
);
|
|
61
114
|
}
|
|
62
115
|
}
|
|
@@ -5,6 +5,7 @@ const crypto = require('node:crypto');
|
|
|
5
5
|
const { Writable } = require('node:stream');
|
|
6
6
|
const { setTimeout } = require('node:timers');
|
|
7
7
|
const secretbox = require('../util/Secretbox');
|
|
8
|
+
const Speaking = require('../../../util/Speaking');
|
|
8
9
|
|
|
9
10
|
const MAX_UINT_16 = 2 ** 16 - 1;
|
|
10
11
|
const MAX_UINT_32 = 2 ** 32 - 1;
|
|
@@ -232,14 +233,16 @@ class BaseDispatcher extends Writable {
|
|
|
232
233
|
callback();
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
_playChunk(chunk, isLastPacket = false) {
|
|
236
|
+
_playChunk(chunk, isLastPacket = false, packetOpts = {}) {
|
|
236
237
|
if (
|
|
237
238
|
(this.player.dispatcher !== this && this.player.videoDispatcher !== this) ||
|
|
238
239
|
!this.player.voiceConnection.authentication.secret_key
|
|
239
240
|
) {
|
|
240
241
|
return;
|
|
241
242
|
}
|
|
243
|
+
this._packetOpts = packetOpts;
|
|
242
244
|
const packet = this._createPacket(chunk, isLastPacket);
|
|
245
|
+
this._packetOpts = null;
|
|
243
246
|
if (packet) this._sendPacket(packet);
|
|
244
247
|
}
|
|
245
248
|
|
|
@@ -375,7 +378,9 @@ class BaseDispatcher extends Writable {
|
|
|
375
378
|
rtpHeader[0] = 0x80; // Version + Flags (1 byte)
|
|
376
379
|
rtpHeader[1] = this.payloadType; // Payload Type (1 byte)
|
|
377
380
|
if (this.extensionEnabled) {
|
|
378
|
-
|
|
381
|
+
const extensionWordCount =
|
|
382
|
+
typeof this.getExtensionWordCount === 'function' ? this.getExtensionWordCount(isLastPacket) : 1;
|
|
383
|
+
rtpHeader = Buffer.concat([rtpHeader, this.createHeaderExtension(extensionWordCount)]);
|
|
379
384
|
rtpHeader[0] |= 1 << 4; // 0x90
|
|
380
385
|
}
|
|
381
386
|
if (this.getTypeDispatcher() === 'video' && isLastPacket) {
|
|
@@ -384,12 +389,11 @@ class BaseDispatcher extends Writable {
|
|
|
384
389
|
|
|
385
390
|
rtpHeader.writeUIntBE(this.getNewSequence(), 2, 2);
|
|
386
391
|
rtpHeader.writeUIntBE(this.timestamp, 4, 4);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
);
|
|
392
|
-
return Buffer.concat([rtpHeader, ...this._encrypt(buffer, rtpHeader)]);
|
|
392
|
+
const { audioSsrc, videoSsrc } = this.player.voiceConnection.getStreamSsrcs();
|
|
393
|
+
rtpHeader.writeUIntBE(this.getTypeDispatcher() === 'video' ? videoSsrc : audioSsrc, 8, 4);
|
|
394
|
+
const dave = this.player.voiceConnection.dave;
|
|
395
|
+
const payload = this.getTypeDispatcher() === 'audio' && dave?.session?.ready ? dave.encrypt(buffer) : buffer;
|
|
396
|
+
return Buffer.concat([rtpHeader, ...this._encrypt(payload, rtpHeader)]);
|
|
393
397
|
}
|
|
394
398
|
|
|
395
399
|
_sendPacket(packet) {
|
|
@@ -399,7 +403,7 @@ class BaseDispatcher extends Writable {
|
|
|
399
403
|
* @param {string} info The debug info
|
|
400
404
|
*/
|
|
401
405
|
if (this.getTypeDispatcher() === 'audio') {
|
|
402
|
-
this._setSpeaking(this.player.isScreenSharing ?
|
|
406
|
+
this._setSpeaking(this.player.isScreenSharing ? Speaking.FLAGS.SOUNDSHARE : Speaking.FLAGS.SPEAKING);
|
|
403
407
|
} else if (this.getTypeDispatcher() === 'video') {
|
|
404
408
|
this._setVideoStatus(true);
|
|
405
409
|
this._setStreamStatus(false);
|
|
@@ -60,6 +60,39 @@ class VideoDispatcher extends BaseDispatcher {
|
|
|
60
60
|
this.fps = value;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
shouldIncludeContentType(isLastPacket) {
|
|
64
|
+
return Boolean(
|
|
65
|
+
this.player.isScreenSharing && this._packetOpts?.isKeyframe && isLastPacket,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getExtensionWordCount(isLastPacket) {
|
|
70
|
+
return this.shouldIncludeContentType(isLastPacket) ? 2 : 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
createHeaderExtension(extensionWordCount = 1) {
|
|
74
|
+
const profile = Buffer.alloc(4);
|
|
75
|
+
profile[0] = 0xbe;
|
|
76
|
+
profile[1] = 0xde;
|
|
77
|
+
profile.writeInt16BE(extensionWordCount, 2);
|
|
78
|
+
return profile;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createPayloadExtension(includeContentType = false) {
|
|
82
|
+
if (includeContentType) {
|
|
83
|
+
const data = Buffer.alloc(8);
|
|
84
|
+
data[0] = (5 << 4) | 1;
|
|
85
|
+
data.writeUIntBE(10, 1, 2);
|
|
86
|
+
data[3] = 6 << 4;
|
|
87
|
+
data[4] = 0x01;
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
const data = Buffer.alloc(4);
|
|
91
|
+
data[0] = (5 << 4) | 1;
|
|
92
|
+
data.writeUIntBE(10, 1, 2);
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
|
|
63
96
|
_codecCallback() {
|
|
64
97
|
throw new Error('The _codecCallback method must be implemented');
|
|
65
98
|
}
|