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
|
@@ -22,7 +22,7 @@ const { Readable: ReadableStream } = require('stream');
|
|
|
22
22
|
const prism = require('prism-media');
|
|
23
23
|
const { H264NalSplitter, H265NalSplitter } = require('./processing/AnnexBNalSplitter');
|
|
24
24
|
const { IvfTransformer } = require('./processing/IvfSplitter');
|
|
25
|
-
const { H264Dispatcher } = require('../dispatcher/AnnexBDispatcher');
|
|
25
|
+
const { H264Dispatcher, H265Dispatcher } = require('../dispatcher/AnnexBDispatcher');
|
|
26
26
|
const AudioDispatcher = require('../dispatcher/AudioDispatcher');
|
|
27
27
|
const { VP8Dispatcher } = require('../dispatcher/VPxDispatcher');
|
|
28
28
|
|
|
@@ -38,7 +38,20 @@ const FFMPEG_INPUT_PREFIX = [
|
|
|
38
38
|
'4294',
|
|
39
39
|
];
|
|
40
40
|
const FFMPEG_PCM_ARGUMENTS = ['-f', 's16le', '-ar', '48000', '-ac', '2'];
|
|
41
|
-
const FFMPEG_VP8_ARGUMENTS =
|
|
41
|
+
const FFMPEG_VP8_ARGUMENTS = options => [
|
|
42
|
+
'-f',
|
|
43
|
+
'ivf',
|
|
44
|
+
'-deadline',
|
|
45
|
+
'realtime',
|
|
46
|
+
'-c:v',
|
|
47
|
+
'libvpx',
|
|
48
|
+
'-g',
|
|
49
|
+
String(Math.max(1, Math.round(options?.fps || 30))),
|
|
50
|
+
'-lag-in-frames',
|
|
51
|
+
'0',
|
|
52
|
+
'-auto-alt-ref',
|
|
53
|
+
'0',
|
|
54
|
+
];
|
|
42
55
|
const FFMPEG_H264_ARGUMENTS = options => [
|
|
43
56
|
'-c:v',
|
|
44
57
|
'libx264',
|
|
@@ -52,10 +65,8 @@ const FFMPEG_H264_ARGUMENTS = options => [
|
|
|
52
65
|
options?.presetH26x || 'faster',
|
|
53
66
|
'-profile:v',
|
|
54
67
|
'baseline',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// '-x264-params',
|
|
58
|
-
// `keyint=${options?.fps}:min-keyint=${options?.fps}`,
|
|
68
|
+
'-g',
|
|
69
|
+
String(Math.max(1, Math.round(options?.fps || 30))),
|
|
59
70
|
'-bf',
|
|
60
71
|
'0',
|
|
61
72
|
'-bsf:v',
|
|
@@ -68,9 +79,15 @@ const FFMPEG_H265_ARGUMENTS = options => [
|
|
|
68
79
|
'-f',
|
|
69
80
|
'hevc',
|
|
70
81
|
'-preset',
|
|
71
|
-
options?.
|
|
82
|
+
options?.presetH26x || 'faster',
|
|
83
|
+
'-tune',
|
|
84
|
+
'zerolatency',
|
|
72
85
|
'-profile:v',
|
|
73
86
|
'main',
|
|
87
|
+
'-g',
|
|
88
|
+
String(Math.max(1, Math.round(options?.fps || 30))),
|
|
89
|
+
'-x265-params',
|
|
90
|
+
'repeat-headers=1:aud=1',
|
|
74
91
|
'-bf',
|
|
75
92
|
'0',
|
|
76
93
|
];
|
|
@@ -173,7 +190,10 @@ class MediaPlayer extends EventEmitter {
|
|
|
173
190
|
playUnknownVideo(input, options = {}) {
|
|
174
191
|
this.destroyVideoDispatcher();
|
|
175
192
|
const isStream = input instanceof ReadableStream;
|
|
176
|
-
if (
|
|
193
|
+
if (options.fps == null) options.fps = 30;
|
|
194
|
+
if (!Number.isFinite(options.fps) || options.fps <= 0) {
|
|
195
|
+
throw new RangeError('Video FPS must be a positive finite number');
|
|
196
|
+
}
|
|
177
197
|
|
|
178
198
|
const args = [
|
|
179
199
|
'-i',
|
|
@@ -202,7 +222,7 @@ class MediaPlayer extends EventEmitter {
|
|
|
202
222
|
|
|
203
223
|
// Get stream type
|
|
204
224
|
if (this.voiceConnection.videoCodec === 'VP8') {
|
|
205
|
-
args.push(...FFMPEG_VP8_ARGUMENTS);
|
|
225
|
+
args.push(...FFMPEG_VP8_ARGUMENTS(options));
|
|
206
226
|
// Remove '-speed', '5' bc bad quality
|
|
207
227
|
}
|
|
208
228
|
|
|
@@ -245,8 +265,11 @@ class MediaPlayer extends EventEmitter {
|
|
|
245
265
|
case 'H264': {
|
|
246
266
|
return this.playAnnexBVideo(ffmpeg, options, streams, 'H264');
|
|
247
267
|
}
|
|
268
|
+
case 'H265': {
|
|
269
|
+
return this.playAnnexBVideo(ffmpeg, options, streams, 'H265');
|
|
270
|
+
}
|
|
248
271
|
default: {
|
|
249
|
-
throw new Error('Invalid codec (Supported: VP8, H264)');
|
|
272
|
+
throw new Error('Invalid codec (Supported: VP8, H264, H265)');
|
|
250
273
|
}
|
|
251
274
|
}
|
|
252
275
|
}
|
|
@@ -261,7 +284,6 @@ class MediaPlayer extends EventEmitter {
|
|
|
261
284
|
return dispatcher;
|
|
262
285
|
}
|
|
263
286
|
|
|
264
|
-
// eslint-disable-next-line no-unused-vars
|
|
265
287
|
playAnnexBVideo(stream, options, streams, type) {
|
|
266
288
|
this.destroyVideoDispatcher();
|
|
267
289
|
let videoStream;
|
|
@@ -300,6 +322,7 @@ class MediaPlayer extends EventEmitter {
|
|
|
300
322
|
streams,
|
|
301
323
|
options?.fps,
|
|
302
324
|
));
|
|
325
|
+
dispatcher.configureCongestion(options);
|
|
303
326
|
return dispatcher;
|
|
304
327
|
}
|
|
305
328
|
case 'H264': {
|
|
@@ -309,6 +332,17 @@ class MediaPlayer extends EventEmitter {
|
|
|
309
332
|
streams,
|
|
310
333
|
options?.fps,
|
|
311
334
|
));
|
|
335
|
+
dispatcher.configureCongestion(options);
|
|
336
|
+
return dispatcher;
|
|
337
|
+
}
|
|
338
|
+
case 'H265': {
|
|
339
|
+
const dispatcher = (this.videoDispatcher = new H265Dispatcher(
|
|
340
|
+
this,
|
|
341
|
+
options?.highWaterMark,
|
|
342
|
+
streams,
|
|
343
|
+
options?.fps,
|
|
344
|
+
));
|
|
345
|
+
dispatcher.configureCongestion(options);
|
|
312
346
|
return dispatcher;
|
|
313
347
|
}
|
|
314
348
|
default: {
|
|
@@ -147,7 +147,6 @@ class AnnexBNalSplitter extends Transform {
|
|
|
147
147
|
const newData = Buffer.allocUnsafe(data.length);
|
|
148
148
|
let newLength = 0;
|
|
149
149
|
|
|
150
|
-
// eslint-disable-next-line no-constant-condition
|
|
151
150
|
while (true) {
|
|
152
151
|
const epbsPos = data.indexOf(epbPrefix);
|
|
153
152
|
if (epbsPos === -1) {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Buffer } = require('node:buffer');
|
|
4
|
+
const { setTimeout } = require('node:timers');
|
|
5
|
+
const { RtpHeader, RtpPacket } = require('werift-rtp');
|
|
6
|
+
const { SILENCE_FRAME } = require('../util/Silence');
|
|
7
|
+
|
|
8
|
+
// Opus TOC frame durations from RFC 6716, section 3.1.
|
|
9
|
+
function duration(payload) {
|
|
10
|
+
if (!payload.length) return 20;
|
|
11
|
+
const config = payload[0] >> 3;
|
|
12
|
+
const frame =
|
|
13
|
+
config >= 16 ? 2.5 * 2 ** (config & 3) : config >= 12 ? 10 * 2 ** (config & 1) : [10, 20, 40, 60][config & 3];
|
|
14
|
+
const code = payload[0] & 3;
|
|
15
|
+
const frames = code === 0 ? 1 : code === 3 ? (payload[1] || 0) & 63 : 2;
|
|
16
|
+
const milliseconds = frame * frames;
|
|
17
|
+
return milliseconds > 0 && milliseconds <= 120 ? milliseconds : 20;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Forty-millisecond playout window with bounded reordering and loss padding. @private */
|
|
21
|
+
class AudioJitter {
|
|
22
|
+
constructor(deliver) {
|
|
23
|
+
this.deliver = deliver;
|
|
24
|
+
this.packets = new Map();
|
|
25
|
+
this.expected = null;
|
|
26
|
+
this.lastArrival = -Infinity;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
push(packet) {
|
|
30
|
+
const now = performance.now();
|
|
31
|
+
const seq = packet.header.sequenceNumber;
|
|
32
|
+
if (packet.payload.length > 4096) return;
|
|
33
|
+
if (
|
|
34
|
+
this.expected === null ||
|
|
35
|
+
now - this.lastArrival > 500 ||
|
|
36
|
+
(((seq - this.expected) & 65535) > 1024 && ((seq - this.expected) & 65535) < 32768)
|
|
37
|
+
) {
|
|
38
|
+
this.reset();
|
|
39
|
+
this.expected = seq;
|
|
40
|
+
this.next = now + 40;
|
|
41
|
+
}
|
|
42
|
+
const ahead = (seq - this.expected) & 65535;
|
|
43
|
+
if (ahead >= 32768 || this.packets.has(seq)) return;
|
|
44
|
+
if (this.packets.size >= 64) return;
|
|
45
|
+
this.lastArrival = now;
|
|
46
|
+
this.packets.set(seq, packet);
|
|
47
|
+
this._schedule();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_schedule() {
|
|
51
|
+
if (this.timer || !this.packets.size) return;
|
|
52
|
+
this.timer = setTimeout(
|
|
53
|
+
() => {
|
|
54
|
+
this.timer = null;
|
|
55
|
+
this._play();
|
|
56
|
+
},
|
|
57
|
+
Math.max(0, this.next - performance.now()),
|
|
58
|
+
).unref();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_play() {
|
|
62
|
+
let packet = this.packets.get(this.expected);
|
|
63
|
+
if (!packet) {
|
|
64
|
+
const sequence = [...this.packets.keys()].sort(
|
|
65
|
+
(a, b) => ((a - this.expected) & 65535) - ((b - this.expected) & 65535),
|
|
66
|
+
)[0];
|
|
67
|
+
if (sequence === undefined) return;
|
|
68
|
+
const gap = (sequence - this.expected) & 65535;
|
|
69
|
+
if (gap > 10 || !this.last) {
|
|
70
|
+
this.expected = sequence;
|
|
71
|
+
packet = this.packets.get(sequence);
|
|
72
|
+
} else {
|
|
73
|
+
packet = new RtpPacket(
|
|
74
|
+
new RtpHeader({
|
|
75
|
+
...this.last.header,
|
|
76
|
+
sequenceNumber: this.expected,
|
|
77
|
+
timestamp: (this.last.header.timestamp + duration(this.last.payload) * 48) >>> 0,
|
|
78
|
+
}),
|
|
79
|
+
Buffer.from(SILENCE_FRAME),
|
|
80
|
+
);
|
|
81
|
+
packet.concealed = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.packets.delete(this.expected);
|
|
85
|
+
this.expected = (this.expected + 1) & 65535;
|
|
86
|
+
this.last = packet;
|
|
87
|
+
this.next += duration(packet.payload);
|
|
88
|
+
this.deliver(packet);
|
|
89
|
+
this._schedule();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
reset() {
|
|
93
|
+
clearTimeout(this.timer);
|
|
94
|
+
this.timer = null;
|
|
95
|
+
this.packets.clear();
|
|
96
|
+
this.expected = null;
|
|
97
|
+
this.last = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { AudioJitter, duration };
|
|
@@ -5,9 +5,11 @@ const { Buffer } = require('node:buffer');
|
|
|
5
5
|
const crypto = require('node:crypto');
|
|
6
6
|
const { setTimeout } = require('node:timers');
|
|
7
7
|
const { RtpPacket, RtpHeader } = require('werift-rtp');
|
|
8
|
+
const { AudioJitter } = require('./AudioJitter');
|
|
8
9
|
const Recorder = require('./Recorder');
|
|
9
10
|
const Speaking = require('../../../util/Speaking');
|
|
10
11
|
const Util = require('../../../util/Util');
|
|
12
|
+
const MediaRecovery = require('../networking/MediaRecovery');
|
|
11
13
|
const secretbox = require('../util/Secretbox');
|
|
12
14
|
const { SILENCE_FRAME } = require('../util/Silence');
|
|
13
15
|
const { VideoFrames, packetize } = require('../util/VideoFrames');
|
|
@@ -21,7 +23,7 @@ const UNPADDED_NONCE_LENGTH = 4;
|
|
|
21
23
|
const AUTH_TAG_LENGTH = 16;
|
|
22
24
|
|
|
23
25
|
class Readable extends require('stream').Readable {
|
|
24
|
-
_read() {}
|
|
26
|
+
_read() {}
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -38,6 +40,9 @@ class PacketHandler extends EventEmitter {
|
|
|
38
40
|
this.speakingTimeouts = new Map();
|
|
39
41
|
this.videoFrames = new VideoFrames();
|
|
40
42
|
this.videoSequences = new Map();
|
|
43
|
+
this.videoPictures = new Map();
|
|
44
|
+
this.audioJitters = new Map();
|
|
45
|
+
this.recovery = this.connection._mediaRecovery = new MediaRecovery(this.connection);
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
getNonceBuffer() {
|
|
@@ -70,9 +75,10 @@ class PacketHandler extends EventEmitter {
|
|
|
70
75
|
return stream;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
makeVideoStream(user, output) {
|
|
78
|
+
makeVideoStream(user, output, options = {}) {
|
|
74
79
|
if (this.videoStreams.has(user)) return this.videoStreams.get(user);
|
|
75
80
|
const stream = new Recorder(this, {
|
|
81
|
+
...options,
|
|
76
82
|
userId: user,
|
|
77
83
|
output,
|
|
78
84
|
});
|
|
@@ -164,7 +170,7 @@ class PacketHandler extends EventEmitter {
|
|
|
164
170
|
if (!opusPacket.payload) {
|
|
165
171
|
return;
|
|
166
172
|
}
|
|
167
|
-
if (SILENCE_FRAME.equals(opusPacket.payload)) {
|
|
173
|
+
if (SILENCE_FRAME.equals(opusPacket.payload) && !opusPacket.concealed) {
|
|
168
174
|
// If this is a silence frame, pretend we never received it
|
|
169
175
|
return;
|
|
170
176
|
}
|
|
@@ -239,13 +245,17 @@ class PacketHandler extends EventEmitter {
|
|
|
239
245
|
if (this.connection.dave) {
|
|
240
246
|
const codec = ['H264', 'H265', 'VP8'].find(name => Util.getPayloadType(name) === packet.header.payloadType);
|
|
241
247
|
if (!codec) return;
|
|
242
|
-
|
|
248
|
+
let plain;
|
|
249
|
+
const frame = this.videoFrames.push(packet, codec, assembled => {
|
|
250
|
+
plain = this.connection.dave.decrypt(assembled, userStat.userId, true);
|
|
251
|
+
return Boolean(plain);
|
|
252
|
+
});
|
|
243
253
|
if (!frame) return;
|
|
244
|
-
const plain = this.connection.dave.decrypt(frame, userStat.userId, true);
|
|
245
|
-
if (!plain) return;
|
|
246
254
|
this.receiver.emit('videoFrame', userStat, plain, codec);
|
|
247
255
|
if (streamInfo) {
|
|
248
|
-
const
|
|
256
|
+
const picture = this.videoPictures.get(ssrc) ?? 0;
|
|
257
|
+
const payloads = packetize(plain, codec, 1200, picture);
|
|
258
|
+
this.videoPictures.set(ssrc, (picture + 1) & 0x7fff);
|
|
249
259
|
let sequence = this.videoSequences.get(ssrc) ?? packet.header.sequenceNumber;
|
|
250
260
|
for (let i = 0; i < payloads.length; i++) {
|
|
251
261
|
streamInfo.feed(
|
|
@@ -270,6 +280,16 @@ class PacketHandler extends EventEmitter {
|
|
|
270
280
|
}
|
|
271
281
|
|
|
272
282
|
push(buffer) {
|
|
283
|
+
if (
|
|
284
|
+
Buffer.isBuffer(buffer) &&
|
|
285
|
+
buffer.length >= 28 &&
|
|
286
|
+
buffer[0] >> 6 === 2 &&
|
|
287
|
+
buffer[1] >= 192 &&
|
|
288
|
+
buffer[1] <= 223
|
|
289
|
+
) {
|
|
290
|
+
this.recovery.handle(buffer);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
273
293
|
if (!Buffer.isBuffer(buffer) || buffer.length < 32 || buffer[0] >> 6 !== 2) return;
|
|
274
294
|
const ssrc = buffer.readUInt32BE(8);
|
|
275
295
|
let userStat, packet;
|
|
@@ -280,6 +300,8 @@ class PacketHandler extends EventEmitter {
|
|
|
280
300
|
return;
|
|
281
301
|
}
|
|
282
302
|
if (packet instanceof Error) return;
|
|
303
|
+
const type = packet.header.payloadType;
|
|
304
|
+
if (!Util.getAllPayloadType().some(codec => codec.payload_type === type || codec.rtx_payload_type === type)) return;
|
|
283
305
|
if (this.connection.ssrcMap.has(ssrc)) {
|
|
284
306
|
userStat = this.connection.ssrcMap.get(ssrc); // Audio_ssrc
|
|
285
307
|
if (userStat.kind === 'video-rtx') {
|
|
@@ -289,17 +311,35 @@ class PacketHandler extends EventEmitter {
|
|
|
289
311
|
packet.header.sequenceNumber = packet.payload.readUInt16BE();
|
|
290
312
|
packet.header.payloadType = codec.payload_type;
|
|
291
313
|
packet.payload = packet.payload.subarray(2);
|
|
314
|
+
this.recovery.receive(packet, true);
|
|
292
315
|
this.videoReceiver(userStat.primarySsrc, userStat, packet);
|
|
293
316
|
} else if (userStat.kind === 'video') {
|
|
317
|
+
this.recovery.receive(packet);
|
|
294
318
|
this.videoReceiver(ssrc, userStat, packet);
|
|
295
319
|
} else {
|
|
320
|
+
if (type !== Util.getPayloadType('opus')) return;
|
|
296
321
|
if (this.connection.dave) {
|
|
297
322
|
const plain = this.connection.dave.decrypt(packet.payload, userStat.userId);
|
|
298
323
|
if (!plain) return;
|
|
299
324
|
packet.payload = plain;
|
|
300
325
|
}
|
|
301
|
-
this.
|
|
302
|
-
|
|
326
|
+
let jitter = this.audioJitters.get(ssrc);
|
|
327
|
+
if (jitter && jitter.userId !== userStat.userId) {
|
|
328
|
+
jitter.reset();
|
|
329
|
+
this.audioJitters.delete(ssrc);
|
|
330
|
+
jitter = null;
|
|
331
|
+
}
|
|
332
|
+
if (!jitter) {
|
|
333
|
+
if (this.audioJitters.size >= 32) return;
|
|
334
|
+
jitter = new AudioJitter(ordered => {
|
|
335
|
+
this.audioReceiver(ssrc, jitter.userStat, ordered);
|
|
336
|
+
this.audioReceiverForStream(ssrc, jitter.userStat, ordered);
|
|
337
|
+
});
|
|
338
|
+
jitter.userId = userStat.userId;
|
|
339
|
+
this.audioJitters.set(ssrc, jitter);
|
|
340
|
+
}
|
|
341
|
+
jitter.userStat = userStat;
|
|
342
|
+
jitter.push(packet);
|
|
303
343
|
}
|
|
304
344
|
} else if (this.connection.ssrcMap.has(ssrc - 1)) {
|
|
305
345
|
userStat = this.connection.ssrcMap.get(ssrc - 1); // Video_ssrc
|
|
@@ -309,12 +349,29 @@ class PacketHandler extends EventEmitter {
|
|
|
309
349
|
}
|
|
310
350
|
|
|
311
351
|
// When udp connection is closed (STREAM_DELETE), destroy all streams (Memory leak)
|
|
352
|
+
forgetSource(ssrc) {
|
|
353
|
+
this.audioJitters.get(ssrc)?.reset();
|
|
354
|
+
this.audioJitters.delete(ssrc);
|
|
355
|
+
this.recovery.sources.delete(ssrc);
|
|
356
|
+
this.videoSequences.delete(ssrc);
|
|
357
|
+
this.videoPictures.delete(ssrc);
|
|
358
|
+
clearTimeout(this.speakingTimeouts.get(ssrc));
|
|
359
|
+
this.speakingTimeouts.delete(ssrc);
|
|
360
|
+
for (const key of this.videoFrames.frames.keys()) {
|
|
361
|
+
if (key.startsWith(`${ssrc}:`)) this.videoFrames.frames.delete(key);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
312
365
|
destroyAllStream() {
|
|
366
|
+
this.recovery.reset();
|
|
367
|
+
for (const jitter of this.audioJitters.values()) jitter.reset();
|
|
368
|
+
this.audioJitters.clear();
|
|
313
369
|
for (const timeout of this.speakingTimeouts.values()) clearTimeout(timeout);
|
|
314
370
|
this.speakingTimeouts.clear();
|
|
315
371
|
this.videoFrames.frames.clear();
|
|
316
372
|
this.videoFrames.completed.clear();
|
|
317
373
|
this.videoSequences.clear();
|
|
374
|
+
this.videoPictures.clear();
|
|
318
375
|
for (const stream of this.streams.values()) {
|
|
319
376
|
stream.stream.destroy();
|
|
320
377
|
}
|
|
@@ -34,7 +34,9 @@ class VoiceReceiver extends EventEmitter {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
_updateVideoSubscriptions(frames = this.listenerCount('videoFrame') > 0) {
|
|
37
|
-
|
|
37
|
+
// Keep default media delivery enabled: any: 0 also suppresses audio when
|
|
38
|
+
// switching on explicit video subscriptions.
|
|
39
|
+
const wants = { any: 100 };
|
|
38
40
|
for (const [ssrc, info] of this.connection.ssrcMap) {
|
|
39
41
|
if (info.kind === 'video') wants[ssrc] = frames || this.packets.videoStreams.has(info.userId) ? 100 : 0;
|
|
40
42
|
}
|
|
@@ -90,15 +92,18 @@ class VoiceReceiver extends EventEmitter {
|
|
|
90
92
|
/**
|
|
91
93
|
* Creates a new video receiving stream. If a stream already exists for a user, then that stream will be returned
|
|
92
94
|
* rather than generating a new one.
|
|
93
|
-
*
|
|
95
|
+
* Records received H264, H265 or VP8 video and Opus audio using FFmpeg.
|
|
96
|
+
* Wait for `ready` before sending media and await `stop()` to finalize the output.
|
|
94
97
|
* @param {UserResolvable} user The user to start listening to.
|
|
95
98
|
* @param {WritableStream|string} output Output stream or file path to write the video stream to.
|
|
99
|
+
* @param {Object} [options] Recording options
|
|
100
|
+
* @param {VideoCodec} [options.codec='H264'] The codec sent by the recorded participant
|
|
96
101
|
* @returns {Recorder} The video stream for the specified user.
|
|
97
102
|
*/
|
|
98
|
-
createVideoStream(user, output) {
|
|
103
|
+
createVideoStream(user, output, options = {}) {
|
|
99
104
|
user = this.connection.client.users.resolve(user);
|
|
100
105
|
if (!user) throw new Error('VOICE_USER_MISSING');
|
|
101
|
-
const stream = this.packets.makeVideoStream(user.id, output);
|
|
106
|
+
const stream = this.packets.makeVideoStream(user.id, output, options);
|
|
102
107
|
this._updateVideoSubscriptions();
|
|
103
108
|
stream.once('closed', () => this._updateVideoSubscriptions());
|
|
104
109
|
return stream;
|