ziplayer 0.3.1 → 0.3.3
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/{AI-Guide.md → AGENTS.md} +36 -7
- package/README.md +113 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/Player.d.ts +48 -17
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +154 -377
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +35 -6
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +215 -19
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/PreloadManager.d.ts +32 -0
- package/dist/structures/PreloadManager.d.ts.map +1 -0
- package/dist/structures/PreloadManager.js +230 -0
- package/dist/structures/PreloadManager.js.map +1 -0
- package/dist/structures/Queue.d.ts +19 -0
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +21 -0
- package/dist/structures/Queue.js.map +1 -1
- package/dist/structures/StreamManager.d.ts +1 -0
- package/dist/structures/StreamManager.d.ts.map +1 -1
- package/dist/structures/StreamManager.js +37 -3
- package/dist/structures/StreamManager.js.map +1 -1
- package/dist/types/index.d.ts +41 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/plugins/index.ts +1 -1
- package/src/structures/Player.ts +174 -440
- package/src/structures/PlayerManager.ts +253 -23
- package/src/structures/PreloadManager.ts +274 -0
- package/src/structures/Queue.ts +22 -0
- package/src/structures/StreamManager.ts +41 -4
- package/src/types/index.ts +47 -1
package/src/structures/Player.ts
CHANGED
|
@@ -16,32 +16,33 @@ import {
|
|
|
16
16
|
import { Readable } from "stream";
|
|
17
17
|
import { LRUCache } from "lru-cache";
|
|
18
18
|
import type { BaseExtension } from "../extensions";
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
StreamSlot,
|
|
19
|
+
import {
|
|
20
|
+
normalizeTrackMiddleware,
|
|
21
|
+
type Track,
|
|
22
|
+
type PlayerOptions,
|
|
23
|
+
type PlayerEvents,
|
|
24
|
+
type SourcePlugin,
|
|
25
|
+
type SearchResult,
|
|
26
|
+
type ProgressBarOptions,
|
|
27
|
+
type LoopMode,
|
|
28
|
+
type StreamInfo,
|
|
29
|
+
type SaveOptions,
|
|
30
|
+
type VoiceChannel,
|
|
31
|
+
type PlayerSession,
|
|
32
|
+
type ExtensionPlayRequest,
|
|
33
|
+
type ExtensionPlayResponse,
|
|
34
|
+
type ExtensionAfterPlayPayload,
|
|
35
|
+
type StreamSlot,
|
|
36
|
+
type TrackMiddleware,
|
|
36
37
|
} from "../types";
|
|
37
38
|
import type { PlayerManager } from "./PlayerManager";
|
|
38
39
|
|
|
39
40
|
import { Queue } from "./Queue";
|
|
40
41
|
import { PluginManager } from "../plugins";
|
|
41
42
|
import { ExtensionManager } from "../extensions";
|
|
42
|
-
import { withTimeout } from "../utils/timeout";
|
|
43
43
|
import { FilterManager } from "./FilterManager";
|
|
44
44
|
import { StreamManager } from "./StreamManager";
|
|
45
|
+
import { PreloadManager } from "./PreloadManager";
|
|
45
46
|
|
|
46
47
|
export declare interface Player {
|
|
47
48
|
on<K extends keyof PlayerEvents>(event: K, listener: (...args: PlayerEvents[K]) => void): this;
|
|
@@ -96,6 +97,8 @@ export class Player extends EventEmitter {
|
|
|
96
97
|
public pluginManager: PluginManager;
|
|
97
98
|
public extensionManager: ExtensionManager;
|
|
98
99
|
public streamManager: StreamManager;
|
|
100
|
+
public preloadManager: PreloadManager;
|
|
101
|
+
public forwardMode: Boolean = false;
|
|
99
102
|
|
|
100
103
|
public userdata?: Record<string, any>;
|
|
101
104
|
public _lastActivity: number = Date.now();
|
|
@@ -108,17 +111,7 @@ export class Player extends EventEmitter {
|
|
|
108
111
|
private skipLoop = false;
|
|
109
112
|
private filter!: FilterManager;
|
|
110
113
|
private refreshLock = false;
|
|
111
|
-
//preloaded resource
|
|
112
114
|
|
|
113
|
-
private preloadState: PreloadState = {
|
|
114
|
-
resource: null,
|
|
115
|
-
track: null,
|
|
116
|
-
abortController: null,
|
|
117
|
-
timeoutId: null,
|
|
118
|
-
isValid: false,
|
|
119
|
-
isBeingUsed: false,
|
|
120
|
-
};
|
|
121
|
-
private isPreloading = false;
|
|
122
115
|
private currentSlot: StreamSlot = {
|
|
123
116
|
resource: null,
|
|
124
117
|
track: null,
|
|
@@ -129,16 +122,6 @@ export class Player extends EventEmitter {
|
|
|
129
122
|
loadPromise: null,
|
|
130
123
|
};
|
|
131
124
|
|
|
132
|
-
private preloadSlot: StreamSlot = {
|
|
133
|
-
resource: null,
|
|
134
|
-
track: null,
|
|
135
|
-
streamId: null,
|
|
136
|
-
abortController: null,
|
|
137
|
-
isValid: false,
|
|
138
|
-
isLoading: false,
|
|
139
|
-
loadPromise: null,
|
|
140
|
-
};
|
|
141
|
-
private preloadLock = false;
|
|
142
125
|
private preloadEnabled = true;
|
|
143
126
|
private crossfadeEnabled = true;
|
|
144
127
|
private crossfadeDurationMs = 500;
|
|
@@ -173,6 +156,8 @@ export class Player extends EventEmitter {
|
|
|
173
156
|
private loudnessMaxBoostDb = 8;
|
|
174
157
|
private loudnessMaxCutDb = 10;
|
|
175
158
|
private loudnessLimiterCeiling = 0.95;
|
|
159
|
+
private destroyed = false;
|
|
160
|
+
private readonly trackMiddlewareChain: TrackMiddleware[];
|
|
176
161
|
|
|
177
162
|
// Cache for search results to avoid duplicate calls
|
|
178
163
|
private searchCache: LRUCache<string, SearchResult>;
|
|
@@ -223,7 +208,7 @@ export class Player extends EventEmitter {
|
|
|
223
208
|
const crossfadeOptions = this.options.crossfade || {};
|
|
224
209
|
const crossfadeAutoEnable = crossfadeOptions.autoEnable ?? true;
|
|
225
210
|
const crossfadeAutoDisable = crossfadeOptions.autoDisableInLowPerformance ?? true;
|
|
226
|
-
this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ??
|
|
211
|
+
this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ?? 500);
|
|
227
212
|
|
|
228
213
|
if (typeof crossfadeOptions.enabled === "boolean") {
|
|
229
214
|
this.crossfadeEnabled = crossfadeOptions.enabled;
|
|
@@ -266,18 +251,29 @@ export class Player extends EventEmitter {
|
|
|
266
251
|
this.debug(
|
|
267
252
|
`[Player] Runtime options resolved: lowPerformance=${this.lowPerformanceMode}, preload=${this.preloadEnabled}, crossfade=${this.crossfadeEnabled} (${this.crossfadeDurationMs}ms), smartTransition=${this.smartTransitionEnabled}, antiStuck=${this.antiStuckEnabled}, loudnessNormalization=${this.loudnessNormalizationEnabled}`,
|
|
268
253
|
);
|
|
254
|
+
|
|
255
|
+
this.trackMiddlewareChain = [...this.manager.getTrackMiddlewareChain(), ...normalizeTrackMiddleware(options.trackMiddleware)];
|
|
256
|
+
|
|
269
257
|
this.filter = new FilterManager(this, this.manager);
|
|
270
258
|
this.extensionManager = new ExtensionManager(this, this.manager);
|
|
271
259
|
this.pluginManager = new PluginManager(this, this.manager, {
|
|
272
260
|
extractorTimeout: this.options.extractorTimeout,
|
|
273
261
|
});
|
|
274
262
|
this.streamManager = new StreamManager({
|
|
275
|
-
maxConcurrentStreams:
|
|
263
|
+
maxConcurrentStreams: 2,
|
|
276
264
|
streamTimeout: 5 * 60 * 1000,
|
|
277
265
|
maxListenersPerStream: 15,
|
|
278
266
|
enableMetrics: true,
|
|
279
267
|
autoDestroy: true,
|
|
280
268
|
});
|
|
269
|
+
this.preloadManager = new PreloadManager({
|
|
270
|
+
streamManager: this.streamManager,
|
|
271
|
+
debug: this.debug.bind(this),
|
|
272
|
+
getNextTrack: () => this.queue.nextTrack,
|
|
273
|
+
getStream: (track) => this.getStream(track),
|
|
274
|
+
isDestroyed: () => this.destroyed,
|
|
275
|
+
isEnabled: () => this.preloadEnabled,
|
|
276
|
+
});
|
|
281
277
|
this.volume = this.options.volume || 100;
|
|
282
278
|
this.userdata = this.options.userdata;
|
|
283
279
|
this.searchCache = new LRUCache<string, SearchResult>({
|
|
@@ -601,6 +597,10 @@ export class Player extends EventEmitter {
|
|
|
601
597
|
this.emit("queueAdd", tracksToAdd[0]);
|
|
602
598
|
}
|
|
603
599
|
|
|
600
|
+
if (this.isPlaying && !this.destroyed) {
|
|
601
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload after queue add error:", err));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
604
|
const started = !this.isPlaying ? await this.playNext() : true;
|
|
605
605
|
|
|
606
606
|
await this.extensionManager.afterPlayHooks({
|
|
@@ -632,311 +632,22 @@ export class Player extends EventEmitter {
|
|
|
632
632
|
* Main preload method - only one at a time
|
|
633
633
|
*/
|
|
634
634
|
private async preloadNextTrack(): Promise<void> {
|
|
635
|
-
|
|
636
|
-
this.debug(`[Preload] Disabled by options/runtime profile`);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Prevent concurrent preloads
|
|
641
|
-
if (this.preloadLock) {
|
|
642
|
-
this.debug(`[Preload] Already preloading, skipping`);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const nextTrack = this.queue.nextTrack;
|
|
647
|
-
if (!nextTrack) {
|
|
648
|
-
this.debug(`[Preload] No next track to preload`);
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Check if already preloaded correctly
|
|
653
|
-
if (this.preloadSlot.isValid && this.preloadSlot.track?.id === nextTrack.id && this.preloadSlot.resource) {
|
|
654
|
-
this.debug(`[Preload] Already have valid preload for: ${nextTrack.title}`);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Check if currently loading the same track
|
|
659
|
-
if (this.preloadSlot.isLoading && this.preloadSlot.track?.id === nextTrack.id) {
|
|
660
|
-
this.debug(`[Preload] Currently loading same track, waiting...`);
|
|
661
|
-
if (this.preloadSlot.loadPromise) {
|
|
662
|
-
await this.preloadSlot.loadPromise;
|
|
663
|
-
}
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Cancel old preload if different track
|
|
668
|
-
if (this.preloadSlot.isValid && this.preloadSlot.track?.id !== nextTrack.id) {
|
|
669
|
-
this.debug(`[Preload] Cancelling old preload for different track: ${this.preloadSlot.track?.title}`);
|
|
670
|
-
await this.safeCancelPreload();
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
this.preloadLock = true;
|
|
674
|
-
|
|
675
|
-
// Create new abort controller
|
|
676
|
-
const abortController = new AbortController();
|
|
677
|
-
|
|
678
|
-
// Setup preload slot
|
|
679
|
-
this.preloadSlot.track = nextTrack;
|
|
680
|
-
this.preloadSlot.abortController = abortController;
|
|
681
|
-
this.preloadSlot.isLoading = true;
|
|
682
|
-
|
|
683
|
-
// Create load promise
|
|
684
|
-
const loadPromise = this.executePreload(nextTrack, abortController);
|
|
685
|
-
this.preloadSlot.loadPromise = loadPromise;
|
|
686
|
-
|
|
687
|
-
try {
|
|
688
|
-
await loadPromise;
|
|
689
|
-
} catch (err) {
|
|
690
|
-
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
691
|
-
this.debug(`[Preload] Cancelled for ${nextTrack.title}`);
|
|
692
|
-
} else {
|
|
693
|
-
this.debug(`[Preload] Failed for ${nextTrack.title}:`, err);
|
|
694
|
-
}
|
|
695
|
-
this.clearSlot(this.preloadSlot);
|
|
696
|
-
} finally {
|
|
697
|
-
this.preloadLock = false;
|
|
698
|
-
this.preloadSlot.isLoading = false;
|
|
699
|
-
this.preloadSlot.loadPromise = null;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* Execute actual preload
|
|
705
|
-
*/
|
|
706
|
-
private async executePreload(track: Track, abortController: AbortController): Promise<void> {
|
|
707
|
-
this.debug(`[Preload] Starting preload for: ${track.title}`);
|
|
708
|
-
|
|
709
|
-
// Check for cancellation
|
|
710
|
-
if (abortController.signal.aborted) {
|
|
711
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// Check if track still relevant
|
|
715
|
-
if (this.queue.nextTrack?.id !== track.id) {
|
|
716
|
-
this.debug(`[Preload] Track changed, cancelling`);
|
|
717
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
try {
|
|
721
|
-
// Get stream with abort support - NO TIMEOUT
|
|
722
|
-
const streamInfo = await this.getStreamWithCancel(track, abortController.signal);
|
|
723
|
-
|
|
724
|
-
// Check cancellation
|
|
725
|
-
if (abortController.signal.aborted) {
|
|
726
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Check track relevance again
|
|
730
|
-
if (this.queue.nextTrack?.id !== track.id) {
|
|
731
|
-
this.debug(`[Preload] Track changed after stream fetch`);
|
|
732
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
if (!streamInfo?.stream) {
|
|
736
|
-
throw new Error(`No stream available`);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// Register with StreamManager as preload
|
|
740
|
-
const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
741
|
-
source: track.source || "preload",
|
|
742
|
-
isPreload: true,
|
|
743
|
-
priority: 5,
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
// Create resource
|
|
747
|
-
const resource = createAudioResource(streamInfo.stream, {
|
|
748
|
-
inlineVolume: true,
|
|
749
|
-
metadata: { ...track, preloaded: true },
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
// Verify resource is valid
|
|
753
|
-
if (!resource.playStream || resource.playStream.readable === false) {
|
|
754
|
-
throw new Error("Resource not readable");
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Update preload slot
|
|
758
|
-
this.preloadSlot.resource = resource;
|
|
759
|
-
this.preloadSlot.streamId = streamId;
|
|
760
|
-
this.preloadSlot.isValid = true;
|
|
761
|
-
this.preloadSlot.track = track;
|
|
762
|
-
|
|
763
|
-
this.debug(`[Preload] Successfully preloaded: ${track.title} (Stream ID: ${streamId})`);
|
|
764
|
-
} catch (err) {
|
|
765
|
-
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
766
|
-
throw err;
|
|
767
|
-
}
|
|
768
|
-
this.debug(`[Preload] Error during preload:`, err);
|
|
769
|
-
throw err;
|
|
770
|
-
}
|
|
635
|
+
await this.preloadManager.preloadNextTrack();
|
|
771
636
|
}
|
|
772
637
|
|
|
773
638
|
/**
|
|
774
639
|
* Safe cancel preload - doesn't throw
|
|
775
640
|
*/
|
|
776
641
|
private async safeCancelPreload(): Promise<void> {
|
|
777
|
-
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
this.debug(`[Preload] Safely cancelling preload for: ${this.preloadSlot.track?.title || "unknown"}`);
|
|
782
|
-
|
|
783
|
-
// Abort the operation
|
|
784
|
-
if (this.preloadSlot.abortController) {
|
|
785
|
-
this.preloadSlot.abortController.abort();
|
|
786
|
-
this.preloadSlot.abortController = null;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Clean up stream
|
|
790
|
-
if (this.preloadSlot.streamId && this.streamManager) {
|
|
791
|
-
this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// Clean up resource
|
|
795
|
-
if (this.preloadSlot.resource) {
|
|
796
|
-
try {
|
|
797
|
-
const stream = this.preloadSlot.resource.playStream;
|
|
798
|
-
if (stream && typeof stream.destroy === "function" && !stream.destroyed) {
|
|
799
|
-
stream.destroy();
|
|
800
|
-
}
|
|
801
|
-
} catch (err) {
|
|
802
|
-
// Ignore destroy errors
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Clear slot
|
|
807
|
-
this.clearSlot(this.preloadSlot);
|
|
642
|
+
await this.preloadManager.safeCancelPreload();
|
|
808
643
|
}
|
|
809
644
|
|
|
810
|
-
|
|
811
|
-
* Get stream with proper cancellation
|
|
812
|
-
*/
|
|
813
|
-
private async getStreamWithCancel(track: Track, signal: AbortSignal): Promise<StreamInfo | null> {
|
|
814
|
-
// Create abort promise
|
|
815
|
-
const abortPromise = new Promise<never>((_, reject) => {
|
|
816
|
-
if (signal.aborted) {
|
|
817
|
-
reject(new Error("PRELOAD_CANCELLED"));
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const handler = () => {
|
|
821
|
-
signal.removeEventListener("abort", handler);
|
|
822
|
-
reject(new Error("PRELOAD_CANCELLED"));
|
|
823
|
-
};
|
|
824
|
-
signal.addEventListener("abort", handler);
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
try {
|
|
828
|
-
// Check if stream already exists and is valid
|
|
829
|
-
const existingStream = this.streamManager.getStreamByTrack(track.id || track.title);
|
|
830
|
-
if (existingStream && !existingStream.destroyed && existingStream.readable !== false) {
|
|
831
|
-
this.debug(`[Stream] Using existing stream for preload: ${track.title}`);
|
|
832
|
-
return { stream: existingStream, type: "arbitrary" };
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// Race between stream fetch and abort
|
|
836
|
-
const streamPromise = this.getStream(track);
|
|
837
|
-
const result = await Promise.race([streamPromise, abortPromise]);
|
|
838
|
-
return result as StreamInfo | null;
|
|
839
|
-
} catch (err) {
|
|
840
|
-
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
841
|
-
throw err;
|
|
842
|
-
}
|
|
843
|
-
throw err;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
645
|
+
// Preload stream fetch/cancel flow has been moved to PreloadManager.
|
|
846
646
|
/**
|
|
847
647
|
* Preload next track with proper error handling and cleanup
|
|
848
648
|
*/
|
|
849
649
|
async preloadNext(): Promise<void> {
|
|
850
|
-
|
|
851
|
-
this.debug(`[Preload] Disabled by options/runtime profile`);
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
this.cancelPreload();
|
|
856
|
-
|
|
857
|
-
const next = this.queue.nextTrack;
|
|
858
|
-
if (!next || this.isPreloading) {
|
|
859
|
-
this.debug(`[Preload] Skipped - ${!next ? "no next track" : "already preloading"}`);
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
this.isPreloading = true;
|
|
864
|
-
|
|
865
|
-
// Create new AbortController
|
|
866
|
-
const abortController = new AbortController();
|
|
867
|
-
const timeoutId = setTimeout(() => {
|
|
868
|
-
// this.debug(`[Preload] Timeout for track: ${next.title}`);
|
|
869
|
-
// abortController.abort();
|
|
870
|
-
}, 30000);
|
|
871
|
-
|
|
872
|
-
this.preloadState.abortController = abortController;
|
|
873
|
-
this.preloadState.timeoutId = timeoutId;
|
|
874
|
-
|
|
875
|
-
try {
|
|
876
|
-
this.debug(`[Preload] Starting preload for: ${next.title}`);
|
|
877
|
-
|
|
878
|
-
// Check if already aborted
|
|
879
|
-
if (abortController.signal.aborted) {
|
|
880
|
-
throw new Error("Preload aborted before start");
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// Check if this track is still the next one
|
|
884
|
-
if (this.queue.nextTrack?.id !== next.id) {
|
|
885
|
-
this.debug(`[Preload] Track changed, cancelling preload`);
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const streamInfo = await this.getStreamWithCancel(next, abortController.signal);
|
|
890
|
-
|
|
891
|
-
// Double check
|
|
892
|
-
if (abortController.signal.aborted) {
|
|
893
|
-
throw new Error("Preload aborted after stream fetch");
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
if (this.queue.nextTrack?.id !== next.id) {
|
|
897
|
-
this.debug(`[Preload] Track changed after stream fetch`);
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
if (!streamInfo?.stream) {
|
|
902
|
-
throw new Error(`No stream available`);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// Register with StreamManager
|
|
906
|
-
const streamId = this.streamManager.registerStream(streamInfo.stream, next, {
|
|
907
|
-
source: next.source || "preload",
|
|
908
|
-
isPreload: true,
|
|
909
|
-
priority: 8,
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
// Create resource
|
|
913
|
-
const resource = createAudioResource(streamInfo.stream, {
|
|
914
|
-
inlineVolume: true,
|
|
915
|
-
metadata: { ...next, preloaded: true },
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
// Store preload state
|
|
919
|
-
this.preloadState = {
|
|
920
|
-
resource,
|
|
921
|
-
track: next,
|
|
922
|
-
abortController,
|
|
923
|
-
timeoutId,
|
|
924
|
-
isValid: true,
|
|
925
|
-
isBeingUsed: false,
|
|
926
|
-
streamId,
|
|
927
|
-
};
|
|
928
|
-
|
|
929
|
-
this.debug(`[Preload] Successfully preloaded: ${next.title} (Stream ID: ${streamId})`);
|
|
930
|
-
} catch (err) {
|
|
931
|
-
if (err instanceof Error && err.message.includes("aborted")) {
|
|
932
|
-
this.debug(`[Preload] Cancelled for ${next.title}`);
|
|
933
|
-
} else {
|
|
934
|
-
this.debug(`[Preload] Failed for ${next?.title}:`, err);
|
|
935
|
-
}
|
|
936
|
-
this.cancelPreload();
|
|
937
|
-
} finally {
|
|
938
|
-
this.isPreloading = false;
|
|
939
|
-
}
|
|
650
|
+
await this.preloadManager.preloadNextTrack();
|
|
940
651
|
}
|
|
941
652
|
|
|
942
653
|
private async fadeResourceVolume(resource: AudioResource, from: number, to: number, durationMs: number): Promise<void> {
|
|
@@ -1074,7 +785,7 @@ export class Player extends EventEmitter {
|
|
|
1074
785
|
}
|
|
1075
786
|
|
|
1076
787
|
try {
|
|
1077
|
-
if (this.antiStuckReusePreloadFirst && this.
|
|
788
|
+
if (this.antiStuckReusePreloadFirst && this.preloadManager.hasValidPreload(track)) {
|
|
1078
789
|
const startedFromPreload = await this.startTrack(track);
|
|
1079
790
|
if (startedFromPreload) {
|
|
1080
791
|
this.antiStuckConsecutiveFailures = 0;
|
|
@@ -1106,63 +817,11 @@ export class Player extends EventEmitter {
|
|
|
1106
817
|
return false;
|
|
1107
818
|
}
|
|
1108
819
|
|
|
1109
|
-
/**
|
|
1110
|
-
* Clear preloaded resource with proper cleanup
|
|
1111
|
-
*/
|
|
1112
|
-
private clearPreload(): void {
|
|
1113
|
-
// Abort ongoing preload
|
|
1114
|
-
if (this.preloadState.abortController) {
|
|
1115
|
-
this.preloadState.abortController.abort();
|
|
1116
|
-
this.preloadState.abortController = null;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
// Clean up stream
|
|
1120
|
-
const stream = (this.preloadState as any).stream;
|
|
1121
|
-
if (stream && typeof stream.destroy === "function") {
|
|
1122
|
-
try {
|
|
1123
|
-
stream.destroy();
|
|
1124
|
-
} catch (err) {
|
|
1125
|
-
this.debug(`[Preload] Error destroying stream:`, err);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// Clean up resource
|
|
1130
|
-
if (this.preloadState.resource) {
|
|
1131
|
-
try {
|
|
1132
|
-
const playStream = this.preloadState.resource.playStream;
|
|
1133
|
-
if (playStream && typeof playStream.destroy === "function") {
|
|
1134
|
-
playStream.destroy();
|
|
1135
|
-
}
|
|
1136
|
-
} catch (err) {
|
|
1137
|
-
this.debug(`[Preload] Error destroying resource:`, err);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
this.preloadState = {
|
|
1142
|
-
resource: null,
|
|
1143
|
-
track: null,
|
|
1144
|
-
abortController: null,
|
|
1145
|
-
timeoutId: null,
|
|
1146
|
-
isValid: false,
|
|
1147
|
-
isBeingUsed: false,
|
|
1148
|
-
streamId: undefined,
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
820
|
/**
|
|
1153
821
|
* Cancel preload (when skipping or stopping)
|
|
1154
822
|
*/
|
|
1155
823
|
private cancelPreload(): void {
|
|
1156
|
-
|
|
1157
|
-
this.debug(`[Preload] Cancelling preload for: ${this.preloadSlot.track?.title}`);
|
|
1158
|
-
this.preloadSlot.abortController.abort();
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
if (this.preloadSlot.streamId && this.streamManager) {
|
|
1162
|
-
this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
this.clearSlot(this.preloadSlot);
|
|
824
|
+
this.preloadManager.cancelPreload();
|
|
1166
825
|
}
|
|
1167
826
|
|
|
1168
827
|
/**
|
|
@@ -1198,27 +857,8 @@ export class Player extends EventEmitter {
|
|
|
1198
857
|
* Promote preload slot to current slot without destroying promoted stream.
|
|
1199
858
|
*/
|
|
1200
859
|
private promotePreloadToCurrent(track: Track): void {
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
// Move ownership to current slot.
|
|
1205
|
-
this.currentSlot.resource = promotedResource;
|
|
1206
|
-
this.currentSlot.track = track;
|
|
1207
|
-
this.currentSlot.streamId = promotedStreamId;
|
|
1208
|
-
this.currentSlot.abortController = null;
|
|
1209
|
-
this.currentSlot.isValid = !!promotedResource;
|
|
1210
|
-
this.currentSlot.isLoading = false;
|
|
1211
|
-
this.currentSlot.loadPromise = null;
|
|
1212
|
-
this.currentResource = promotedResource;
|
|
1213
|
-
|
|
1214
|
-
// Reset preload slot only (do not destroy promoted resource/stream).
|
|
1215
|
-
this.preloadSlot.resource = null;
|
|
1216
|
-
this.preloadSlot.track = null;
|
|
1217
|
-
this.preloadSlot.streamId = null;
|
|
1218
|
-
this.preloadSlot.abortController = null;
|
|
1219
|
-
this.preloadSlot.isValid = false;
|
|
1220
|
-
this.preloadSlot.isLoading = false;
|
|
1221
|
-
this.preloadSlot.loadPromise = null;
|
|
860
|
+
const promoted = this.preloadManager.promoteToCurrent(track, this.currentSlot);
|
|
861
|
+
this.currentResource = promoted;
|
|
1222
862
|
}
|
|
1223
863
|
|
|
1224
864
|
/**
|
|
@@ -1273,7 +913,36 @@ export class Player extends EventEmitter {
|
|
|
1273
913
|
}
|
|
1274
914
|
}
|
|
1275
915
|
|
|
916
|
+
private mergeTrackPreserveRef(target: Track, source: Track): void {
|
|
917
|
+
if (source === target) return;
|
|
918
|
+
const mergedMeta = {
|
|
919
|
+
...(target.metadata || {}),
|
|
920
|
+
...(source.metadata || {}),
|
|
921
|
+
};
|
|
922
|
+
Object.assign(target, source);
|
|
923
|
+
target.metadata = mergedMeta;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
private async applyTrackMiddleware(track: Track): Promise<void> {
|
|
927
|
+
if (this.trackMiddlewareChain.length === 0) return;
|
|
928
|
+
const ctx = { player: this, manager: this.manager };
|
|
929
|
+
for (const mw of this.trackMiddlewareChain) {
|
|
930
|
+
try {
|
|
931
|
+
const out = await mw(track, ctx);
|
|
932
|
+
if (out != null && out !== track) {
|
|
933
|
+
this.mergeTrackPreserveRef(track, out);
|
|
934
|
+
}
|
|
935
|
+
} catch (err) {
|
|
936
|
+
this.debug(`[TrackMiddleware] Error:`, err);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
1276
941
|
private async getStream(track: Track): Promise<StreamInfo | null> {
|
|
942
|
+
if (this.destroyed) {
|
|
943
|
+
throw new Error("PLAYER_DESTROYED");
|
|
944
|
+
}
|
|
945
|
+
await this.applyTrackMiddleware(track);
|
|
1277
946
|
const trackId = track.id || track.url || track.title;
|
|
1278
947
|
const existingStream = this.streamManager.getStreamByTrack(trackId);
|
|
1279
948
|
|
|
@@ -1283,18 +952,24 @@ export class Player extends EventEmitter {
|
|
|
1283
952
|
}
|
|
1284
953
|
|
|
1285
954
|
let stream = await this.extensionManager.provideStream(track);
|
|
955
|
+
if (this.destroyed) {
|
|
956
|
+
if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
|
|
957
|
+
stream.stream.destroy();
|
|
958
|
+
}
|
|
959
|
+
throw new Error("PLAYER_DESTROYED");
|
|
960
|
+
}
|
|
1286
961
|
if (stream?.stream) {
|
|
1287
|
-
|
|
1288
|
-
const streamId = this.streamManager.registerStream(stream.stream, track, {
|
|
1289
|
-
source: "extension",
|
|
1290
|
-
isPreload: false,
|
|
1291
|
-
priority: 10,
|
|
1292
|
-
});
|
|
1293
|
-
this.debug(`[Stream] Extension stream registered with ID: ${streamId}`);
|
|
962
|
+
this.debug(`[Stream] Extension provided stream for: ${track.title}`);
|
|
1294
963
|
return stream;
|
|
1295
964
|
}
|
|
1296
965
|
|
|
1297
966
|
stream = await this.pluginManager.getStream(track);
|
|
967
|
+
if (this.destroyed) {
|
|
968
|
+
if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
|
|
969
|
+
stream.stream.destroy();
|
|
970
|
+
}
|
|
971
|
+
throw new Error("PLAYER_DESTROYED");
|
|
972
|
+
}
|
|
1298
973
|
if (stream?.stream) {
|
|
1299
974
|
const existingAgain = this.streamManager.getStreamByTrack(trackId);
|
|
1300
975
|
if (existingAgain && !existingAgain.destroyed) {
|
|
@@ -1302,12 +977,7 @@ export class Player extends EventEmitter {
|
|
|
1302
977
|
return { stream: existingAgain, type: "arbitrary" };
|
|
1303
978
|
}
|
|
1304
979
|
// Register with StreamManager
|
|
1305
|
-
|
|
1306
|
-
source: track.source || "plugin",
|
|
1307
|
-
isPreload: false,
|
|
1308
|
-
priority: 5,
|
|
1309
|
-
});
|
|
1310
|
-
this.debug(`[Stream] Plugin stream registered with ID: ${streamId}`);
|
|
980
|
+
this.debug(`[Stream] Plugin provided stream for: ${track.title}`);
|
|
1311
981
|
return stream;
|
|
1312
982
|
}
|
|
1313
983
|
|
|
@@ -1327,14 +997,10 @@ export class Player extends EventEmitter {
|
|
|
1327
997
|
* Start playing a specific track immediately, replacing the current resource.
|
|
1328
998
|
*/
|
|
1329
999
|
private async startTrack(track: Track): Promise<boolean> {
|
|
1000
|
+
if (this.destroyed) return false;
|
|
1330
1001
|
try {
|
|
1331
1002
|
// Try to use preloaded resource
|
|
1332
|
-
if (
|
|
1333
|
-
this.preloadSlot.isValid &&
|
|
1334
|
-
this.preloadSlot.track?.id === track.id &&
|
|
1335
|
-
this.preloadSlot.resource &&
|
|
1336
|
-
this.preloadSlot.resource.playStream?.readable !== false
|
|
1337
|
-
) {
|
|
1003
|
+
if (this.preloadManager.hasValidPreload(track)) {
|
|
1338
1004
|
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
1339
1005
|
|
|
1340
1006
|
// Stop current playback
|
|
@@ -1391,13 +1057,10 @@ export class Player extends EventEmitter {
|
|
|
1391
1057
|
* Swap preload slot to current slot
|
|
1392
1058
|
*/
|
|
1393
1059
|
private async swapToCurrent(track: Track): Promise<boolean> {
|
|
1394
|
-
|
|
1395
|
-
const newResource = this.preloadSlot.resource;
|
|
1396
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
1397
|
-
|
|
1398
|
-
if (!newResource) {
|
|
1060
|
+
if (!this.preloadManager.hasValidPreload(track)) {
|
|
1399
1061
|
return false;
|
|
1400
1062
|
}
|
|
1063
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1401
1064
|
|
|
1402
1065
|
// Stop current playback
|
|
1403
1066
|
this.audioPlayer.stop(true);
|
|
@@ -1449,6 +1112,7 @@ export class Player extends EventEmitter {
|
|
|
1449
1112
|
* Load fresh stream when no preload available
|
|
1450
1113
|
*/
|
|
1451
1114
|
private async loadFreshStream(track: Track): Promise<boolean> {
|
|
1115
|
+
if (this.destroyed) return false;
|
|
1452
1116
|
// Cancel preload to free resources
|
|
1453
1117
|
await this.safeCancelPreload();
|
|
1454
1118
|
|
|
@@ -1495,9 +1159,11 @@ export class Player extends EventEmitter {
|
|
|
1495
1159
|
await this.applyCrossfadeIn(resource, track);
|
|
1496
1160
|
|
|
1497
1161
|
// Preload next (async)
|
|
1498
|
-
this.
|
|
1499
|
-
this.
|
|
1500
|
-
|
|
1162
|
+
if (!this.destroyed) {
|
|
1163
|
+
this.preloadNextTrack().catch((err) => {
|
|
1164
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1501
1167
|
|
|
1502
1168
|
return true;
|
|
1503
1169
|
} catch (error) {
|
|
@@ -1510,6 +1176,7 @@ export class Player extends EventEmitter {
|
|
|
1510
1176
|
* Play the next track in the queue, handling errors and edge cases gracefully
|
|
1511
1177
|
*/
|
|
1512
1178
|
private async playNext(): Promise<boolean> {
|
|
1179
|
+
if (this.destroyed) return false;
|
|
1513
1180
|
this.debug("[Player] playNext called");
|
|
1514
1181
|
|
|
1515
1182
|
// Don't cancel preload here unless absolutely necessary
|
|
@@ -1524,6 +1191,15 @@ export class Player extends EventEmitter {
|
|
|
1524
1191
|
const willnext = this.queue.willNextTrack();
|
|
1525
1192
|
if (willnext) {
|
|
1526
1193
|
this.queue.addMultiple([willnext]);
|
|
1194
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload autoplay error:", err));
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
await this.generateWillNext().catch((err) => this.debug("[Player] generateWillNext autoplay fallback error:", err));
|
|
1199
|
+
const generatedNext = this.queue.willNextTrack();
|
|
1200
|
+
if (generatedNext) {
|
|
1201
|
+
this.queue.add(generatedNext);
|
|
1202
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload autoplay generated error:", err));
|
|
1527
1203
|
continue;
|
|
1528
1204
|
}
|
|
1529
1205
|
}
|
|
@@ -1620,6 +1296,8 @@ export class Player extends EventEmitter {
|
|
|
1620
1296
|
if (!this.connection) throw new Error("No voice connection for TTS");
|
|
1621
1297
|
const ttsPlayer = this.ensureTTSPlayer();
|
|
1622
1298
|
|
|
1299
|
+
await this.applyTrackMiddleware(track);
|
|
1300
|
+
|
|
1623
1301
|
// Build resource from plugin stream
|
|
1624
1302
|
const streamInfo = await this.pluginManager.getStream(track);
|
|
1625
1303
|
if (!streamInfo) {
|
|
@@ -1744,6 +1422,56 @@ export class Player extends EventEmitter {
|
|
|
1744
1422
|
}
|
|
1745
1423
|
}
|
|
1746
1424
|
|
|
1425
|
+
/**
|
|
1426
|
+
* Subscribe this player's voice connection
|
|
1427
|
+
* to another player's audio stream.
|
|
1428
|
+
*
|
|
1429
|
+
* This is primarily used for:
|
|
1430
|
+
* - playback mirroring
|
|
1431
|
+
* - radio/broadcast systems
|
|
1432
|
+
* - multi-guild synchronized playback
|
|
1433
|
+
* - forwardMode shared streaming
|
|
1434
|
+
*
|
|
1435
|
+
* Instead of creating a separate audio stream,
|
|
1436
|
+
* this player will directly receive audio packets
|
|
1437
|
+
* from the target player's AudioPlayer instance.
|
|
1438
|
+
*
|
|
1439
|
+
* Benefits:
|
|
1440
|
+
* - drastically lower CPU usage
|
|
1441
|
+
* - only one ffmpeg/extractor stream
|
|
1442
|
+
* - lower bandwidth and memory usage
|
|
1443
|
+
* - perfect sync across guilds
|
|
1444
|
+
*
|
|
1445
|
+
* Important:
|
|
1446
|
+
* - both players must already have active voice connections
|
|
1447
|
+
* - this does NOT transfer queue ownership
|
|
1448
|
+
* - this does NOT clone playback state automatically
|
|
1449
|
+
*
|
|
1450
|
+
* @param {Player} player - Source player to subscribe to
|
|
1451
|
+
*
|
|
1452
|
+
* @returns {boolean}
|
|
1453
|
+
* Returns true if subscription succeeded,
|
|
1454
|
+
* otherwise false.
|
|
1455
|
+
*
|
|
1456
|
+
* @example
|
|
1457
|
+
* follower.subscribeTo(leader);
|
|
1458
|
+
*
|
|
1459
|
+
* @example
|
|
1460
|
+
* if (!player.subscribeTo(leader)) {
|
|
1461
|
+
* console.log("Failed to subscribe");
|
|
1462
|
+
* }
|
|
1463
|
+
*/
|
|
1464
|
+
public subscribeTo(player: Player): boolean {
|
|
1465
|
+
if (!this.connection) return false;
|
|
1466
|
+
|
|
1467
|
+
this.connection.subscribe(player.audioPlayer);
|
|
1468
|
+
|
|
1469
|
+
this.isPlaying = player.isPlaying;
|
|
1470
|
+
this.isPaused = player.isPaused;
|
|
1471
|
+
this.forwardMode = true;
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1747
1475
|
/**
|
|
1748
1476
|
* Pause the current track
|
|
1749
1477
|
*
|
|
@@ -1947,6 +1675,8 @@ export class Player extends EventEmitter {
|
|
|
1947
1675
|
}
|
|
1948
1676
|
|
|
1949
1677
|
try {
|
|
1678
|
+
await this.applyTrackMiddleware(track);
|
|
1679
|
+
|
|
1950
1680
|
// Skip extension manager for saving - we want the raw stream without filters/seek applied, and extensions may not support this
|
|
1951
1681
|
let streamInfo: StreamInfo | null = await this.pluginManager.getStream(track);
|
|
1952
1682
|
|
|
@@ -2340,6 +2070,8 @@ export class Player extends EventEmitter {
|
|
|
2340
2070
|
*/
|
|
2341
2071
|
destroy(): void {
|
|
2342
2072
|
this.debug(`[Player] destroy called`);
|
|
2073
|
+
if (this.destroyed) return;
|
|
2074
|
+
this.destroyed = true;
|
|
2343
2075
|
|
|
2344
2076
|
if (this.leaveTimeout) {
|
|
2345
2077
|
clearTimeout(this.leaveTimeout);
|
|
@@ -2350,11 +2082,11 @@ export class Player extends EventEmitter {
|
|
|
2350
2082
|
this.destroyCurrentStream();
|
|
2351
2083
|
|
|
2352
2084
|
this.clearSlot(this.currentSlot);
|
|
2353
|
-
this.
|
|
2085
|
+
this.preloadManager.clearPreloadSlot();
|
|
2354
2086
|
|
|
2355
2087
|
this.audioPlayer.removeAllListeners();
|
|
2356
2088
|
this.audioPlayer.stop(true);
|
|
2357
|
-
this.
|
|
2089
|
+
this.preloadManager.cancelPreload();
|
|
2358
2090
|
|
|
2359
2091
|
if (this.ttsPlayer) {
|
|
2360
2092
|
try {
|
|
@@ -2530,6 +2262,7 @@ export class Player extends EventEmitter {
|
|
|
2530
2262
|
|
|
2531
2263
|
private setupEventListeners(): void {
|
|
2532
2264
|
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
2265
|
+
if (this.destroyed) return;
|
|
2533
2266
|
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
2534
2267
|
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
|
|
2535
2268
|
// Track ended
|
|
@@ -2538,7 +2271,7 @@ export class Player extends EventEmitter {
|
|
|
2538
2271
|
this.debug(`[Player] Track ended: ${track.title}`);
|
|
2539
2272
|
this.emit("trackEnd", track);
|
|
2540
2273
|
}
|
|
2541
|
-
this.playNext();
|
|
2274
|
+
void this.playNext();
|
|
2542
2275
|
} else if (
|
|
2543
2276
|
newState.status === AudioPlayerStatus.Playing &&
|
|
2544
2277
|
(oldState.status === AudioPlayerStatus.Idle || oldState.status === AudioPlayerStatus.Buffering)
|
|
@@ -2596,18 +2329,19 @@ export class Player extends EventEmitter {
|
|
|
2596
2329
|
}
|
|
2597
2330
|
});
|
|
2598
2331
|
this.audioPlayer.on("error", (error) => {
|
|
2332
|
+
if (this.destroyed) return;
|
|
2599
2333
|
this.debug(`[Player] AudioPlayer error:`, error);
|
|
2600
2334
|
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
2601
2335
|
const track = this.queue.currentTrack;
|
|
2602
2336
|
if (track && this.antiStuckEnabled) {
|
|
2603
2337
|
void this.attemptTrackRecovery(track, error).then((recovered) => {
|
|
2604
2338
|
if (!recovered) {
|
|
2605
|
-
this.playNext();
|
|
2339
|
+
void this.playNext();
|
|
2606
2340
|
}
|
|
2607
2341
|
});
|
|
2608
2342
|
return;
|
|
2609
2343
|
}
|
|
2610
|
-
this.playNext();
|
|
2344
|
+
void this.playNext();
|
|
2611
2345
|
});
|
|
2612
2346
|
|
|
2613
2347
|
this.audioPlayer.on("debug", (...args) => {
|