djs-selfbot-v13 3.7.32 → 3.7.34

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.
@@ -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
  }
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+ const Davey = require('@snazzah/davey');
5
+
6
+ const TRANSITION_EXPIRY = 10;
7
+ const TRANSITION_EXPIRY_PENDING_DOWNGRADE = 24;
8
+ const DEFAULT_DECRYPTION_FAILURE_TOLERANCE = 36;
9
+
10
+ const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]);
11
+
12
+ function getMaxProtocolVersion() {
13
+ return Davey.DAVE_PROTOCOL_VERSION ?? 0;
14
+ }
15
+
16
+ class DAVESession extends EventEmitter {
17
+ constructor(protocolVersion, userId, channelId, options = {}) {
18
+ super();
19
+ this.protocolVersion = protocolVersion;
20
+ this.userId = userId;
21
+ this.channelId = channelId;
22
+ this.failureTolerance = options.decryptionFailureTolerance ?? DEFAULT_DECRYPTION_FAILURE_TOLERANCE;
23
+ this.pendingTransitions = new Map();
24
+ this.downgraded = false;
25
+ this.consecutiveFailures = 0;
26
+ this.reinitializing = false;
27
+ this.lastTransitionId = undefined;
28
+ this.session = undefined;
29
+ }
30
+
31
+ get voicePrivacyCode() {
32
+ if (this.protocolVersion === 0 || !this.session?.voicePrivacyCode) return null;
33
+ return this.session.voicePrivacyCode;
34
+ }
35
+
36
+ async getVerificationCode(userId) {
37
+ if (!this.session) throw new Error('Session not available');
38
+ return this.session.getVerificationCode(userId);
39
+ }
40
+
41
+ reinit() {
42
+ if (this.protocolVersion > 0) {
43
+ if (this.session) {
44
+ this.session.reinit(this.protocolVersion, this.userId, this.channelId);
45
+ this.emit('debug', `Session reinitialized for protocol version ${this.protocolVersion}`);
46
+ } else {
47
+ this.session = new Davey.DAVESession(this.protocolVersion, this.userId, this.channelId);
48
+ this.emit('debug', `Session initialized for protocol version ${this.protocolVersion}`);
49
+ }
50
+ this.emit('keyPackage', this.session.getSerializedKeyPackage());
51
+ } else if (this.session) {
52
+ this.session.reset();
53
+ this.session.setPassthroughMode(true, TRANSITION_EXPIRY);
54
+ this.emit('debug', 'Session reset');
55
+ }
56
+ }
57
+
58
+ setExternalSender(externalSender) {
59
+ if (!this.session) throw new Error('No session available');
60
+ try {
61
+ this.session.setExternalSender(externalSender);
62
+ this.emit('debug', 'Set MLS external sender');
63
+ } catch (error) {
64
+ if (String(error).includes('AlreadyInGroup')) {
65
+ this.emit('debug', 'MLS external sender already set, skipping');
66
+ return;
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ prepareTransition(data) {
73
+ this.emit('debug', `Preparing for transition (${data.transition_id}, v${data.protocol_version})`);
74
+ this.pendingTransitions.set(data.transition_id, data.protocol_version);
75
+
76
+ if (data.transition_id === 0) {
77
+ this.executeTransition(data.transition_id);
78
+ } else {
79
+ if (data.protocol_version === 0) this.session?.setPassthroughMode(true, TRANSITION_EXPIRY_PENDING_DOWNGRADE);
80
+ return true;
81
+ }
82
+
83
+ return false;
84
+ }
85
+
86
+ executeTransition(transitionId) {
87
+ this.emit('debug', `Executing transition (${transitionId})`);
88
+ if (!this.pendingTransitions.has(transitionId)) {
89
+ this.emit('debug', `Received execute transition, but we don't have a pending transition for ${transitionId}`);
90
+ return false;
91
+ }
92
+
93
+ const oldVersion = this.protocolVersion;
94
+ this.protocolVersion = this.pendingTransitions.get(transitionId);
95
+
96
+ if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {
97
+ this.downgraded = true;
98
+ this.emit('debug', 'Session downgraded');
99
+ } else if (transitionId > 0 && this.downgraded) {
100
+ this.downgraded = false;
101
+ this.session?.setPassthroughMode(true, TRANSITION_EXPIRY);
102
+ this.emit('debug', 'Session upgraded');
103
+ }
104
+
105
+ this.reinitializing = false;
106
+ this.lastTransitionId = transitionId;
107
+ this.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);
108
+ this.pendingTransitions.delete(transitionId);
109
+ return true;
110
+ }
111
+
112
+ prepareEpoch(data) {
113
+ this.emit('debug', `Preparing for epoch (${data.epoch})`);
114
+ if (data.epoch === 1) {
115
+ this.protocolVersion = data.protocol_version;
116
+ this.reinit();
117
+ }
118
+ }
119
+
120
+ recoverFromInvalidTransition(transitionId) {
121
+ if (this.reinitializing) return;
122
+ this.emit('debug', `Invalidating transition ${transitionId}`);
123
+ this.reinitializing = true;
124
+ this.consecutiveFailures = 0;
125
+ this.emit('invalidateTransition', transitionId);
126
+ this.reinit();
127
+ }
128
+
129
+ processProposals(payload, connectedClients) {
130
+ if (!this.session) throw new Error('No session available');
131
+ try {
132
+ const optype = payload.readUInt8(0);
133
+ const { commit, welcome } = this.session.processProposals(
134
+ optype,
135
+ payload.subarray(1),
136
+ Array.from(connectedClients),
137
+ );
138
+ this.emit('debug', 'MLS proposals processed');
139
+ if (!commit) return;
140
+ return welcome ? Buffer.concat([commit, welcome]) : commit;
141
+ } catch (error) {
142
+ this.emit('debug', `MLS proposals errored: ${error}`);
143
+ return null;
144
+ }
145
+ }
146
+
147
+ processCommit(payload) {
148
+ if (!this.session) throw new Error('No session available');
149
+ const transitionId = payload.readUInt16BE(0);
150
+ try {
151
+ this.session.processCommit(payload.subarray(2));
152
+ if (transitionId === 0) {
153
+ this.reinitializing = false;
154
+ this.lastTransitionId = transitionId;
155
+ } else {
156
+ this.pendingTransitions.set(transitionId, this.protocolVersion);
157
+ }
158
+ this.emit('debug', `MLS commit processed (transition id: ${transitionId})`);
159
+ return { transitionId, success: true };
160
+ } catch (error) {
161
+ this.emit('debug', `MLS commit errored from transition ${transitionId}: ${error}`);
162
+ this.recoverFromInvalidTransition(transitionId);
163
+ return { transitionId, success: false };
164
+ }
165
+ }
166
+
167
+ processWelcome(payload) {
168
+ if (!this.session) throw new Error('No session available');
169
+ const transitionId = payload.readUInt16BE(0);
170
+ try {
171
+ this.session.processWelcome(payload.subarray(2));
172
+ if (transitionId === 0) {
173
+ this.reinitializing = false;
174
+ this.lastTransitionId = transitionId;
175
+ } else {
176
+ this.pendingTransitions.set(transitionId, this.protocolVersion);
177
+ }
178
+ this.emit('debug', `MLS welcome processed (transition id: ${transitionId})`);
179
+ return { transitionId, success: true };
180
+ } catch (error) {
181
+ this.emit('debug', `MLS welcome errored from transition ${transitionId}: ${error}`);
182
+ this.recoverFromInvalidTransition(transitionId);
183
+ return { transitionId, success: false };
184
+ }
185
+ }
186
+
187
+ encrypt(packet) {
188
+ if (this.protocolVersion === 0 || !this.session?.ready || packet.equals(SILENCE_FRAME)) return packet;
189
+ return this.session.encryptOpus(packet);
190
+ }
191
+
192
+ encryptVideo(packet, codec = 'H264') {
193
+ if (this.protocolVersion === 0 || !this.session?.ready) return packet;
194
+ const codecMap = {
195
+ H264: Davey.Codec.H264,
196
+ VP8: Davey.Codec.VP8,
197
+ H265: Davey.Codec.H265,
198
+ AV1: Davey.Codec.AV1,
199
+ };
200
+ return this.session.encrypt(Davey.MediaType.VIDEO, codecMap[codec] ?? Davey.Codec.UNKNOWN, packet);
201
+ }
202
+
203
+ decrypt(packet, userId) {
204
+ const canDecrypt = this.session?.ready && (this.protocolVersion !== 0 || this.session?.canPassthrough(userId));
205
+ if (packet.equals(SILENCE_FRAME) || !canDecrypt || !this.session) return packet;
206
+ try {
207
+ const buffer = this.session.decrypt(userId, Davey.MediaType.AUDIO, packet);
208
+ this.consecutiveFailures = 0;
209
+ return buffer;
210
+ } catch (error) {
211
+ if (!this.reinitializing && this.pendingTransitions.size === 0) {
212
+ this.consecutiveFailures++;
213
+ this.emit('debug', `Failed to decrypt a packet (${this.consecutiveFailures} consecutive fails)`);
214
+ if (this.consecutiveFailures > this.failureTolerance) {
215
+ if (this.lastTransitionId) this.recoverFromInvalidTransition(this.lastTransitionId);
216
+ else throw error;
217
+ }
218
+ } else if (this.reinitializing) {
219
+ this.emit('debug', 'Failed to decrypt a packet (reinitializing session)');
220
+ } else if (this.pendingTransitions.size > 0) {
221
+ this.emit('debug', `Failed to decrypt a packet (${this.pendingTransitions.size} pending transition[s])`);
222
+ }
223
+ }
224
+ return null;
225
+ }
226
+
227
+ destroy() {
228
+ try {
229
+ this.session?.reset();
230
+ } catch {}
231
+ }
232
+ }
233
+
234
+ module.exports = { DAVESession, getMaxProtocolVersion };