ziplayer 0.3.0 → 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/plugins/index.d.ts +1 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +10 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/Player.d.ts +4 -16
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +86 -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/plugins/index.ts +8 -0
- package/src/structures/Player.ts +84 -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,26 +808,28 @@ 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
|
}
|
|
814
|
+
if (!this.pluginManager.hasStreamCandidate(track)) {
|
|
815
|
+
throw new Error(`UNRECOVERABLE_NO_PLUGIN:${track.title}`);
|
|
816
|
+
}
|
|
1135
817
|
throw new Error(`No stream available for track: ${track.title}`);
|
|
1136
818
|
}
|
|
819
|
+
isUnrecoverableStreamError(error) {
|
|
820
|
+
if (!(error instanceof Error))
|
|
821
|
+
return false;
|
|
822
|
+
return error.message.startsWith("UNRECOVERABLE_NO_PLUGIN:");
|
|
823
|
+
}
|
|
1137
824
|
/**
|
|
1138
825
|
* Start playing a specific track immediately, replacing the current resource.
|
|
1139
826
|
*/
|
|
1140
827
|
async startTrack(track) {
|
|
828
|
+
if (this.destroyed)
|
|
829
|
+
return false;
|
|
1141
830
|
try {
|
|
1142
831
|
// Try to use preloaded resource
|
|
1143
|
-
if (this.
|
|
1144
|
-
this.preloadSlot.track?.id === track.id &&
|
|
1145
|
-
this.preloadSlot.resource &&
|
|
1146
|
-
this.preloadSlot.resource.playStream?.readable !== false) {
|
|
832
|
+
if (this.preloadManager.hasValidPreload(track)) {
|
|
1147
833
|
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
1148
834
|
// Stop current playback
|
|
1149
835
|
this.audioPlayer.stop(true);
|
|
@@ -1192,12 +878,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1192
878
|
* Swap preload slot to current slot
|
|
1193
879
|
*/
|
|
1194
880
|
async swapToCurrent(track) {
|
|
1195
|
-
|
|
1196
|
-
const newResource = this.preloadSlot.resource;
|
|
1197
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
1198
|
-
if (!newResource) {
|
|
881
|
+
if (!this.preloadManager.hasValidPreload(track)) {
|
|
1199
882
|
return false;
|
|
1200
883
|
}
|
|
884
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1201
885
|
// Stop current playback
|
|
1202
886
|
this.audioPlayer.stop(true);
|
|
1203
887
|
// Clean up old current stream (but keep it for a moment)
|
|
@@ -1241,6 +925,8 @@ class Player extends events_1.EventEmitter {
|
|
|
1241
925
|
* Load fresh stream when no preload available
|
|
1242
926
|
*/
|
|
1243
927
|
async loadFreshStream(track) {
|
|
928
|
+
if (this.destroyed)
|
|
929
|
+
return false;
|
|
1244
930
|
// Cancel preload to free resources
|
|
1245
931
|
await this.safeCancelPreload();
|
|
1246
932
|
try {
|
|
@@ -1278,9 +964,11 @@ class Player extends events_1.EventEmitter {
|
|
|
1278
964
|
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
1279
965
|
await this.applyCrossfadeIn(resource, track);
|
|
1280
966
|
// Preload next (async)
|
|
1281
|
-
this.
|
|
1282
|
-
this.
|
|
1283
|
-
|
|
967
|
+
if (!this.destroyed) {
|
|
968
|
+
this.preloadNextTrack().catch((err) => {
|
|
969
|
+
this.debug(`[Player] Preload error:`, err);
|
|
970
|
+
});
|
|
971
|
+
}
|
|
1284
972
|
return true;
|
|
1285
973
|
}
|
|
1286
974
|
catch (error) {
|
|
@@ -1292,6 +980,8 @@ class Player extends events_1.EventEmitter {
|
|
|
1292
980
|
* Play the next track in the queue, handling errors and edge cases gracefully
|
|
1293
981
|
*/
|
|
1294
982
|
async playNext() {
|
|
983
|
+
if (this.destroyed)
|
|
984
|
+
return false;
|
|
1295
985
|
this.debug("[Player] playNext called");
|
|
1296
986
|
// Don't cancel preload here unless absolutely necessary
|
|
1297
987
|
// Let startTrack handle it
|
|
@@ -1303,6 +993,14 @@ class Player extends events_1.EventEmitter {
|
|
|
1303
993
|
const willnext = this.queue.willNextTrack();
|
|
1304
994
|
if (willnext) {
|
|
1305
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));
|
|
1306
1004
|
continue;
|
|
1307
1005
|
}
|
|
1308
1006
|
}
|
|
@@ -1340,6 +1038,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1340
1038
|
catch (err) {
|
|
1341
1039
|
this.debug(`[Player] playNext error:`, err);
|
|
1342
1040
|
this.emit("playerError", err, track);
|
|
1041
|
+
if (this.isUnrecoverableStreamError(err)) {
|
|
1042
|
+
this.debug(`[Player] Skipping unrecoverable track (no plugin): ${track.title}`);
|
|
1043
|
+
continue;
|
|
1044
|
+
}
|
|
1343
1045
|
const recovered = await this.attemptTrackRecovery(track, err);
|
|
1344
1046
|
if (recovered) {
|
|
1345
1047
|
return true;
|
|
@@ -2049,6 +1751,9 @@ class Player extends events_1.EventEmitter {
|
|
|
2049
1751
|
*/
|
|
2050
1752
|
destroy() {
|
|
2051
1753
|
this.debug(`[Player] destroy called`);
|
|
1754
|
+
if (this.destroyed)
|
|
1755
|
+
return;
|
|
1756
|
+
this.destroyed = true;
|
|
2052
1757
|
if (this.leaveTimeout) {
|
|
2053
1758
|
clearTimeout(this.leaveTimeout);
|
|
2054
1759
|
this.leaveTimeout = null;
|
|
@@ -2057,10 +1762,10 @@ class Player extends events_1.EventEmitter {
|
|
|
2057
1762
|
// Destroy current stream before stopping audio
|
|
2058
1763
|
this.destroyCurrentStream();
|
|
2059
1764
|
this.clearSlot(this.currentSlot);
|
|
2060
|
-
this.
|
|
1765
|
+
this.preloadManager.clearPreloadSlot();
|
|
2061
1766
|
this.audioPlayer.removeAllListeners();
|
|
2062
1767
|
this.audioPlayer.stop(true);
|
|
2063
|
-
this.
|
|
1768
|
+
this.preloadManager.cancelPreload();
|
|
2064
1769
|
if (this.ttsPlayer) {
|
|
2065
1770
|
try {
|
|
2066
1771
|
this.ttsPlayer.stop(true);
|
|
@@ -2218,6 +1923,8 @@ class Player extends events_1.EventEmitter {
|
|
|
2218
1923
|
}
|
|
2219
1924
|
setupEventListeners() {
|
|
2220
1925
|
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
1926
|
+
if (this.destroyed)
|
|
1927
|
+
return;
|
|
2221
1928
|
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
2222
1929
|
if (newState.status === voice_1.AudioPlayerStatus.Idle && oldState.status !== voice_1.AudioPlayerStatus.Idle) {
|
|
2223
1930
|
// Track ended
|
|
@@ -2226,7 +1933,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2226
1933
|
this.debug(`[Player] Track ended: ${track.title}`);
|
|
2227
1934
|
this.emit("trackEnd", track);
|
|
2228
1935
|
}
|
|
2229
|
-
this.playNext();
|
|
1936
|
+
void this.playNext();
|
|
2230
1937
|
}
|
|
2231
1938
|
else if (newState.status === voice_1.AudioPlayerStatus.Playing &&
|
|
2232
1939
|
(oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) {
|
|
@@ -2288,18 +1995,20 @@ class Player extends events_1.EventEmitter {
|
|
|
2288
1995
|
}
|
|
2289
1996
|
});
|
|
2290
1997
|
this.audioPlayer.on("error", (error) => {
|
|
1998
|
+
if (this.destroyed)
|
|
1999
|
+
return;
|
|
2291
2000
|
this.debug(`[Player] AudioPlayer error:`, error);
|
|
2292
2001
|
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
2293
2002
|
const track = this.queue.currentTrack;
|
|
2294
2003
|
if (track && this.antiStuckEnabled) {
|
|
2295
2004
|
void this.attemptTrackRecovery(track, error).then((recovered) => {
|
|
2296
2005
|
if (!recovered) {
|
|
2297
|
-
this.playNext();
|
|
2006
|
+
void this.playNext();
|
|
2298
2007
|
}
|
|
2299
2008
|
});
|
|
2300
2009
|
return;
|
|
2301
2010
|
}
|
|
2302
|
-
this.playNext();
|
|
2011
|
+
void this.playNext();
|
|
2303
2012
|
});
|
|
2304
2013
|
this.audioPlayer.on("debug", (...args) => {
|
|
2305
2014
|
if (this.manager.debugEnabled) {
|