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
@@ -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 = ['-f', 'ivf', '-deadline', 'realtime', '-c:v', 'libvpx'];
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
- // '-g',
56
- // `${options?.fps}`,
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?.presetH265 || 'faster',
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 (!options?.fps) options.fps = 30;
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() {} // eslint-disable-line no-empty-function
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
- const frame = this.videoFrames.push(packet, codec);
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 payloads = packetize(plain, codec);
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.audioReceiver(ssrc, userStat, packet);
302
- this.audioReceiverForStream(ssrc, userStat, packet);
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
- const wants = { any: 0 };
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
- * <info>Proof of concept - Requires a very good internet connection</info>
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;