@ovencord/voice 0.19.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.
@@ -0,0 +1,650 @@
1
+ /* eslint-disable @typescript-eslint/prefer-ts-expect-error, @typescript-eslint/method-signature-style */
2
+ import { Buffer } from 'node:buffer';
3
+ import { EventEmitter } from 'node:events';
4
+ import { addAudioPlayer, deleteAudioPlayer } from '../DataStore';
5
+ import { VoiceConnectionStatus, type VoiceConnection } from '../VoiceConnection';
6
+ import { noop } from '../util/util';
7
+ import { AudioPlayerError } from './AudioPlayerError';
8
+ import type { AudioResource } from './AudioResource';
9
+ import { PlayerSubscription } from './PlayerSubscription';
10
+
11
+ // The Opus "silent" frame
12
+ export const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]);
13
+
14
+ /**
15
+ * Describes the behavior of the player when an audio packet is played but there are no available
16
+ * voice connections to play to.
17
+ */
18
+ export enum NoSubscriberBehavior {
19
+ /**
20
+ * Pauses playing the stream until a voice connection becomes available.
21
+ */
22
+ Pause = 'pause',
23
+
24
+ /**
25
+ * Continues to play through the resource regardless.
26
+ */
27
+ Play = 'play',
28
+
29
+ /**
30
+ * The player stops and enters the Idle state.
31
+ */
32
+ Stop = 'stop',
33
+ }
34
+
35
+ export enum AudioPlayerStatus {
36
+ /**
37
+ * When the player has paused itself. Only possible with the "pause" no subscriber behavior.
38
+ */
39
+ AutoPaused = 'autopaused',
40
+
41
+ /**
42
+ * When the player is waiting for an audio resource to become readable before transitioning to Playing.
43
+ */
44
+ Buffering = 'buffering',
45
+
46
+ /**
47
+ * When there is currently no resource for the player to be playing.
48
+ */
49
+ Idle = 'idle',
50
+
51
+ /**
52
+ * When the player has been manually paused.
53
+ */
54
+ Paused = 'paused',
55
+
56
+ /**
57
+ * When the player is actively playing an audio resource.
58
+ */
59
+ Playing = 'playing',
60
+ }
61
+
62
+ /**
63
+ * Options that can be passed when creating an audio player, used to specify its behavior.
64
+ */
65
+ export interface CreateAudioPlayerOptions {
66
+ behaviors?: {
67
+ maxMissedFrames?: number;
68
+ noSubscriber?: NoSubscriberBehavior;
69
+ };
70
+ debug?: boolean;
71
+ }
72
+
73
+ /**
74
+ * The state that an AudioPlayer is in when it has no resource to play. This is the starting state.
75
+ */
76
+ export interface AudioPlayerIdleState {
77
+ status: AudioPlayerStatus.Idle;
78
+ }
79
+
80
+ /**
81
+ * The state that an AudioPlayer is in when it is waiting for a resource to become readable. Once this
82
+ * happens, the AudioPlayer will enter the Playing state. If the resource ends/errors before this, then
83
+ * it will re-enter the Idle state.
84
+ */
85
+ export interface AudioPlayerBufferingState {
86
+ onFailureCallback: () => void;
87
+ onReadableCallback: () => void;
88
+ onStreamError: (error: Error) => void;
89
+ /**
90
+ * The resource that the AudioPlayer is waiting for
91
+ */
92
+ resource: AudioResource;
93
+ status: AudioPlayerStatus.Buffering;
94
+ }
95
+
96
+ /**
97
+ * The state that an AudioPlayer is in when it is actively playing an AudioResource. When playback ends,
98
+ * it will enter the Idle state.
99
+ */
100
+ export interface AudioPlayerPlayingState {
101
+ /**
102
+ * The number of consecutive times that the audio resource has been unable to provide an Opus frame.
103
+ */
104
+ missedFrames: number;
105
+ onStreamError: (error: Error) => void;
106
+
107
+ /**
108
+ * The playback duration in milliseconds of the current audio resource. This includes filler silence packets
109
+ * that have been played when the resource was buffering.
110
+ */
111
+ playbackDuration: number;
112
+
113
+ /**
114
+ * The resource that is being played.
115
+ */
116
+ resource: AudioResource;
117
+
118
+ status: AudioPlayerStatus.Playing;
119
+ }
120
+
121
+ /**
122
+ * The state that an AudioPlayer is in when it has either been explicitly paused by the user, or done
123
+ * automatically by the AudioPlayer itself if there are no available subscribers.
124
+ */
125
+ export interface AudioPlayerPausedState {
126
+ onStreamError: (error: Error) => void;
127
+ /**
128
+ * The playback duration in milliseconds of the current audio resource. This includes filler silence packets
129
+ * that have been played when the resource was buffering.
130
+ */
131
+ playbackDuration: number;
132
+
133
+ /**
134
+ * The current resource of the audio player.
135
+ */
136
+ resource: AudioResource;
137
+
138
+ /**
139
+ * How many silence packets still need to be played to avoid audio interpolation due to the stream suddenly pausing.
140
+ */
141
+ silencePacketsRemaining: number;
142
+
143
+ status: AudioPlayerStatus.AutoPaused | AudioPlayerStatus.Paused;
144
+ }
145
+
146
+ /**
147
+ * The various states that the player can be in.
148
+ */
149
+ export type AudioPlayerState =
150
+ | AudioPlayerBufferingState
151
+ | AudioPlayerIdleState
152
+ | AudioPlayerPausedState
153
+ | AudioPlayerPlayingState;
154
+
155
+ export interface AudioPlayer extends EventEmitter {
156
+ /**
157
+ * Emitted when there is an error emitted from the audio resource played by the audio player
158
+ *
159
+ * @eventProperty
160
+ */
161
+ on(event: 'error', listener: (error: AudioPlayerError) => void): this;
162
+ /**
163
+ * Emitted debugging information about the audio player
164
+ *
165
+ * @eventProperty
166
+ */
167
+ on(event: 'debug', listener: (message: string) => void): this;
168
+ /**
169
+ * Emitted when the state of the audio player changes
170
+ *
171
+ * @eventProperty
172
+ */
173
+ on(event: 'stateChange', listener: (oldState: AudioPlayerState, newState: AudioPlayerState) => void): this;
174
+ /**
175
+ * Emitted when the audio player is subscribed to a voice connection
176
+ *
177
+ * @eventProperty
178
+ */
179
+ on(event: 'subscribe' | 'unsubscribe', listener: (subscription: PlayerSubscription) => void): this;
180
+ /**
181
+ * Emitted when the status of state changes to a specific status
182
+ *
183
+ * @eventProperty
184
+ */
185
+ on<Event extends AudioPlayerStatus>(
186
+ event: Event,
187
+ listener: (oldState: AudioPlayerState, newState: AudioPlayerState & { status: Event }) => void,
188
+ ): this;
189
+ }
190
+
191
+ /**
192
+ * Stringifies an AudioPlayerState instance.
193
+ *
194
+ * @param state - The state to stringify
195
+ */
196
+ function stringifyState(state: AudioPlayerState) {
197
+ return JSON.stringify({
198
+ ...state,
199
+ resource: Reflect.has(state, 'resource'),
200
+ stepTimeout: Reflect.has(state, 'stepTimeout'),
201
+ });
202
+ }
203
+
204
+ /**
205
+ * Used to play audio resources (i.e. tracks, streams) to voice connections.
206
+ *
207
+ * @remarks
208
+ * Audio players are designed to be re-used - even if a resource has finished playing, the player itself
209
+ * can still be used.
210
+ *
211
+ * The AudioPlayer drives the timing of playback, and therefore is unaffected by voice connections
212
+ * becoming unavailable. Its behavior in these scenarios can be configured.
213
+ */
214
+ export class AudioPlayer extends EventEmitter {
215
+ /**
216
+ * The state that the AudioPlayer is in.
217
+ */
218
+ private _state: AudioPlayerState;
219
+
220
+ /**
221
+ * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio
222
+ * to the streams in this list.
223
+ */
224
+ private readonly subscribers: PlayerSubscription[] = [];
225
+
226
+ /**
227
+ * The behavior that the player should follow when it enters certain situations.
228
+ */
229
+ private readonly behaviors: {
230
+ maxMissedFrames: number;
231
+ noSubscriber: NoSubscriberBehavior;
232
+ };
233
+
234
+ /**
235
+ * The debug logger function, if debugging is enabled.
236
+ */
237
+ private readonly debug: ((message: string) => void) | null;
238
+
239
+ /**
240
+ * Creates a new AudioPlayer.
241
+ */
242
+ public constructor(options: CreateAudioPlayerOptions = {}) {
243
+ super();
244
+ this._state = { status: AudioPlayerStatus.Idle };
245
+ this.behaviors = {
246
+ noSubscriber: NoSubscriberBehavior.Pause,
247
+ maxMissedFrames: 5,
248
+ ...options.behaviors,
249
+ };
250
+ this.debug = options.debug === false ? null : (message: string) => this.emit('debug', message);
251
+ }
252
+
253
+ /**
254
+ * A list of subscribed voice connections that can currently receive audio to play.
255
+ */
256
+ public get playable() {
257
+ return this.subscribers
258
+ .filter(({ connection }) => connection.state.status === VoiceConnectionStatus.Ready)
259
+ .map(({ connection }) => connection);
260
+ }
261
+
262
+ /**
263
+ * Subscribes a VoiceConnection to the audio player's play list. If the VoiceConnection is already subscribed,
264
+ * then the existing subscription is used.
265
+ *
266
+ * @remarks
267
+ * This method should not be directly called. Instead, use VoiceConnection#subscribe.
268
+ * @param connection - The connection to subscribe
269
+ * @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription
270
+ */
271
+ // @ts-ignore
272
+ private subscribe(connection: VoiceConnection) {
273
+ const existingSubscription = this.subscribers.find((subscription) => subscription.connection === connection);
274
+ if (!existingSubscription) {
275
+ const subscription = new PlayerSubscription(connection, this);
276
+ this.subscribers.push(subscription);
277
+ setImmediate(() => this.emit('subscribe', subscription));
278
+ return subscription;
279
+ }
280
+
281
+ return existingSubscription;
282
+ }
283
+
284
+ /**
285
+ * Unsubscribes a subscription - i.e. removes a voice connection from the play list of the audio player.
286
+ *
287
+ * @remarks
288
+ * This method should not be directly called. Instead, use PlayerSubscription#unsubscribe.
289
+ * @param subscription - The subscription to remove
290
+ * @returns Whether or not the subscription existed on the player and was removed
291
+ */
292
+ // @ts-ignore
293
+ private unsubscribe(subscription: PlayerSubscription) {
294
+ const index = this.subscribers.indexOf(subscription);
295
+ const exists = index !== -1;
296
+ if (exists) {
297
+ this.subscribers.splice(index, 1);
298
+ subscription.connection.setSpeaking(false);
299
+ this.emit('unsubscribe', subscription);
300
+ }
301
+
302
+ return exists;
303
+ }
304
+
305
+ /**
306
+ * The state that the player is in.
307
+ *
308
+ * @remarks
309
+ * The setter will perform clean-up operations where necessary.
310
+ */
311
+ public get state() {
312
+ return this._state;
313
+ }
314
+
315
+ public set state(newState: AudioPlayerState) {
316
+ const oldState = this._state;
317
+ const newResource = Reflect.get(newState, 'resource') as AudioResource | undefined;
318
+
319
+ if (oldState.status !== AudioPlayerStatus.Idle && oldState.resource !== newResource) {
320
+ oldState.resource.playStream.on('error', noop);
321
+ oldState.resource.playStream.off('error', oldState.onStreamError);
322
+ oldState.resource.audioPlayer = undefined;
323
+ oldState.resource.playStream.destroy();
324
+ oldState.resource.playStream.read(); // required to ensure buffered data is drained, prevents memory leak
325
+ }
326
+
327
+ // When leaving the Buffering state (or buffering a new resource), then remove the event listeners from it
328
+ if (
329
+ oldState.status === AudioPlayerStatus.Buffering &&
330
+ (newState.status !== AudioPlayerStatus.Buffering || newState.resource !== oldState.resource)
331
+ ) {
332
+ oldState.resource.playStream.off('end', oldState.onFailureCallback);
333
+ oldState.resource.playStream.off('close', oldState.onFailureCallback);
334
+ oldState.resource.playStream.off('finish', oldState.onFailureCallback);
335
+ oldState.resource.playStream.off('readable', oldState.onReadableCallback);
336
+ }
337
+
338
+ // transitioning into an idle should ensure that connections stop speaking
339
+ if (newState.status === AudioPlayerStatus.Idle) {
340
+ this._signalStopSpeaking();
341
+ deleteAudioPlayer(this);
342
+ }
343
+
344
+ // attach to the global audio player timer
345
+ if (newResource) {
346
+ addAudioPlayer(this);
347
+ }
348
+
349
+ // playing -> playing state changes should still transition if a resource changed (seems like it would be useful!)
350
+ const didChangeResources =
351
+ oldState.status !== AudioPlayerStatus.Idle &&
352
+ newState.status === AudioPlayerStatus.Playing &&
353
+ oldState.resource !== newState.resource;
354
+
355
+ this._state = newState;
356
+
357
+ this.emit('stateChange', oldState, this._state);
358
+ if (oldState.status !== newState.status || didChangeResources) {
359
+ this.emit(newState.status, oldState, this._state as any);
360
+ }
361
+
362
+ this.debug?.(`state change:\nfrom ${stringifyState(oldState)}\nto ${stringifyState(newState)}`);
363
+ }
364
+
365
+ /**
366
+ * Plays a new resource on the player. If the player is already playing a resource, the existing resource is destroyed
367
+ * (it cannot be reused, even in another player) and is replaced with the new resource.
368
+ *
369
+ * @remarks
370
+ * The player will transition to the Playing state once playback begins, and will return to the Idle state once
371
+ * playback is ended.
372
+ *
373
+ * If the player was previously playing a resource and this method is called, the player will not transition to the
374
+ * Idle state during the swap over.
375
+ * @param resource - The resource to play
376
+ * @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player
377
+ */
378
+ public play<Metadata>(resource: AudioResource<Metadata>) {
379
+ if (resource.ended) {
380
+ throw new Error('Cannot play a resource that has already ended.');
381
+ }
382
+
383
+ if (resource.audioPlayer) {
384
+ if (resource.audioPlayer === this) {
385
+ return;
386
+ }
387
+
388
+ throw new Error('Resource is already being played by another audio player.');
389
+ }
390
+
391
+ resource.audioPlayer = this;
392
+
393
+ // Attach error listeners to the stream that will propagate the error and then return to the Idle
394
+ // state if the resource is still being used.
395
+ const onStreamError = (error: Error) => {
396
+ if (this.state.status !== AudioPlayerStatus.Idle) {
397
+ this.emit('error', new AudioPlayerError(error, this.state.resource));
398
+ }
399
+
400
+ if (this.state.status !== AudioPlayerStatus.Idle && this.state.resource === resource) {
401
+ this.state = {
402
+ status: AudioPlayerStatus.Idle,
403
+ };
404
+ }
405
+ };
406
+
407
+ resource.playStream.once('error', onStreamError);
408
+
409
+ if (resource.started) {
410
+ this.state = {
411
+ status: AudioPlayerStatus.Playing,
412
+ missedFrames: 0,
413
+ playbackDuration: 0,
414
+ resource,
415
+ onStreamError,
416
+ };
417
+ } else {
418
+ const onReadableCallback = () => {
419
+ if (this.state.status === AudioPlayerStatus.Buffering && this.state.resource === resource) {
420
+ this.state = {
421
+ status: AudioPlayerStatus.Playing,
422
+ missedFrames: 0,
423
+ playbackDuration: 0,
424
+ resource,
425
+ onStreamError,
426
+ };
427
+ }
428
+ };
429
+
430
+ const onFailureCallback = () => {
431
+ if (this.state.status === AudioPlayerStatus.Buffering && this.state.resource === resource) {
432
+ this.state = {
433
+ status: AudioPlayerStatus.Idle,
434
+ };
435
+ }
436
+ };
437
+
438
+ resource.playStream.once('readable', onReadableCallback);
439
+
440
+ resource.playStream.once('end', onFailureCallback);
441
+ resource.playStream.once('close', onFailureCallback);
442
+ resource.playStream.once('finish', onFailureCallback);
443
+
444
+ this.state = {
445
+ status: AudioPlayerStatus.Buffering,
446
+ resource,
447
+ onReadableCallback,
448
+ onFailureCallback,
449
+ onStreamError,
450
+ };
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Pauses playback of the current resource, if any.
456
+ *
457
+ * @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches
458
+ * @returns `true` if the player was successfully paused, otherwise `false`
459
+ */
460
+ public pause(interpolateSilence = true) {
461
+ if (this.state.status !== AudioPlayerStatus.Playing) return false;
462
+ this.state = {
463
+ ...this.state,
464
+ status: AudioPlayerStatus.Paused,
465
+ silencePacketsRemaining: interpolateSilence ? 5 : 0,
466
+ };
467
+ return true;
468
+ }
469
+
470
+ /**
471
+ * Unpauses playback of the current resource, if any.
472
+ *
473
+ * @returns `true` if the player was successfully unpaused, otherwise `false`
474
+ */
475
+ public unpause() {
476
+ if (this.state.status !== AudioPlayerStatus.Paused) return false;
477
+ this.state = {
478
+ ...this.state,
479
+ status: AudioPlayerStatus.Playing,
480
+ missedFrames: 0,
481
+ };
482
+ return true;
483
+ }
484
+
485
+ /**
486
+ * Stops playback of the current resource and destroys the resource. The player will either transition to the Idle state,
487
+ * or remain in its current state until the silence padding frames of the resource have been played.
488
+ *
489
+ * @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames
490
+ * @returns `true` if the player will come to a stop, otherwise `false`
491
+ */
492
+ public stop(force = false) {
493
+ if (this.state.status === AudioPlayerStatus.Idle) return false;
494
+ if (force || this.state.resource.silencePaddingFrames === 0) {
495
+ this.state = {
496
+ status: AudioPlayerStatus.Idle,
497
+ };
498
+ } else if (this.state.resource.silenceRemaining === -1) {
499
+ this.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames;
500
+ }
501
+
502
+ return true;
503
+ }
504
+
505
+ /**
506
+ * Checks whether the underlying resource (if any) is playable (readable)
507
+ *
508
+ * @returns `true` if the resource is playable, otherwise `false`
509
+ */
510
+ public checkPlayable() {
511
+ const state = this._state;
512
+ if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return false;
513
+
514
+ // If the stream has been destroyed or is no longer readable, then transition to the Idle state.
515
+ if (!state.resource.readable) {
516
+ this.state = {
517
+ status: AudioPlayerStatus.Idle,
518
+ };
519
+ return false;
520
+ }
521
+
522
+ return true;
523
+ }
524
+
525
+ /**
526
+ * Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered
527
+ * by the active connections of this audio player.
528
+ */
529
+ // @ts-ignore
530
+ private _stepDispatch() {
531
+ const state = this._state;
532
+
533
+ // Guard against the Idle state
534
+ if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return;
535
+
536
+ // Dispatch any audio packets that were prepared in the previous cycle
537
+ for (const connection of this.playable) {
538
+ connection.dispatchAudio();
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the
544
+ * underlying resource of the stream, and then has all the active connections of the audio player prepare it
545
+ * (encrypt it, append header data) so that it is ready to play at the start of the next cycle.
546
+ */
547
+ // @ts-ignore
548
+ private _stepPrepare() {
549
+ const state = this._state;
550
+
551
+ // Guard against the Idle state
552
+ if (state.status === AudioPlayerStatus.Idle || state.status === AudioPlayerStatus.Buffering) return;
553
+
554
+ // List of connections that can receive the packet
555
+ const playable = this.playable;
556
+
557
+ /* If the player was previously in the AutoPaused state, check to see whether there are newly available
558
+ connections, allowing us to transition out of the AutoPaused state back into the Playing state */
559
+ if (state.status === AudioPlayerStatus.AutoPaused && playable.length > 0) {
560
+ this.state = {
561
+ ...state,
562
+ status: AudioPlayerStatus.Playing,
563
+ missedFrames: 0,
564
+ };
565
+ }
566
+
567
+ /* If the player is (auto)paused, check to see whether silence packets should be played and
568
+ set a timeout to begin the next cycle, ending the current cycle here. */
569
+ if (state.status === AudioPlayerStatus.Paused || state.status === AudioPlayerStatus.AutoPaused) {
570
+ if (state.silencePacketsRemaining > 0) {
571
+ state.silencePacketsRemaining--;
572
+ this._preparePacket(SILENCE_FRAME, playable, state);
573
+ if (state.silencePacketsRemaining === 0) {
574
+ this._signalStopSpeaking();
575
+ }
576
+ }
577
+
578
+ return;
579
+ }
580
+
581
+ // If there are no available connections in this cycle, observe the configured "no subscriber" behavior.
582
+ if (playable.length === 0) {
583
+ if (this.behaviors.noSubscriber === NoSubscriberBehavior.Pause) {
584
+ this.state = {
585
+ ...state,
586
+ status: AudioPlayerStatus.AutoPaused,
587
+ silencePacketsRemaining: 5,
588
+ };
589
+ return;
590
+ } else if (this.behaviors.noSubscriber === NoSubscriberBehavior.Stop) {
591
+ this.stop(true);
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Attempt to read an Opus packet from the resource. If there isn't an available packet,
597
+ * play a silence packet. If there are 5 consecutive cycles with failed reads, then the
598
+ * playback will end.
599
+ */
600
+ const packet: Buffer | null = state.resource.read();
601
+
602
+ if (state.status === AudioPlayerStatus.Playing) {
603
+ if (packet) {
604
+ this._preparePacket(packet, playable, state);
605
+ state.missedFrames = 0;
606
+ } else {
607
+ this._preparePacket(SILENCE_FRAME, playable, state);
608
+ state.missedFrames++;
609
+ if (state.missedFrames >= this.behaviors.maxMissedFrames) {
610
+ this.stop();
611
+ }
612
+ }
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Signals to all the subscribed connections that they should send a packet to Discord indicating
618
+ * they are no longer speaking. Called once playback of a resource ends.
619
+ */
620
+ private _signalStopSpeaking() {
621
+ for (const { connection } of this.subscribers) {
622
+ connection.setSpeaking(false);
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Instructs the given connections to each prepare this packet to be played at the start of the
628
+ * next cycle.
629
+ *
630
+ * @param packet - The Opus packet to be prepared by each receiver
631
+ * @param receivers - The connections that should play this packet
632
+ */
633
+ private _preparePacket(
634
+ packet: Buffer,
635
+ receivers: VoiceConnection[],
636
+ state: AudioPlayerPausedState | AudioPlayerPlayingState,
637
+ ) {
638
+ state.playbackDuration += 20;
639
+ for (const connection of receivers) {
640
+ connection.prepareAudioPacket(packet);
641
+ }
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Creates a new AudioPlayer to be used.
647
+ */
648
+ export function createAudioPlayer(options?: CreateAudioPlayerOptions) {
649
+ return new AudioPlayer(options);
650
+ }
@@ -0,0 +1,19 @@
1
+ import type { AudioResource } from './AudioResource';
2
+
3
+ /**
4
+ * An error emitted by an AudioPlayer. Contains an attached resource to aid with
5
+ * debugging and identifying where the error came from.
6
+ */
7
+ export class AudioPlayerError extends Error {
8
+ /**
9
+ * The resource associated with the audio player at the time the error was thrown.
10
+ */
11
+ public readonly resource: AudioResource;
12
+
13
+ public constructor(error: Error, resource: AudioResource) {
14
+ super(error.message);
15
+ this.resource = resource;
16
+ this.name = error.name;
17
+ this.stack = error.stack!;
18
+ }
19
+ }