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.
@@ -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: this.authentication.ssrc,
259
- video_ssrc: this.authentication.ssrc + 1,
260
- rtx_ssrc: this.authentication.ssrc + 2,
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: this.authentication.ssrc + 1,
304
+ ssrc: videoSsrc,
266
305
  active: true,
267
306
  quality: 100,
268
- rtx_ssrc: this.authentication.ssrc + 2,
269
- max_bitrate: 8000000,
270
- max_framerate: 60,
307
+ rtx_ssrc: rtxSsrc,
308
+ max_bitrate: 10_000_000,
309
+ max_framerate: fps,
271
310
  max_resolution: {
272
- type: 'source',
273
- width: 0,
274
- height: 0,
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'), 15_000).unref();
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
- if (this.dispatcher || this.videoDispatcher) {
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.sendScreenshareState(true);
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
- return this.channel.client.ws.broadcast({
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
- // Send as Single NAL Unit Packet.
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._playChunk(
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
- rtpHeader = Buffer.concat([rtpHeader, this.createHeaderExtension()]);
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
- rtpHeader.writeUIntBE(
388
- this.player.voiceConnection.authentication.ssrc + Number(this.getTypeDispatcher() === 'video'),
389
- 8,
390
- 4,
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 ? 1 << 1 : 1 << 0); // 1 << 0 = SPEAKING, 1 << 1 = SOUND SHARE
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
  }