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.
- package/README.md +5 -3
- package/package.json +17 -14
- package/src/client/Client.js +3 -3
- package/src/client/WebhookClient.js +1 -1
- package/src/client/actions/VoiceStateUpdate.js +2 -2
- package/src/client/voice/VoiceConnection.js +98 -24
- package/src/client/voice/dispatcher/AudioDispatcher.js +1 -1
- package/src/client/voice/dispatcher/BaseDispatcher.js +49 -61
- package/src/client/voice/dispatcher/VideoDispatcher.js +86 -2
- package/src/client/voice/networking/CongestionControl.js +84 -0
- package/src/client/voice/networking/DAVE.js +26 -1
- package/src/client/voice/networking/MediaRecovery.js +300 -0
- package/src/client/voice/networking/VoiceUDPClient.js +10 -1
- package/src/client/voice/networking/VoiceWebSocket.js +4 -1
- package/src/client/voice/player/MediaPlayer.js +45 -11
- package/src/client/voice/player/processing/AnnexBNalSplitter.js +0 -1
- package/src/client/voice/receiver/AudioJitter.js +101 -0
- package/src/client/voice/receiver/PacketHandler.js +66 -9
- package/src/client/voice/receiver/Receiver.js +9 -4
- package/src/client/voice/receiver/Recorder.js +186 -57
- package/src/client/voice/util/PlayInterface.js +2 -0
- package/src/client/voice/util/Secretbox.js +1 -1
- package/src/client/voice/util/TransportCrypto.js +54 -0
- package/src/client/voice/util/VideoFrames.js +4 -1
- package/src/client/websocket/WebSocketShard.js +0 -1
- package/src/managers/ApplicationCommandManager.js +0 -2
- package/src/managers/ApplicationCommandPermissionsManager.js +0 -1
- package/src/managers/GuildChannelManager.js +2 -3
- package/src/managers/GuildMemberManager.js +0 -1
- package/src/managers/GuildTextThreadManager.js +1 -1
- package/src/managers/MessageManager.js +5 -5
- package/src/managers/RoleManager.js +1 -1
- package/src/rest/APIRequest.js +0 -1
- package/src/rest/APIRouter.js +2 -2
- package/src/rest/DiscordAPIError.js +1 -1
- package/src/rest/RequestHandler.js +3 -3
- package/src/sharding/ShardingManager.js +2 -2
- package/src/structures/ApplicationCommand.js +2 -7
- package/src/structures/BaseCommandInteraction.js +1 -2
- package/src/structures/BaseGuildTextChannel.js +1 -1
- package/src/structures/BaseGuildVoiceChannel.js +1 -1
- package/src/structures/ClientPresence.js +0 -1
- package/src/structures/ClientUser.js +13 -5
- package/src/structures/ContainerComponent.js +1 -1
- package/src/structures/DMChannel.js +1 -1
- package/src/structures/GroupDMChannel.js +4 -4
- package/src/structures/Guild.js +0 -2
- package/src/structures/GuildAuditLogs.js +4 -4
- package/src/structures/GuildScheduledEvent.js +0 -2
- package/src/structures/Interaction.js +1 -1
- package/src/structures/InteractionCollector.js +2 -2
- package/src/structures/InteractionWebhook.js +0 -1
- package/src/structures/Message.js +16 -17
- package/src/structures/MessageComponentInteraction.js +1 -1
- package/src/structures/MessageEmbed.js +0 -2
- package/src/structures/MessagePayload.js +2 -2
- package/src/structures/ModalSubmitInteraction.js +2 -2
- package/src/structures/ThreadChannel.js +3 -3
- package/src/structures/ThreadOnlyChannel.js +1 -1
- package/src/structures/WebEmbed.js +0 -1
- package/src/structures/interfaces/Application.js +2 -2
- package/src/structures/interfaces/Collector.js +1 -4
- package/src/structures/interfaces/TextBasedChannel.js +6 -6
- package/src/util/AttachmentFlags.js +0 -1
- package/src/util/Constants.js +0 -2
- package/src/util/Options.js +3 -3
- package/src/util/Util.js +1 -2
- package/typings/index.d.ts +109 -115
- 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
|
|
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))
|
|
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)
|
|
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) {
|