ziplayer 0.3.1 → 0.3.2
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/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/structures/Player.d.ts +3 -16
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +74 -377
- package/dist/structures/Player.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/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 +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/structures/Player.ts +71 -424
- package/src/structures/PreloadManager.ts +274 -0
- package/src/structures/StreamManager.ts +41 -4
- package/src/types/index.ts +1 -1
|
@@ -9,6 +9,7 @@ const plugins_1 = require("../plugins");
|
|
|
9
9
|
const extensions_1 = require("../extensions");
|
|
10
10
|
const FilterManager_1 = require("./FilterManager");
|
|
11
11
|
const StreamManager_1 = require("./StreamManager");
|
|
12
|
+
const PreloadManager_1 = require("./PreloadManager");
|
|
12
13
|
/**
|
|
13
14
|
* Represents a music player for a specific Discord guild.
|
|
14
15
|
*
|
|
@@ -59,16 +60,6 @@ class Player extends events_1.EventEmitter {
|
|
|
59
60
|
this.stuckTimer = null;
|
|
60
61
|
this.skipLoop = false;
|
|
61
62
|
this.refreshLock = false;
|
|
62
|
-
//preloaded resource
|
|
63
|
-
this.preloadState = {
|
|
64
|
-
resource: null,
|
|
65
|
-
track: null,
|
|
66
|
-
abortController: null,
|
|
67
|
-
timeoutId: null,
|
|
68
|
-
isValid: false,
|
|
69
|
-
isBeingUsed: false,
|
|
70
|
-
};
|
|
71
|
-
this.isPreloading = false;
|
|
72
63
|
this.currentSlot = {
|
|
73
64
|
resource: null,
|
|
74
65
|
track: null,
|
|
@@ -78,16 +69,6 @@ class Player extends events_1.EventEmitter {
|
|
|
78
69
|
isLoading: false,
|
|
79
70
|
loadPromise: null,
|
|
80
71
|
};
|
|
81
|
-
this.preloadSlot = {
|
|
82
|
-
resource: null,
|
|
83
|
-
track: null,
|
|
84
|
-
streamId: null,
|
|
85
|
-
abortController: null,
|
|
86
|
-
isValid: false,
|
|
87
|
-
isLoading: false,
|
|
88
|
-
loadPromise: null,
|
|
89
|
-
};
|
|
90
|
-
this.preloadLock = false;
|
|
91
72
|
this.preloadEnabled = true;
|
|
92
73
|
this.crossfadeEnabled = true;
|
|
93
74
|
this.crossfadeDurationMs = 500;
|
|
@@ -122,6 +103,7 @@ class Player extends events_1.EventEmitter {
|
|
|
122
103
|
this.loudnessMaxBoostDb = 8;
|
|
123
104
|
this.loudnessMaxCutDb = 10;
|
|
124
105
|
this.loudnessLimiterCeiling = 0.95;
|
|
106
|
+
this.destroyed = false;
|
|
125
107
|
this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
126
108
|
this.ttsPlayer = null;
|
|
127
109
|
this.lastDuration = 0;
|
|
@@ -163,7 +145,7 @@ class Player extends events_1.EventEmitter {
|
|
|
163
145
|
const crossfadeOptions = this.options.crossfade || {};
|
|
164
146
|
const crossfadeAutoEnable = crossfadeOptions.autoEnable ?? true;
|
|
165
147
|
const crossfadeAutoDisable = crossfadeOptions.autoDisableInLowPerformance ?? true;
|
|
166
|
-
this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ??
|
|
148
|
+
this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ?? 500);
|
|
167
149
|
if (typeof crossfadeOptions.enabled === "boolean") {
|
|
168
150
|
this.crossfadeEnabled = crossfadeOptions.enabled;
|
|
169
151
|
}
|
|
@@ -205,12 +187,20 @@ class Player extends events_1.EventEmitter {
|
|
|
205
187
|
extractorTimeout: this.options.extractorTimeout,
|
|
206
188
|
});
|
|
207
189
|
this.streamManager = new StreamManager_1.StreamManager({
|
|
208
|
-
maxConcurrentStreams:
|
|
190
|
+
maxConcurrentStreams: 2,
|
|
209
191
|
streamTimeout: 5 * 60 * 1000,
|
|
210
192
|
maxListenersPerStream: 15,
|
|
211
193
|
enableMetrics: true,
|
|
212
194
|
autoDestroy: true,
|
|
213
195
|
});
|
|
196
|
+
this.preloadManager = new PreloadManager_1.PreloadManager({
|
|
197
|
+
streamManager: this.streamManager,
|
|
198
|
+
debug: this.debug.bind(this),
|
|
199
|
+
getNextTrack: () => this.queue.nextTrack,
|
|
200
|
+
getStream: (track) => this.getStream(track),
|
|
201
|
+
isDestroyed: () => this.destroyed,
|
|
202
|
+
isEnabled: () => this.preloadEnabled,
|
|
203
|
+
});
|
|
214
204
|
this.volume = this.options.volume || 100;
|
|
215
205
|
this.userdata = this.options.userdata;
|
|
216
206
|
this.searchCache = new lru_cache_1.LRUCache({
|
|
@@ -495,6 +485,9 @@ class Player extends events_1.EventEmitter {
|
|
|
495
485
|
this.queue.add(tracksToAdd[0]);
|
|
496
486
|
this.emit("queueAdd", tracksToAdd[0]);
|
|
497
487
|
}
|
|
488
|
+
if (this.isPlaying && !this.destroyed) {
|
|
489
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload after queue add error:", err));
|
|
490
|
+
}
|
|
498
491
|
const started = !this.isPlaying ? await this.playNext() : true;
|
|
499
492
|
await this.extensionManager.afterPlayHooks({
|
|
500
493
|
success: started,
|
|
@@ -525,273 +518,20 @@ class Player extends events_1.EventEmitter {
|
|
|
525
518
|
* Main preload method - only one at a time
|
|
526
519
|
*/
|
|
527
520
|
async preloadNextTrack() {
|
|
528
|
-
|
|
529
|
-
this.debug(`[Preload] Disabled by options/runtime profile`);
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
// Prevent concurrent preloads
|
|
533
|
-
if (this.preloadLock) {
|
|
534
|
-
this.debug(`[Preload] Already preloading, skipping`);
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
const nextTrack = this.queue.nextTrack;
|
|
538
|
-
if (!nextTrack) {
|
|
539
|
-
this.debug(`[Preload] No next track to preload`);
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
// Check if already preloaded correctly
|
|
543
|
-
if (this.preloadSlot.isValid && this.preloadSlot.track?.id === nextTrack.id && this.preloadSlot.resource) {
|
|
544
|
-
this.debug(`[Preload] Already have valid preload for: ${nextTrack.title}`);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
// Check if currently loading the same track
|
|
548
|
-
if (this.preloadSlot.isLoading && this.preloadSlot.track?.id === nextTrack.id) {
|
|
549
|
-
this.debug(`[Preload] Currently loading same track, waiting...`);
|
|
550
|
-
if (this.preloadSlot.loadPromise) {
|
|
551
|
-
await this.preloadSlot.loadPromise;
|
|
552
|
-
}
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
// Cancel old preload if different track
|
|
556
|
-
if (this.preloadSlot.isValid && this.preloadSlot.track?.id !== nextTrack.id) {
|
|
557
|
-
this.debug(`[Preload] Cancelling old preload for different track: ${this.preloadSlot.track?.title}`);
|
|
558
|
-
await this.safeCancelPreload();
|
|
559
|
-
}
|
|
560
|
-
this.preloadLock = true;
|
|
561
|
-
// Create new abort controller
|
|
562
|
-
const abortController = new AbortController();
|
|
563
|
-
// Setup preload slot
|
|
564
|
-
this.preloadSlot.track = nextTrack;
|
|
565
|
-
this.preloadSlot.abortController = abortController;
|
|
566
|
-
this.preloadSlot.isLoading = true;
|
|
567
|
-
// Create load promise
|
|
568
|
-
const loadPromise = this.executePreload(nextTrack, abortController);
|
|
569
|
-
this.preloadSlot.loadPromise = loadPromise;
|
|
570
|
-
try {
|
|
571
|
-
await loadPromise;
|
|
572
|
-
}
|
|
573
|
-
catch (err) {
|
|
574
|
-
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
575
|
-
this.debug(`[Preload] Cancelled for ${nextTrack.title}`);
|
|
576
|
-
}
|
|
577
|
-
else {
|
|
578
|
-
this.debug(`[Preload] Failed for ${nextTrack.title}:`, err);
|
|
579
|
-
}
|
|
580
|
-
this.clearSlot(this.preloadSlot);
|
|
581
|
-
}
|
|
582
|
-
finally {
|
|
583
|
-
this.preloadLock = false;
|
|
584
|
-
this.preloadSlot.isLoading = false;
|
|
585
|
-
this.preloadSlot.loadPromise = null;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Execute actual preload
|
|
590
|
-
*/
|
|
591
|
-
async executePreload(track, abortController) {
|
|
592
|
-
this.debug(`[Preload] Starting preload for: ${track.title}`);
|
|
593
|
-
// Check for cancellation
|
|
594
|
-
if (abortController.signal.aborted) {
|
|
595
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
596
|
-
}
|
|
597
|
-
// Check if track still relevant
|
|
598
|
-
if (this.queue.nextTrack?.id !== track.id) {
|
|
599
|
-
this.debug(`[Preload] Track changed, cancelling`);
|
|
600
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
601
|
-
}
|
|
602
|
-
try {
|
|
603
|
-
// Get stream with abort support - NO TIMEOUT
|
|
604
|
-
const streamInfo = await this.getStreamWithCancel(track, abortController.signal);
|
|
605
|
-
// Check cancellation
|
|
606
|
-
if (abortController.signal.aborted) {
|
|
607
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
608
|
-
}
|
|
609
|
-
// Check track relevance again
|
|
610
|
-
if (this.queue.nextTrack?.id !== track.id) {
|
|
611
|
-
this.debug(`[Preload] Track changed after stream fetch`);
|
|
612
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
613
|
-
}
|
|
614
|
-
if (!streamInfo?.stream) {
|
|
615
|
-
throw new Error(`No stream available`);
|
|
616
|
-
}
|
|
617
|
-
// Register with StreamManager as preload
|
|
618
|
-
const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
619
|
-
source: track.source || "preload",
|
|
620
|
-
isPreload: true,
|
|
621
|
-
priority: 5,
|
|
622
|
-
});
|
|
623
|
-
// Create resource
|
|
624
|
-
const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
625
|
-
inlineVolume: true,
|
|
626
|
-
metadata: { ...track, preloaded: true },
|
|
627
|
-
});
|
|
628
|
-
// Verify resource is valid
|
|
629
|
-
if (!resource.playStream || resource.playStream.readable === false) {
|
|
630
|
-
throw new Error("Resource not readable");
|
|
631
|
-
}
|
|
632
|
-
// Update preload slot
|
|
633
|
-
this.preloadSlot.resource = resource;
|
|
634
|
-
this.preloadSlot.streamId = streamId;
|
|
635
|
-
this.preloadSlot.isValid = true;
|
|
636
|
-
this.preloadSlot.track = track;
|
|
637
|
-
this.debug(`[Preload] Successfully preloaded: ${track.title} (Stream ID: ${streamId})`);
|
|
638
|
-
}
|
|
639
|
-
catch (err) {
|
|
640
|
-
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
641
|
-
throw err;
|
|
642
|
-
}
|
|
643
|
-
this.debug(`[Preload] Error during preload:`, err);
|
|
644
|
-
throw err;
|
|
645
|
-
}
|
|
521
|
+
await this.preloadManager.preloadNextTrack();
|
|
646
522
|
}
|
|
647
523
|
/**
|
|
648
524
|
* Safe cancel preload - doesn't throw
|
|
649
525
|
*/
|
|
650
526
|
async safeCancelPreload() {
|
|
651
|
-
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
this.debug(`[Preload] Safely cancelling preload for: ${this.preloadSlot.track?.title || "unknown"}`);
|
|
655
|
-
// Abort the operation
|
|
656
|
-
if (this.preloadSlot.abortController) {
|
|
657
|
-
this.preloadSlot.abortController.abort();
|
|
658
|
-
this.preloadSlot.abortController = null;
|
|
659
|
-
}
|
|
660
|
-
// Clean up stream
|
|
661
|
-
if (this.preloadSlot.streamId && this.streamManager) {
|
|
662
|
-
this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
|
|
663
|
-
}
|
|
664
|
-
// Clean up resource
|
|
665
|
-
if (this.preloadSlot.resource) {
|
|
666
|
-
try {
|
|
667
|
-
const stream = this.preloadSlot.resource.playStream;
|
|
668
|
-
if (stream && typeof stream.destroy === "function" && !stream.destroyed) {
|
|
669
|
-
stream.destroy();
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
catch (err) {
|
|
673
|
-
// Ignore destroy errors
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
// Clear slot
|
|
677
|
-
this.clearSlot(this.preloadSlot);
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* Get stream with proper cancellation
|
|
681
|
-
*/
|
|
682
|
-
async getStreamWithCancel(track, signal) {
|
|
683
|
-
// Create abort promise
|
|
684
|
-
const abortPromise = new Promise((_, reject) => {
|
|
685
|
-
if (signal.aborted) {
|
|
686
|
-
reject(new Error("PRELOAD_CANCELLED"));
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
const handler = () => {
|
|
690
|
-
signal.removeEventListener("abort", handler);
|
|
691
|
-
reject(new Error("PRELOAD_CANCELLED"));
|
|
692
|
-
};
|
|
693
|
-
signal.addEventListener("abort", handler);
|
|
694
|
-
});
|
|
695
|
-
try {
|
|
696
|
-
// Check if stream already exists and is valid
|
|
697
|
-
const existingStream = this.streamManager.getStreamByTrack(track.id || track.title);
|
|
698
|
-
if (existingStream && !existingStream.destroyed && existingStream.readable !== false) {
|
|
699
|
-
this.debug(`[Stream] Using existing stream for preload: ${track.title}`);
|
|
700
|
-
return { stream: existingStream, type: "arbitrary" };
|
|
701
|
-
}
|
|
702
|
-
// Race between stream fetch and abort
|
|
703
|
-
const streamPromise = this.getStream(track);
|
|
704
|
-
const result = await Promise.race([streamPromise, abortPromise]);
|
|
705
|
-
return result;
|
|
706
|
-
}
|
|
707
|
-
catch (err) {
|
|
708
|
-
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
709
|
-
throw err;
|
|
710
|
-
}
|
|
711
|
-
throw err;
|
|
712
|
-
}
|
|
527
|
+
await this.preloadManager.safeCancelPreload();
|
|
713
528
|
}
|
|
529
|
+
// Preload stream fetch/cancel flow has been moved to PreloadManager.
|
|
714
530
|
/**
|
|
715
531
|
* Preload next track with proper error handling and cleanup
|
|
716
532
|
*/
|
|
717
533
|
async preloadNext() {
|
|
718
|
-
|
|
719
|
-
this.debug(`[Preload] Disabled by options/runtime profile`);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
this.cancelPreload();
|
|
723
|
-
const next = this.queue.nextTrack;
|
|
724
|
-
if (!next || this.isPreloading) {
|
|
725
|
-
this.debug(`[Preload] Skipped - ${!next ? "no next track" : "already preloading"}`);
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
this.isPreloading = true;
|
|
729
|
-
// Create new AbortController
|
|
730
|
-
const abortController = new AbortController();
|
|
731
|
-
const timeoutId = setTimeout(() => {
|
|
732
|
-
// this.debug(`[Preload] Timeout for track: ${next.title}`);
|
|
733
|
-
// abortController.abort();
|
|
734
|
-
}, 30000);
|
|
735
|
-
this.preloadState.abortController = abortController;
|
|
736
|
-
this.preloadState.timeoutId = timeoutId;
|
|
737
|
-
try {
|
|
738
|
-
this.debug(`[Preload] Starting preload for: ${next.title}`);
|
|
739
|
-
// Check if already aborted
|
|
740
|
-
if (abortController.signal.aborted) {
|
|
741
|
-
throw new Error("Preload aborted before start");
|
|
742
|
-
}
|
|
743
|
-
// Check if this track is still the next one
|
|
744
|
-
if (this.queue.nextTrack?.id !== next.id) {
|
|
745
|
-
this.debug(`[Preload] Track changed, cancelling preload`);
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
const streamInfo = await this.getStreamWithCancel(next, abortController.signal);
|
|
749
|
-
// Double check
|
|
750
|
-
if (abortController.signal.aborted) {
|
|
751
|
-
throw new Error("Preload aborted after stream fetch");
|
|
752
|
-
}
|
|
753
|
-
if (this.queue.nextTrack?.id !== next.id) {
|
|
754
|
-
this.debug(`[Preload] Track changed after stream fetch`);
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
if (!streamInfo?.stream) {
|
|
758
|
-
throw new Error(`No stream available`);
|
|
759
|
-
}
|
|
760
|
-
// Register with StreamManager
|
|
761
|
-
const streamId = this.streamManager.registerStream(streamInfo.stream, next, {
|
|
762
|
-
source: next.source || "preload",
|
|
763
|
-
isPreload: true,
|
|
764
|
-
priority: 8,
|
|
765
|
-
});
|
|
766
|
-
// Create resource
|
|
767
|
-
const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
768
|
-
inlineVolume: true,
|
|
769
|
-
metadata: { ...next, preloaded: true },
|
|
770
|
-
});
|
|
771
|
-
// Store preload state
|
|
772
|
-
this.preloadState = {
|
|
773
|
-
resource,
|
|
774
|
-
track: next,
|
|
775
|
-
abortController,
|
|
776
|
-
timeoutId,
|
|
777
|
-
isValid: true,
|
|
778
|
-
isBeingUsed: false,
|
|
779
|
-
streamId,
|
|
780
|
-
};
|
|
781
|
-
this.debug(`[Preload] Successfully preloaded: ${next.title} (Stream ID: ${streamId})`);
|
|
782
|
-
}
|
|
783
|
-
catch (err) {
|
|
784
|
-
if (err instanceof Error && err.message.includes("aborted")) {
|
|
785
|
-
this.debug(`[Preload] Cancelled for ${next.title}`);
|
|
786
|
-
}
|
|
787
|
-
else {
|
|
788
|
-
this.debug(`[Preload] Failed for ${next?.title}:`, err);
|
|
789
|
-
}
|
|
790
|
-
this.cancelPreload();
|
|
791
|
-
}
|
|
792
|
-
finally {
|
|
793
|
-
this.isPreloading = false;
|
|
794
|
-
}
|
|
534
|
+
await this.preloadManager.preloadNextTrack();
|
|
795
535
|
}
|
|
796
536
|
async fadeResourceVolume(resource, from, to, durationMs) {
|
|
797
537
|
if (!resource.volume)
|
|
@@ -916,7 +656,7 @@ class Player extends events_1.EventEmitter {
|
|
|
916
656
|
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
917
657
|
}
|
|
918
658
|
try {
|
|
919
|
-
if (this.antiStuckReusePreloadFirst && this.
|
|
659
|
+
if (this.antiStuckReusePreloadFirst && this.preloadManager.hasValidPreload(track)) {
|
|
920
660
|
const startedFromPreload = await this.startTrack(track);
|
|
921
661
|
if (startedFromPreload) {
|
|
922
662
|
this.antiStuckConsecutiveFailures = 0;
|
|
@@ -945,59 +685,11 @@ class Player extends events_1.EventEmitter {
|
|
|
945
685
|
this.debug(`[AntiStuck] Keeping track for controlled retry window: ${track.title}`);
|
|
946
686
|
return false;
|
|
947
687
|
}
|
|
948
|
-
/**
|
|
949
|
-
* Clear preloaded resource with proper cleanup
|
|
950
|
-
*/
|
|
951
|
-
clearPreload() {
|
|
952
|
-
// Abort ongoing preload
|
|
953
|
-
if (this.preloadState.abortController) {
|
|
954
|
-
this.preloadState.abortController.abort();
|
|
955
|
-
this.preloadState.abortController = null;
|
|
956
|
-
}
|
|
957
|
-
// Clean up stream
|
|
958
|
-
const stream = this.preloadState.stream;
|
|
959
|
-
if (stream && typeof stream.destroy === "function") {
|
|
960
|
-
try {
|
|
961
|
-
stream.destroy();
|
|
962
|
-
}
|
|
963
|
-
catch (err) {
|
|
964
|
-
this.debug(`[Preload] Error destroying stream:`, err);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
// Clean up resource
|
|
968
|
-
if (this.preloadState.resource) {
|
|
969
|
-
try {
|
|
970
|
-
const playStream = this.preloadState.resource.playStream;
|
|
971
|
-
if (playStream && typeof playStream.destroy === "function") {
|
|
972
|
-
playStream.destroy();
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
catch (err) {
|
|
976
|
-
this.debug(`[Preload] Error destroying resource:`, err);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
this.preloadState = {
|
|
980
|
-
resource: null,
|
|
981
|
-
track: null,
|
|
982
|
-
abortController: null,
|
|
983
|
-
timeoutId: null,
|
|
984
|
-
isValid: false,
|
|
985
|
-
isBeingUsed: false,
|
|
986
|
-
streamId: undefined,
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
688
|
/**
|
|
990
689
|
* Cancel preload (when skipping or stopping)
|
|
991
690
|
*/
|
|
992
691
|
cancelPreload() {
|
|
993
|
-
|
|
994
|
-
this.debug(`[Preload] Cancelling preload for: ${this.preloadSlot.track?.title}`);
|
|
995
|
-
this.preloadSlot.abortController.abort();
|
|
996
|
-
}
|
|
997
|
-
if (this.preloadSlot.streamId && this.streamManager) {
|
|
998
|
-
this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
|
|
999
|
-
}
|
|
1000
|
-
this.clearSlot(this.preloadSlot);
|
|
692
|
+
this.preloadManager.cancelPreload();
|
|
1001
693
|
}
|
|
1002
694
|
/**
|
|
1003
695
|
* Clear a stream slot
|
|
@@ -1030,25 +722,8 @@ class Player extends events_1.EventEmitter {
|
|
|
1030
722
|
* Promote preload slot to current slot without destroying promoted stream.
|
|
1031
723
|
*/
|
|
1032
724
|
promotePreloadToCurrent(track) {
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
// Move ownership to current slot.
|
|
1036
|
-
this.currentSlot.resource = promotedResource;
|
|
1037
|
-
this.currentSlot.track = track;
|
|
1038
|
-
this.currentSlot.streamId = promotedStreamId;
|
|
1039
|
-
this.currentSlot.abortController = null;
|
|
1040
|
-
this.currentSlot.isValid = !!promotedResource;
|
|
1041
|
-
this.currentSlot.isLoading = false;
|
|
1042
|
-
this.currentSlot.loadPromise = null;
|
|
1043
|
-
this.currentResource = promotedResource;
|
|
1044
|
-
// Reset preload slot only (do not destroy promoted resource/stream).
|
|
1045
|
-
this.preloadSlot.resource = null;
|
|
1046
|
-
this.preloadSlot.track = null;
|
|
1047
|
-
this.preloadSlot.streamId = null;
|
|
1048
|
-
this.preloadSlot.abortController = null;
|
|
1049
|
-
this.preloadSlot.isValid = false;
|
|
1050
|
-
this.preloadSlot.isLoading = false;
|
|
1051
|
-
this.preloadSlot.loadPromise = null;
|
|
725
|
+
const promoted = this.preloadManager.promoteToCurrent(track, this.currentSlot);
|
|
726
|
+
this.currentResource = promoted;
|
|
1052
727
|
}
|
|
1053
728
|
/**
|
|
1054
729
|
* Create AudioResource with filters and seek applied
|
|
@@ -1098,6 +773,9 @@ class Player extends events_1.EventEmitter {
|
|
|
1098
773
|
}
|
|
1099
774
|
}
|
|
1100
775
|
async getStream(track) {
|
|
776
|
+
if (this.destroyed) {
|
|
777
|
+
throw new Error("PLAYER_DESTROYED");
|
|
778
|
+
}
|
|
1101
779
|
const trackId = track.id || track.url || track.title;
|
|
1102
780
|
const existingStream = this.streamManager.getStreamByTrack(trackId);
|
|
1103
781
|
if (existingStream && !existingStream.destroyed) {
|
|
@@ -1105,17 +783,23 @@ class Player extends events_1.EventEmitter {
|
|
|
1105
783
|
return { stream: existingStream, type: "arbitrary" };
|
|
1106
784
|
}
|
|
1107
785
|
let stream = await this.extensionManager.provideStream(track);
|
|
786
|
+
if (this.destroyed) {
|
|
787
|
+
if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
|
|
788
|
+
stream.stream.destroy();
|
|
789
|
+
}
|
|
790
|
+
throw new Error("PLAYER_DESTROYED");
|
|
791
|
+
}
|
|
1108
792
|
if (stream?.stream) {
|
|
1109
|
-
|
|
1110
|
-
const streamId = this.streamManager.registerStream(stream.stream, track, {
|
|
1111
|
-
source: "extension",
|
|
1112
|
-
isPreload: false,
|
|
1113
|
-
priority: 10,
|
|
1114
|
-
});
|
|
1115
|
-
this.debug(`[Stream] Extension stream registered with ID: ${streamId}`);
|
|
793
|
+
this.debug(`[Stream] Extension provided stream for: ${track.title}`);
|
|
1116
794
|
return stream;
|
|
1117
795
|
}
|
|
1118
796
|
stream = await this.pluginManager.getStream(track);
|
|
797
|
+
if (this.destroyed) {
|
|
798
|
+
if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
|
|
799
|
+
stream.stream.destroy();
|
|
800
|
+
}
|
|
801
|
+
throw new Error("PLAYER_DESTROYED");
|
|
802
|
+
}
|
|
1119
803
|
if (stream?.stream) {
|
|
1120
804
|
const existingAgain = this.streamManager.getStreamByTrack(trackId);
|
|
1121
805
|
if (existingAgain && !existingAgain.destroyed) {
|
|
@@ -1124,12 +808,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1124
808
|
return { stream: existingAgain, type: "arbitrary" };
|
|
1125
809
|
}
|
|
1126
810
|
// Register with StreamManager
|
|
1127
|
-
|
|
1128
|
-
source: track.source || "plugin",
|
|
1129
|
-
isPreload: false,
|
|
1130
|
-
priority: 5,
|
|
1131
|
-
});
|
|
1132
|
-
this.debug(`[Stream] Plugin stream registered with ID: ${streamId}`);
|
|
811
|
+
this.debug(`[Stream] Plugin provided stream for: ${track.title}`);
|
|
1133
812
|
return stream;
|
|
1134
813
|
}
|
|
1135
814
|
if (!this.pluginManager.hasStreamCandidate(track)) {
|
|
@@ -1146,12 +825,11 @@ class Player extends events_1.EventEmitter {
|
|
|
1146
825
|
* Start playing a specific track immediately, replacing the current resource.
|
|
1147
826
|
*/
|
|
1148
827
|
async startTrack(track) {
|
|
828
|
+
if (this.destroyed)
|
|
829
|
+
return false;
|
|
1149
830
|
try {
|
|
1150
831
|
// Try to use preloaded resource
|
|
1151
|
-
if (this.
|
|
1152
|
-
this.preloadSlot.track?.id === track.id &&
|
|
1153
|
-
this.preloadSlot.resource &&
|
|
1154
|
-
this.preloadSlot.resource.playStream?.readable !== false) {
|
|
832
|
+
if (this.preloadManager.hasValidPreload(track)) {
|
|
1155
833
|
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
1156
834
|
// Stop current playback
|
|
1157
835
|
this.audioPlayer.stop(true);
|
|
@@ -1200,12 +878,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1200
878
|
* Swap preload slot to current slot
|
|
1201
879
|
*/
|
|
1202
880
|
async swapToCurrent(track) {
|
|
1203
|
-
|
|
1204
|
-
const newResource = this.preloadSlot.resource;
|
|
1205
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
1206
|
-
if (!newResource) {
|
|
881
|
+
if (!this.preloadManager.hasValidPreload(track)) {
|
|
1207
882
|
return false;
|
|
1208
883
|
}
|
|
884
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1209
885
|
// Stop current playback
|
|
1210
886
|
this.audioPlayer.stop(true);
|
|
1211
887
|
// Clean up old current stream (but keep it for a moment)
|
|
@@ -1249,6 +925,8 @@ class Player extends events_1.EventEmitter {
|
|
|
1249
925
|
* Load fresh stream when no preload available
|
|
1250
926
|
*/
|
|
1251
927
|
async loadFreshStream(track) {
|
|
928
|
+
if (this.destroyed)
|
|
929
|
+
return false;
|
|
1252
930
|
// Cancel preload to free resources
|
|
1253
931
|
await this.safeCancelPreload();
|
|
1254
932
|
try {
|
|
@@ -1286,9 +964,11 @@ class Player extends events_1.EventEmitter {
|
|
|
1286
964
|
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
1287
965
|
await this.applyCrossfadeIn(resource, track);
|
|
1288
966
|
// Preload next (async)
|
|
1289
|
-
this.
|
|
1290
|
-
this.
|
|
1291
|
-
|
|
967
|
+
if (!this.destroyed) {
|
|
968
|
+
this.preloadNextTrack().catch((err) => {
|
|
969
|
+
this.debug(`[Player] Preload error:`, err);
|
|
970
|
+
});
|
|
971
|
+
}
|
|
1292
972
|
return true;
|
|
1293
973
|
}
|
|
1294
974
|
catch (error) {
|
|
@@ -1300,6 +980,8 @@ class Player extends events_1.EventEmitter {
|
|
|
1300
980
|
* Play the next track in the queue, handling errors and edge cases gracefully
|
|
1301
981
|
*/
|
|
1302
982
|
async playNext() {
|
|
983
|
+
if (this.destroyed)
|
|
984
|
+
return false;
|
|
1303
985
|
this.debug("[Player] playNext called");
|
|
1304
986
|
// Don't cancel preload here unless absolutely necessary
|
|
1305
987
|
// Let startTrack handle it
|
|
@@ -1311,6 +993,14 @@ class Player extends events_1.EventEmitter {
|
|
|
1311
993
|
const willnext = this.queue.willNextTrack();
|
|
1312
994
|
if (willnext) {
|
|
1313
995
|
this.queue.addMultiple([willnext]);
|
|
996
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload autoplay error:", err));
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
await this.generateWillNext().catch((err) => this.debug("[Player] generateWillNext autoplay fallback error:", err));
|
|
1000
|
+
const generatedNext = this.queue.willNextTrack();
|
|
1001
|
+
if (generatedNext) {
|
|
1002
|
+
this.queue.add(generatedNext);
|
|
1003
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload autoplay generated error:", err));
|
|
1314
1004
|
continue;
|
|
1315
1005
|
}
|
|
1316
1006
|
}
|
|
@@ -2061,6 +1751,9 @@ class Player extends events_1.EventEmitter {
|
|
|
2061
1751
|
*/
|
|
2062
1752
|
destroy() {
|
|
2063
1753
|
this.debug(`[Player] destroy called`);
|
|
1754
|
+
if (this.destroyed)
|
|
1755
|
+
return;
|
|
1756
|
+
this.destroyed = true;
|
|
2064
1757
|
if (this.leaveTimeout) {
|
|
2065
1758
|
clearTimeout(this.leaveTimeout);
|
|
2066
1759
|
this.leaveTimeout = null;
|
|
@@ -2069,10 +1762,10 @@ class Player extends events_1.EventEmitter {
|
|
|
2069
1762
|
// Destroy current stream before stopping audio
|
|
2070
1763
|
this.destroyCurrentStream();
|
|
2071
1764
|
this.clearSlot(this.currentSlot);
|
|
2072
|
-
this.
|
|
1765
|
+
this.preloadManager.clearPreloadSlot();
|
|
2073
1766
|
this.audioPlayer.removeAllListeners();
|
|
2074
1767
|
this.audioPlayer.stop(true);
|
|
2075
|
-
this.
|
|
1768
|
+
this.preloadManager.cancelPreload();
|
|
2076
1769
|
if (this.ttsPlayer) {
|
|
2077
1770
|
try {
|
|
2078
1771
|
this.ttsPlayer.stop(true);
|
|
@@ -2230,6 +1923,8 @@ class Player extends events_1.EventEmitter {
|
|
|
2230
1923
|
}
|
|
2231
1924
|
setupEventListeners() {
|
|
2232
1925
|
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
1926
|
+
if (this.destroyed)
|
|
1927
|
+
return;
|
|
2233
1928
|
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
2234
1929
|
if (newState.status === voice_1.AudioPlayerStatus.Idle && oldState.status !== voice_1.AudioPlayerStatus.Idle) {
|
|
2235
1930
|
// Track ended
|
|
@@ -2238,7 +1933,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2238
1933
|
this.debug(`[Player] Track ended: ${track.title}`);
|
|
2239
1934
|
this.emit("trackEnd", track);
|
|
2240
1935
|
}
|
|
2241
|
-
this.playNext();
|
|
1936
|
+
void this.playNext();
|
|
2242
1937
|
}
|
|
2243
1938
|
else if (newState.status === voice_1.AudioPlayerStatus.Playing &&
|
|
2244
1939
|
(oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) {
|
|
@@ -2300,18 +1995,20 @@ class Player extends events_1.EventEmitter {
|
|
|
2300
1995
|
}
|
|
2301
1996
|
});
|
|
2302
1997
|
this.audioPlayer.on("error", (error) => {
|
|
1998
|
+
if (this.destroyed)
|
|
1999
|
+
return;
|
|
2303
2000
|
this.debug(`[Player] AudioPlayer error:`, error);
|
|
2304
2001
|
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
2305
2002
|
const track = this.queue.currentTrack;
|
|
2306
2003
|
if (track && this.antiStuckEnabled) {
|
|
2307
2004
|
void this.attemptTrackRecovery(track, error).then((recovered) => {
|
|
2308
2005
|
if (!recovered) {
|
|
2309
|
-
this.playNext();
|
|
2006
|
+
void this.playNext();
|
|
2310
2007
|
}
|
|
2311
2008
|
});
|
|
2312
2009
|
return;
|
|
2313
2010
|
}
|
|
2314
|
-
this.playNext();
|
|
2011
|
+
void this.playNext();
|
|
2315
2012
|
});
|
|
2316
2013
|
this.audioPlayer.on("debug", (...args) => {
|
|
2317
2014
|
if (this.manager.debugEnabled) {
|