rockcord 4.0.0-dev.4 → 4.0.0

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.
Files changed (69) hide show
  1. package/README.md +5 -3
  2. package/package.json +17 -14
  3. package/src/client/Client.js +3 -3
  4. package/src/client/WebhookClient.js +1 -1
  5. package/src/client/actions/VoiceStateUpdate.js +2 -2
  6. package/src/client/voice/VoiceConnection.js +98 -24
  7. package/src/client/voice/dispatcher/AudioDispatcher.js +1 -1
  8. package/src/client/voice/dispatcher/BaseDispatcher.js +49 -61
  9. package/src/client/voice/dispatcher/VideoDispatcher.js +86 -2
  10. package/src/client/voice/networking/CongestionControl.js +84 -0
  11. package/src/client/voice/networking/DAVE.js +26 -1
  12. package/src/client/voice/networking/MediaRecovery.js +300 -0
  13. package/src/client/voice/networking/VoiceUDPClient.js +10 -1
  14. package/src/client/voice/networking/VoiceWebSocket.js +4 -1
  15. package/src/client/voice/player/MediaPlayer.js +45 -11
  16. package/src/client/voice/player/processing/AnnexBNalSplitter.js +0 -1
  17. package/src/client/voice/receiver/AudioJitter.js +101 -0
  18. package/src/client/voice/receiver/PacketHandler.js +66 -9
  19. package/src/client/voice/receiver/Receiver.js +9 -4
  20. package/src/client/voice/receiver/Recorder.js +186 -57
  21. package/src/client/voice/util/PlayInterface.js +2 -0
  22. package/src/client/voice/util/Secretbox.js +1 -1
  23. package/src/client/voice/util/TransportCrypto.js +54 -0
  24. package/src/client/voice/util/VideoFrames.js +4 -1
  25. package/src/client/websocket/WebSocketShard.js +0 -1
  26. package/src/managers/ApplicationCommandManager.js +0 -2
  27. package/src/managers/ApplicationCommandPermissionsManager.js +0 -1
  28. package/src/managers/GuildChannelManager.js +2 -3
  29. package/src/managers/GuildMemberManager.js +0 -1
  30. package/src/managers/GuildTextThreadManager.js +1 -1
  31. package/src/managers/MessageManager.js +5 -5
  32. package/src/managers/RoleManager.js +1 -1
  33. package/src/rest/APIRequest.js +0 -1
  34. package/src/rest/APIRouter.js +2 -2
  35. package/src/rest/DiscordAPIError.js +1 -1
  36. package/src/rest/RequestHandler.js +3 -3
  37. package/src/sharding/ShardingManager.js +2 -2
  38. package/src/structures/ApplicationCommand.js +2 -7
  39. package/src/structures/BaseCommandInteraction.js +1 -2
  40. package/src/structures/BaseGuildTextChannel.js +1 -1
  41. package/src/structures/BaseGuildVoiceChannel.js +1 -1
  42. package/src/structures/ClientPresence.js +0 -1
  43. package/src/structures/ClientUser.js +13 -5
  44. package/src/structures/ContainerComponent.js +1 -1
  45. package/src/structures/DMChannel.js +1 -1
  46. package/src/structures/GroupDMChannel.js +4 -4
  47. package/src/structures/Guild.js +0 -2
  48. package/src/structures/GuildAuditLogs.js +4 -4
  49. package/src/structures/GuildScheduledEvent.js +0 -2
  50. package/src/structures/Interaction.js +1 -1
  51. package/src/structures/InteractionCollector.js +2 -2
  52. package/src/structures/InteractionWebhook.js +0 -1
  53. package/src/structures/Message.js +16 -17
  54. package/src/structures/MessageComponentInteraction.js +1 -1
  55. package/src/structures/MessageEmbed.js +0 -2
  56. package/src/structures/MessagePayload.js +2 -2
  57. package/src/structures/ModalSubmitInteraction.js +2 -2
  58. package/src/structures/ThreadChannel.js +3 -3
  59. package/src/structures/ThreadOnlyChannel.js +1 -1
  60. package/src/structures/WebEmbed.js +0 -1
  61. package/src/structures/interfaces/Application.js +2 -2
  62. package/src/structures/interfaces/Collector.js +1 -4
  63. package/src/structures/interfaces/TextBasedChannel.js +6 -6
  64. package/src/util/AttachmentFlags.js +0 -1
  65. package/src/util/Constants.js +0 -2
  66. package/src/util/Options.js +3 -3
  67. package/src/util/Util.js +1 -2
  68. package/typings/index.d.ts +109 -115
  69. package/typings/rawDataTypes.d.ts +5 -15
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const { setTimeout } = require('node:timers');
3
4
  const BaseDispatcher = require('./BaseDispatcher');
5
+ const CongestionControl = require('../networking/CongestionControl');
4
6
 
5
7
  /**
6
8
  * The class that sends video packet data to the voice connection.
@@ -14,13 +16,13 @@ const BaseDispatcher = require('./BaseDispatcher');
14
16
  * @extends {BaseDispatcher}
15
17
  */
16
18
  class VideoDispatcher extends BaseDispatcher {
17
- constructor(player, highWaterMark = 12, streams, fps, payloadType) {
19
+ constructor(player, highWaterMark = 12, streams, fps = 30, payloadType) {
18
20
  super(player, highWaterMark, payloadType, true, streams);
19
21
  /**
20
22
  * Video FPS
21
23
  * @type {number}
22
24
  */
23
- this.fps = fps;
25
+ this.setFPSSource(fps);
24
26
 
25
27
  this.mtu = 1200;
26
28
  }
@@ -57,12 +59,94 @@ class VideoDispatcher extends BaseDispatcher {
57
59
  * @param {number} value fps
58
60
  */
59
61
  setFPSSource(value) {
62
+ if (!Number.isFinite(value) || value <= 0) throw new RangeError('Video FPS must be a positive finite number');
60
63
  this.fps = value;
61
64
  }
62
65
 
63
66
  _codecCallback() {
64
67
  throw new Error('The _codecCallback method must be implemented');
65
68
  }
69
+
70
+ configureCongestion(options = {}) {
71
+ if (options.congestionControl === false) return;
72
+ const config = typeof options.congestionControl === 'object' ? options.congestionControl : {};
73
+ const bitrate = config.bitrate ?? (typeof options.bitrate === 'number' ? options.bitrate : 2000);
74
+ this.congestionControl = new CongestionControl({
75
+ bitrate,
76
+ minBitrate: Math.min(128, bitrate),
77
+ ...config,
78
+ onChange: state => this.emit('congestion', state),
79
+ });
80
+ this.player.voiceConnection._congestionControl = this.congestionControl;
81
+ }
82
+
83
+ _write(chunk, encoding, done) {
84
+ if (this.congestionControl) {
85
+ this._pacedPackets = [];
86
+ this._pacedBytes = 0;
87
+ this._pacingKey = this.player.voiceConnection.authentication.secret_key;
88
+ }
89
+ super._write(chunk, encoding, done);
90
+ }
91
+
92
+ _sendPacket(packet) {
93
+ if (!this._pacedPackets) return super._sendPacket(packet);
94
+ this._pacedBytes += packet.length;
95
+ if (this._pacedBytes > 4 * 1024 * 1024 || this._pacedPackets.length >= 4096) {
96
+ throw new RangeError('Video frame exceeds the pacing queue limit');
97
+ }
98
+ this._pacedPackets.push(packet);
99
+ return undefined;
100
+ }
101
+
102
+ _step(done) {
103
+ if (!this._pacedPackets) return super._step(done);
104
+ const packets = this._pacedPackets;
105
+ this._pacedPackets = null;
106
+ let index = 0;
107
+ const sendNext = () => {
108
+ if (this.destroyed) return;
109
+ if (this.paused) {
110
+ this._pacingTimer = setTimeout(sendNext, 20).unref();
111
+ return;
112
+ }
113
+ if (index === packets.length) {
114
+ super._step(done);
115
+ return;
116
+ }
117
+ const packet = packets[index++];
118
+ const delay = this.congestionControl.delay(packet.length);
119
+ this._pacingTimer = setTimeout(
120
+ () => {
121
+ if (this.destroyed) return;
122
+ if (this._pacingKey !== this.player.voiceConnection.authentication.secret_key) {
123
+ this.destroy(new Error('Video transport key changed during paced playback'));
124
+ return;
125
+ }
126
+ if (this.paused) {
127
+ index--;
128
+ sendNext();
129
+ return;
130
+ }
131
+ this.congestionControl.packetSent();
132
+ super._sendPacket(packet);
133
+ sendNext();
134
+ },
135
+ Math.max(0, delay),
136
+ ).unref();
137
+ };
138
+ sendNext();
139
+ return undefined;
140
+ }
141
+
142
+ _cleanup() {
143
+ clearTimeout(this._pacingTimer);
144
+ this._pacedPackets = null;
145
+ if (this.player.voiceConnection._congestionControl === this.congestionControl) {
146
+ this.player.voiceConnection._congestionControl = null;
147
+ }
148
+ super._cleanup();
149
+ }
66
150
  }
67
151
 
68
152
  module.exports = VideoDispatcher;
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ /** Loss-based AIMD rate control and bounded video packet pacing. @private */
4
+ class CongestionControl {
5
+ constructor({ bitrate = 2000, minBitrate = 128, onChange = () => {} } = {}) {
6
+ if (!Number.isFinite(bitrate) || !Number.isFinite(minBitrate) || minBitrate <= 0 || bitrate < minBitrate) {
7
+ throw new RangeError('Congestion bitrates must be finite, positive and ordered');
8
+ }
9
+ this.maximum = bitrate * 1000;
10
+ this.minimum = minBitrate * 1000;
11
+ this.targetBitrate = this.maximum;
12
+ this.onChange = onChange;
13
+ this.loss = 0;
14
+ this.reason = 'initial';
15
+ this.nextSendAt = 0;
16
+ this.windowStart = performance.now();
17
+ this.lastUpdate = -Infinity;
18
+ this.lastReport = -Infinity;
19
+ this.sent = 0;
20
+ this.totalSent = 0;
21
+ this.missing = new Set();
22
+ this.reports = new Map();
23
+ }
24
+
25
+ get state() {
26
+ return { targetBitrate: this.targetBitrate, loss: this.loss, packetsSent: this.totalSent, reason: this.reason };
27
+ }
28
+
29
+ report(loss, highest, reporter, now = performance.now()) {
30
+ if (!Number.isFinite(loss) || loss < 0 || loss > 1) return;
31
+ const previous = this.reports.get(reporter);
32
+ const distance = previous ? (highest - previous.highest) >>> 0 : 1;
33
+ if (distance >= 0x80000000 || !distance) return;
34
+ for (const [id, report] of this.reports) if (now - report.time > 3000) this.reports.delete(id);
35
+ if (!this.reports.has(reporter) && this.reports.size >= 32) return;
36
+ this.reports.set(reporter, { highest, loss, time: now });
37
+ this.lastReport = now;
38
+ this._update(Math.max(...[...this.reports.values()].map(report => report.loss)), now, true);
39
+ }
40
+
41
+ nack(sequence) {
42
+ if (this.missing.size < 512) this.missing.add(sequence);
43
+ }
44
+
45
+ packetSent(now = performance.now()) {
46
+ if (now - this.windowStart >= 1000) {
47
+ if (now - this.lastReport > 2000 && this.sent >= 20 && this.missing.size) {
48
+ this._update(Math.min(1, this.missing.size / this.sent), now, false);
49
+ }
50
+ this.windowStart = now;
51
+ this.sent = 0;
52
+ this.missing.clear();
53
+ }
54
+ this.sent++;
55
+ this.totalSent++;
56
+ }
57
+
58
+ _update(loss, now, canIncrease) {
59
+ if (now - this.lastUpdate < 1000) return;
60
+ this.lastUpdate = now;
61
+ this.loss = loss;
62
+ const before = this.targetBitrate;
63
+ if (loss >= 0.1) {
64
+ this.targetBitrate *= 0.75;
65
+ this.reason = 'high-loss';
66
+ } else if (loss >= 0.02) {
67
+ this.targetBitrate *= 0.9;
68
+ this.reason = 'loss';
69
+ } else if (canIncrease && loss < 0.02) {
70
+ this.targetBitrate += Math.max(8000, this.targetBitrate * 0.05);
71
+ this.reason = 'recovery';
72
+ }
73
+ this.targetBitrate = Math.round(Math.max(this.minimum, Math.min(this.maximum, this.targetBitrate)));
74
+ if (before !== this.targetBitrate) this.onChange(this.state);
75
+ }
76
+
77
+ delay(bytes, now = performance.now()) {
78
+ const due = Math.max(now, this.nextSendAt);
79
+ this.nextSendAt = due + (bytes * 8000) / this.targetBitrate;
80
+ return due - now;
81
+ }
82
+ }
83
+
84
+ module.exports = CongestionControl;
@@ -51,6 +51,8 @@ class DAVE {
51
51
  }
52
52
 
53
53
  reinit(version = this.targetVersion) {
54
+ this._mediaStarted = false;
55
+ this._bootstrapSender = null;
54
56
  this.validateVersion(version);
55
57
  this.blocked = true;
56
58
  if (!version) return;
@@ -81,6 +83,8 @@ class DAVE {
81
83
  this.version = this.targetVersion = this.pending.get(id);
82
84
  this.pending.delete(id);
83
85
  this.lastTransition = id;
86
+ this._mediaStarted = false;
87
+ this._bootstrapSender = null;
84
88
  this.recovering = false;
85
89
  this.blocked = this.version > 0 && !this.session?.ready;
86
90
  // When upgrading, expire the explicitly permitted plaintext grace period after ten seconds.
@@ -137,7 +141,11 @@ class DAVE {
137
141
  encrypt(frame, codec = 'OPUS') {
138
142
  if (this.closed || !this.negotiated) return null;
139
143
  // The protocol's Opus silence marker is exempt from frame encryption, including while joining.
140
- if (codec === 'OPUS' && SILENCE_FRAME.equals(frame)) return frame;
144
+ if (codec === 'OPUS' && SILENCE_FRAME.equals(frame)) {
145
+ // Exempt silence must not unlock nonce-zero startup ordering.
146
+ this._bootstrapSender = null;
147
+ return frame;
148
+ }
141
149
  if (this.blocked) return null;
142
150
  if (this.version === 0) return frame;
143
151
  if (!this.session?.ready) return null;
@@ -146,6 +154,23 @@ class DAVE {
146
154
  return this.session.encrypt(codec === 'OPUS' ? api.MediaType.AUDIO : api.MediaType.VIDEO, api.Codec[codec], frame);
147
155
  }
148
156
 
157
+ beginMedia(sender) {
158
+ if (this._mediaStarted || !this.version || this.blocked || !this.negotiated || !this.session?.ready) return true;
159
+ if (this._bootstrapSender && this._bootstrapSender !== sender) return false;
160
+ this._bootstrapSender = sender;
161
+ return true;
162
+ }
163
+
164
+ sentMedia(sender) {
165
+ if (this._bootstrapSender !== sender) return;
166
+ this._mediaStarted = true;
167
+ this._bootstrapSender = null;
168
+ }
169
+
170
+ releaseMedia(sender) {
171
+ if (this._bootstrapSender === sender) this._bootstrapSender = null;
172
+ }
173
+
149
174
  decrypt(frame, userId, video = false) {
150
175
  if (this.closed || !this.negotiated) return null;
151
176
  if (!video && SILENCE_FRAME.equals(frame)) return frame;
@@ -0,0 +1,300 @@
1
+ 'use strict';
2
+
3
+ const { Buffer } = require('node:buffer');
4
+ const { setInterval, setTimeout } = require('node:timers');
5
+ const Util = require('../../../util/Util');
6
+ const { encrypt, decrypt } = require('../util/TransportCrypto');
7
+
8
+ /** Bounded RFC 4585 NACK feedback and RFC 4588 video RTX handling. @private */
9
+ class MediaRecovery {
10
+ constructor(connection) {
11
+ this.connection = connection;
12
+ this.cache = new Map();
13
+ this.sources = new Map();
14
+ this.pendingRtx = new Set();
15
+ this.rtxSequence = 0;
16
+ this.stats = { requested: 0, retransmitted: 0, recovered: 0, expired: 0 };
17
+ }
18
+
19
+ remember(header, payload) {
20
+ const type = Util.getAllPayloadType().find(
21
+ codec => codec.rtx_payload_type && codec.payload_type === (header[1] & 127),
22
+ );
23
+ if (!type || payload.length > 4096) return;
24
+ const key = Buffer.from(this.connection.authentication.secret_key);
25
+ if (!this.key?.equals(key)) {
26
+ this.reset();
27
+ this.key = key;
28
+ }
29
+ const now = performance.now();
30
+ for (const [id, item] of this.cache) if (now - item.time > 2000) this.cache.delete(id);
31
+ if (this.cache.size >= 512) this.cache.delete(this.cache.keys().next().value);
32
+ const extension = header[0] & 0x10 ? header.readUInt16BE(header.length - 2) * 4 : 0;
33
+ this.cache.set(`${header.readUInt32BE(8)}:${header.readUInt16BE(2)}`, {
34
+ header: Buffer.from(header.subarray(0, 12)),
35
+ payload: Buffer.from(payload.subarray(extension)),
36
+ type: type.rtx_payload_type,
37
+ time: now,
38
+ retries: 0,
39
+ last: -Infinity,
40
+ });
41
+ }
42
+
43
+ receive(packet, retransmission = false) {
44
+ const { ssrc, sequenceNumber: seq } = packet.header;
45
+ let source = this.sources.get(ssrc);
46
+ if (retransmission) {
47
+ if (source?.missing.delete(seq)) this.stats.recovered++;
48
+ return;
49
+ }
50
+ if (!source) {
51
+ if (this.sources.size >= 32) return;
52
+ this.sources.set(ssrc, {
53
+ highest: seq,
54
+ highestExtended: seq,
55
+ first: seq,
56
+ received: 1,
57
+ seen: new Set([seq]),
58
+ missing: new Map(),
59
+ reportedExpected: 0,
60
+ reportedReceived: 0,
61
+ });
62
+ if (!this.reportTimer) this.reportTimer = setInterval(() => this._report(), 1000).unref();
63
+ return;
64
+ }
65
+ if (!source.seen.has(seq)) {
66
+ const age = (source.highest - seq) & 65535;
67
+ if (age < 128 || age >= 32768) source.received++;
68
+ source.seen.add(seq);
69
+ if (source.seen.size > 512) source.seen.delete(source.seen.values().next().value);
70
+ }
71
+ if (source.missing.delete(seq)) this.stats.recovered++;
72
+ const distance = (seq - source.highest) & 65535;
73
+ if (!distance || distance >= 32768) return;
74
+ if (distance > 128) {
75
+ this.stats.expired += source.missing.size;
76
+ source.missing.clear();
77
+ } else {
78
+ const time = performance.now();
79
+ for (let i = 1; i < distance; i++) {
80
+ if (source.missing.size >= 128) break;
81
+ source.missing.set((source.highest + i) & 65535, { time, last: -Infinity, retries: 0 });
82
+ }
83
+ }
84
+ source.highest = seq;
85
+ source.highestExtended += distance;
86
+ if (source.missing.size && !this.timer) this.timer = setInterval(() => this._tick(), 20).unref();
87
+ }
88
+
89
+ _tick() {
90
+ const now = performance.now();
91
+ let pending = false;
92
+ for (const [ssrc, source] of this.sources) {
93
+ const sequences = [];
94
+ for (const [seq, item] of source.missing) {
95
+ if (now - item.time >= 500) {
96
+ source.missing.delete(seq);
97
+ this.stats.expired++;
98
+ } else if (now - item.time >= 20 && now - item.last >= 60 && item.retries < 3) {
99
+ item.last = now;
100
+ item.retries++;
101
+ sequences.push(seq);
102
+ }
103
+ }
104
+ if (sequences.length) this._nack(ssrc, sequences);
105
+ pending ||= source.missing.size > 0;
106
+ }
107
+ if (!pending) {
108
+ clearInterval(this.timer);
109
+ this.timer = null;
110
+ }
111
+ }
112
+
113
+ _nack(ssrc, sequences) {
114
+ const entries = [];
115
+ for (const seq of sequences) {
116
+ const entry = entries.at(-1);
117
+ const delta = entry ? (seq - entry.seq) & 65535 : 0;
118
+ if (entry && delta > 0 && delta <= 16) entry.mask |= 1 << (delta - 1);
119
+ else entries.push({ seq, mask: 0 });
120
+ }
121
+ const packet = Buffer.alloc(12 + entries.length * 4);
122
+ packet[0] = 0x81;
123
+ packet[1] = 205;
124
+ packet.writeUInt16BE(packet.length / 4 - 1, 2);
125
+ packet.writeUInt32BE(this.connection.authentication.ssrc, 4);
126
+ packet.writeUInt32BE(ssrc, 8);
127
+ entries.forEach((entry, index) => {
128
+ packet.writeUInt16BE(entry.seq, 12 + index * 4);
129
+ packet.writeUInt16BE(entry.mask, 14 + index * 4);
130
+ });
131
+ this.stats.requested += sequences.length;
132
+ try {
133
+ const header = packet.subarray(0, 8);
134
+ this._send(Buffer.concat([header, ...encrypt(this.connection, packet.subarray(8), header)]));
135
+ } catch (error) {
136
+ this.connection.receiver?.emit('debug', error);
137
+ }
138
+ }
139
+
140
+ handle(buffer) {
141
+ let packet;
142
+ try {
143
+ packet = decrypt(this.connection, buffer, 8);
144
+ } catch {
145
+ return;
146
+ }
147
+ const messages = [];
148
+ let offset = 0;
149
+ while (offset + 8 <= packet.length) {
150
+ const size = (packet.readUInt16BE(offset + 2) + 1) * 4;
151
+ if (packet[offset] >> 6 !== 2 || size < 8 || offset + size > packet.length) return;
152
+ if (packet[offset + 1] === 205 && (packet[offset] & 31) === 1 && size >= 16 && size % 4 === 0) {
153
+ messages.push(packet.subarray(offset, offset + size));
154
+ } else if (packet[offset + 1] === 201 || packet[offset + 1] === 200) {
155
+ const base = packet[offset + 1] === 201 ? 8 : 28;
156
+ if (size < base + (packet[offset] & 31) * 24) return;
157
+ messages.push(packet.subarray(offset, offset + size));
158
+ }
159
+ offset += size;
160
+ }
161
+ if (offset !== packet.length) return;
162
+ for (const message of messages) {
163
+ if (message[1] === 205) this._handleNack(message);
164
+ else this._handleReport(message);
165
+ }
166
+ }
167
+
168
+ _handleReport(packet) {
169
+ const control = this.connection._congestionControl;
170
+ if (!control) return;
171
+ const reporter = packet.readUInt32BE(4);
172
+ const videoSsrc = this.connection.authentication.ssrc + 1;
173
+ const base = packet[1] === 201 ? 8 : 28;
174
+ for (let index = 0; index < (packet[0] & 31); index++) {
175
+ const offset = base + index * 24;
176
+ if (packet.readUInt32BE(offset) === videoSsrc) {
177
+ control.report(packet[offset + 4] / 256, packet.readUInt32BE(offset + 8), reporter);
178
+ }
179
+ }
180
+ }
181
+
182
+ _report() {
183
+ for (const [ssrc, source] of this.sources) {
184
+ const expected = source.highestExtended - source.first + 1;
185
+ const intervalExpected = expected - source.reportedExpected;
186
+ const intervalReceived = source.received - source.reportedReceived;
187
+ source.reportedExpected = expected;
188
+ source.reportedReceived = source.received;
189
+ if (!intervalExpected) continue;
190
+ const packet = Buffer.alloc(32);
191
+ packet[0] = 0x81;
192
+ packet[1] = 201;
193
+ packet.writeUInt16BE(7, 2);
194
+ packet.writeUInt32BE(this.connection.authentication.ssrc, 4);
195
+ packet.writeUInt32BE(ssrc, 8);
196
+ packet[12] = Math.max(
197
+ 0,
198
+ Math.min(255, Math.floor(((intervalExpected - intervalReceived) * 256) / intervalExpected)),
199
+ );
200
+ packet.writeUIntBE(Math.max(0, Math.min(0x7fffff, expected - source.received)), 13, 3);
201
+ packet.writeUInt32BE(source.highestExtended >>> 0, 16);
202
+ try {
203
+ const header = packet.subarray(0, 8);
204
+ this._send(Buffer.concat([header, ...encrypt(this.connection, packet.subarray(8), header)]));
205
+ } catch (error) {
206
+ this.connection.receiver?.emit('debug', error);
207
+ }
208
+ }
209
+ }
210
+
211
+ _handleNack(packet) {
212
+ const now = performance.now();
213
+ const ssrc = packet.readUInt32BE(8);
214
+ for (let offset = 12; offset < packet.length; offset += 4) {
215
+ const seq = packet.readUInt16BE(offset),
216
+ mask = packet.readUInt16BE(offset + 2);
217
+ this._retransmit(ssrc, seq, now);
218
+ for (let bit = 0; bit < 16; bit++) {
219
+ if (mask & (1 << bit)) this._retransmit(ssrc, (seq + bit + 1) & 65535, now);
220
+ }
221
+ }
222
+ }
223
+
224
+ _retransmit(ssrc, sequence, now) {
225
+ const item = this.cache.get(`${ssrc}:${sequence}`);
226
+ const auth = this.connection.authentication;
227
+ if (
228
+ !item ||
229
+ !auth.secret_key ||
230
+ !this.key?.equals(Buffer.from(auth.secret_key)) ||
231
+ now - item.time > 2000 ||
232
+ item.retries >= 3 ||
233
+ now - item.last < 20
234
+ ) {
235
+ return;
236
+ }
237
+ const stream = auth.streams?.find(value => value.ssrc === ssrc);
238
+ const rtx = stream?.rtx_ssrc ?? (ssrc === auth.ssrc + 1 ? auth.ssrc + 2 : null);
239
+ if (!rtx) return;
240
+ item.retries++;
241
+ item.last = now;
242
+ if (ssrc === auth.ssrc + 1) this.connection._congestionControl?.nack(sequence);
243
+ const header = Buffer.from(item.header);
244
+ header[0] = 0x80;
245
+ header[1] = (header[1] & 0x80) | item.type;
246
+ header.writeUInt16BE(this.rtxSequence++ & 65535, 2);
247
+ header.writeUInt32BE(rtx, 8);
248
+ const original = Buffer.alloc(2);
249
+ original.writeUInt16BE(sequence);
250
+ const send = () => {
251
+ const current = this.connection.authentication;
252
+ if (
253
+ !current.secret_key ||
254
+ !this.key?.equals(Buffer.from(current.secret_key)) ||
255
+ performance.now() - item.time > 2000
256
+ )
257
+ return;
258
+ try {
259
+ this._send(
260
+ Buffer.concat([header, ...encrypt(this.connection, Buffer.concat([original, item.payload]), header)]),
261
+ );
262
+ this.stats.retransmitted++;
263
+ } catch (error) {
264
+ this.connection.receiver?.emit('debug', error);
265
+ }
266
+ };
267
+ const control = this.connection._congestionControl;
268
+ if (!control) {
269
+ send();
270
+ return;
271
+ }
272
+ if (this.pendingRtx.size >= 128) return;
273
+ if (control.nextSendAt - performance.now() > 500) return;
274
+ const delay = control.delay(header.length + original.length + item.payload.length + 20);
275
+ if (delay > 500) return;
276
+ const timer = setTimeout(() => {
277
+ this.pendingRtx.delete(timer);
278
+ send();
279
+ }, delay).unref();
280
+ this.pendingRtx.add(timer);
281
+ }
282
+
283
+ _send(packet) {
284
+ this.connection.sockets?.udp?.send(packet).catch(error => this.connection.receiver?.emit('debug', error));
285
+ }
286
+
287
+ reset() {
288
+ clearInterval(this.timer);
289
+ clearInterval(this.reportTimer);
290
+ this.reportTimer = null;
291
+ this.timer = null;
292
+ this.cache.clear();
293
+ this.sources.clear();
294
+ for (const timer of this.pendingRtx) clearTimeout(timer);
295
+ this.pendingRtx.clear();
296
+ this.rtxSequence = 0;
297
+ }
298
+ }
299
+
300
+ module.exports = MediaRecovery;
@@ -137,7 +137,16 @@ class VoiceConnectionUDPClient extends EventEmitter {
137
137
  op: VoiceOpcodes.SELECT_PROTOCOL,
138
138
  d: {
139
139
  protocol: 'udp',
140
- codecs: Util.getAllPayloadType(),
140
+ codecs: Util.getAllPayloadType().map(codec =>
141
+ codec.type === 'video'
142
+ ? {
143
+ ...codec,
144
+ priority: codec.name === this.voiceConnection.videoCodec ? 1 : codec.priority,
145
+ encode: codec.name === this.voiceConnection.videoCodec,
146
+ decode: ['H264', 'H265', 'VP8'].includes(codec.name),
147
+ }
148
+ : codec,
149
+ ),
141
150
  data: {
142
151
  address: packet.address,
143
152
  port: packet.port,
@@ -263,7 +263,10 @@ class VoiceWebSocket extends EventEmitter {
263
263
  case VoiceOpcodes.CLIENT_DISCONNECT:
264
264
  this.dave.clients.delete(packet.d.user_id);
265
265
  for (const [ssrc, info] of this.connection.ssrcMap) {
266
- if (info.userId === packet.d.user_id) this.connection.ssrcMap.delete(ssrc);
266
+ if (info.userId === packet.d.user_id) {
267
+ this.connection.ssrcMap.delete(ssrc);
268
+ this.connection.receiver?.packets.forgetSource(ssrc);
269
+ }
267
270
  }
268
271
  const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);
269
272
  if (streamInfo) {