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
|
@@ -4,11 +4,13 @@ exports.Player = void 0;
|
|
|
4
4
|
const events_1 = require("events");
|
|
5
5
|
const voice_1 = require("@discordjs/voice");
|
|
6
6
|
const lru_cache_1 = require("lru-cache");
|
|
7
|
+
const types_1 = require("../types");
|
|
7
8
|
const Queue_1 = require("./Queue");
|
|
8
9
|
const plugins_1 = require("../plugins");
|
|
9
10
|
const extensions_1 = require("../extensions");
|
|
10
11
|
const FilterManager_1 = require("./FilterManager");
|
|
11
12
|
const StreamManager_1 = require("./StreamManager");
|
|
13
|
+
const PreloadManager_1 = require("./PreloadManager");
|
|
12
14
|
/**
|
|
13
15
|
* Represents a music player for a specific Discord guild.
|
|
14
16
|
*
|
|
@@ -52,6 +54,7 @@ class Player extends events_1.EventEmitter {
|
|
|
52
54
|
this.volume = 100;
|
|
53
55
|
this.isPlaying = false;
|
|
54
56
|
this.isPaused = false;
|
|
57
|
+
this.forwardMode = false;
|
|
55
58
|
this._lastActivity = Date.now();
|
|
56
59
|
this.leaveTimeout = null;
|
|
57
60
|
this.currentResource = null;
|
|
@@ -59,16 +62,6 @@ class Player extends events_1.EventEmitter {
|
|
|
59
62
|
this.stuckTimer = null;
|
|
60
63
|
this.skipLoop = false;
|
|
61
64
|
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
65
|
this.currentSlot = {
|
|
73
66
|
resource: null,
|
|
74
67
|
track: null,
|
|
@@ -78,16 +71,6 @@ class Player extends events_1.EventEmitter {
|
|
|
78
71
|
isLoading: false,
|
|
79
72
|
loadPromise: null,
|
|
80
73
|
};
|
|
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
74
|
this.preloadEnabled = true;
|
|
92
75
|
this.crossfadeEnabled = true;
|
|
93
76
|
this.crossfadeDurationMs = 500;
|
|
@@ -122,6 +105,7 @@ class Player extends events_1.EventEmitter {
|
|
|
122
105
|
this.loudnessMaxBoostDb = 8;
|
|
123
106
|
this.loudnessMaxCutDb = 10;
|
|
124
107
|
this.loudnessLimiterCeiling = 0.95;
|
|
108
|
+
this.destroyed = false;
|
|
125
109
|
this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
126
110
|
this.ttsPlayer = null;
|
|
127
111
|
this.lastDuration = 0;
|
|
@@ -163,7 +147,7 @@ class Player extends events_1.EventEmitter {
|
|
|
163
147
|
const crossfadeOptions = this.options.crossfade || {};
|
|
164
148
|
const crossfadeAutoEnable = crossfadeOptions.autoEnable ?? true;
|
|
165
149
|
const crossfadeAutoDisable = crossfadeOptions.autoDisableInLowPerformance ?? true;
|
|
166
|
-
this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ??
|
|
150
|
+
this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ?? 500);
|
|
167
151
|
if (typeof crossfadeOptions.enabled === "boolean") {
|
|
168
152
|
this.crossfadeEnabled = crossfadeOptions.enabled;
|
|
169
153
|
}
|
|
@@ -199,18 +183,27 @@ class Player extends events_1.EventEmitter {
|
|
|
199
183
|
this.loudnessMaxCutDb = Math.max(0, loudnessOptions.maxCutDb ?? 10);
|
|
200
184
|
this.loudnessLimiterCeiling = Math.min(1, Math.max(0.1, loudnessOptions.limiterCeiling ?? 0.95));
|
|
201
185
|
this.debug(`[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}`);
|
|
186
|
+
this.trackMiddlewareChain = [...this.manager.getTrackMiddlewareChain(), ...(0, types_1.normalizeTrackMiddleware)(options.trackMiddleware)];
|
|
202
187
|
this.filter = new FilterManager_1.FilterManager(this, this.manager);
|
|
203
188
|
this.extensionManager = new extensions_1.ExtensionManager(this, this.manager);
|
|
204
189
|
this.pluginManager = new plugins_1.PluginManager(this, this.manager, {
|
|
205
190
|
extractorTimeout: this.options.extractorTimeout,
|
|
206
191
|
});
|
|
207
192
|
this.streamManager = new StreamManager_1.StreamManager({
|
|
208
|
-
maxConcurrentStreams:
|
|
193
|
+
maxConcurrentStreams: 2,
|
|
209
194
|
streamTimeout: 5 * 60 * 1000,
|
|
210
195
|
maxListenersPerStream: 15,
|
|
211
196
|
enableMetrics: true,
|
|
212
197
|
autoDestroy: true,
|
|
213
198
|
});
|
|
199
|
+
this.preloadManager = new PreloadManager_1.PreloadManager({
|
|
200
|
+
streamManager: this.streamManager,
|
|
201
|
+
debug: this.debug.bind(this),
|
|
202
|
+
getNextTrack: () => this.queue.nextTrack,
|
|
203
|
+
getStream: (track) => this.getStream(track),
|
|
204
|
+
isDestroyed: () => this.destroyed,
|
|
205
|
+
isEnabled: () => this.preloadEnabled,
|
|
206
|
+
});
|
|
214
207
|
this.volume = this.options.volume || 100;
|
|
215
208
|
this.userdata = this.options.userdata;
|
|
216
209
|
this.searchCache = new lru_cache_1.LRUCache({
|
|
@@ -495,6 +488,9 @@ class Player extends events_1.EventEmitter {
|
|
|
495
488
|
this.queue.add(tracksToAdd[0]);
|
|
496
489
|
this.emit("queueAdd", tracksToAdd[0]);
|
|
497
490
|
}
|
|
491
|
+
if (this.isPlaying && !this.destroyed) {
|
|
492
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload after queue add error:", err));
|
|
493
|
+
}
|
|
498
494
|
const started = !this.isPlaying ? await this.playNext() : true;
|
|
499
495
|
await this.extensionManager.afterPlayHooks({
|
|
500
496
|
success: started,
|
|
@@ -525,273 +521,20 @@ class Player extends events_1.EventEmitter {
|
|
|
525
521
|
* Main preload method - only one at a time
|
|
526
522
|
*/
|
|
527
523
|
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
|
-
}
|
|
524
|
+
await this.preloadManager.preloadNextTrack();
|
|
646
525
|
}
|
|
647
526
|
/**
|
|
648
527
|
* Safe cancel preload - doesn't throw
|
|
649
528
|
*/
|
|
650
529
|
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
|
-
}
|
|
530
|
+
await this.preloadManager.safeCancelPreload();
|
|
713
531
|
}
|
|
532
|
+
// Preload stream fetch/cancel flow has been moved to PreloadManager.
|
|
714
533
|
/**
|
|
715
534
|
* Preload next track with proper error handling and cleanup
|
|
716
535
|
*/
|
|
717
536
|
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
|
-
}
|
|
537
|
+
await this.preloadManager.preloadNextTrack();
|
|
795
538
|
}
|
|
796
539
|
async fadeResourceVolume(resource, from, to, durationMs) {
|
|
797
540
|
if (!resource.volume)
|
|
@@ -916,7 +659,7 @@ class Player extends events_1.EventEmitter {
|
|
|
916
659
|
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
917
660
|
}
|
|
918
661
|
try {
|
|
919
|
-
if (this.antiStuckReusePreloadFirst && this.
|
|
662
|
+
if (this.antiStuckReusePreloadFirst && this.preloadManager.hasValidPreload(track)) {
|
|
920
663
|
const startedFromPreload = await this.startTrack(track);
|
|
921
664
|
if (startedFromPreload) {
|
|
922
665
|
this.antiStuckConsecutiveFailures = 0;
|
|
@@ -945,59 +688,11 @@ class Player extends events_1.EventEmitter {
|
|
|
945
688
|
this.debug(`[AntiStuck] Keeping track for controlled retry window: ${track.title}`);
|
|
946
689
|
return false;
|
|
947
690
|
}
|
|
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
691
|
/**
|
|
990
692
|
* Cancel preload (when skipping or stopping)
|
|
991
693
|
*/
|
|
992
694
|
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);
|
|
695
|
+
this.preloadManager.cancelPreload();
|
|
1001
696
|
}
|
|
1002
697
|
/**
|
|
1003
698
|
* Clear a stream slot
|
|
@@ -1030,25 +725,8 @@ class Player extends events_1.EventEmitter {
|
|
|
1030
725
|
* Promote preload slot to current slot without destroying promoted stream.
|
|
1031
726
|
*/
|
|
1032
727
|
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;
|
|
728
|
+
const promoted = this.preloadManager.promoteToCurrent(track, this.currentSlot);
|
|
729
|
+
this.currentResource = promoted;
|
|
1052
730
|
}
|
|
1053
731
|
/**
|
|
1054
732
|
* Create AudioResource with filters and seek applied
|
|
@@ -1097,7 +775,37 @@ class Player extends events_1.EventEmitter {
|
|
|
1097
775
|
}
|
|
1098
776
|
}
|
|
1099
777
|
}
|
|
778
|
+
mergeTrackPreserveRef(target, source) {
|
|
779
|
+
if (source === target)
|
|
780
|
+
return;
|
|
781
|
+
const mergedMeta = {
|
|
782
|
+
...(target.metadata || {}),
|
|
783
|
+
...(source.metadata || {}),
|
|
784
|
+
};
|
|
785
|
+
Object.assign(target, source);
|
|
786
|
+
target.metadata = mergedMeta;
|
|
787
|
+
}
|
|
788
|
+
async applyTrackMiddleware(track) {
|
|
789
|
+
if (this.trackMiddlewareChain.length === 0)
|
|
790
|
+
return;
|
|
791
|
+
const ctx = { player: this, manager: this.manager };
|
|
792
|
+
for (const mw of this.trackMiddlewareChain) {
|
|
793
|
+
try {
|
|
794
|
+
const out = await mw(track, ctx);
|
|
795
|
+
if (out != null && out !== track) {
|
|
796
|
+
this.mergeTrackPreserveRef(track, out);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
this.debug(`[TrackMiddleware] Error:`, err);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
1100
804
|
async getStream(track) {
|
|
805
|
+
if (this.destroyed) {
|
|
806
|
+
throw new Error("PLAYER_DESTROYED");
|
|
807
|
+
}
|
|
808
|
+
await this.applyTrackMiddleware(track);
|
|
1101
809
|
const trackId = track.id || track.url || track.title;
|
|
1102
810
|
const existingStream = this.streamManager.getStreamByTrack(trackId);
|
|
1103
811
|
if (existingStream && !existingStream.destroyed) {
|
|
@@ -1105,17 +813,23 @@ class Player extends events_1.EventEmitter {
|
|
|
1105
813
|
return { stream: existingStream, type: "arbitrary" };
|
|
1106
814
|
}
|
|
1107
815
|
let stream = await this.extensionManager.provideStream(track);
|
|
816
|
+
if (this.destroyed) {
|
|
817
|
+
if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
|
|
818
|
+
stream.stream.destroy();
|
|
819
|
+
}
|
|
820
|
+
throw new Error("PLAYER_DESTROYED");
|
|
821
|
+
}
|
|
1108
822
|
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}`);
|
|
823
|
+
this.debug(`[Stream] Extension provided stream for: ${track.title}`);
|
|
1116
824
|
return stream;
|
|
1117
825
|
}
|
|
1118
826
|
stream = await this.pluginManager.getStream(track);
|
|
827
|
+
if (this.destroyed) {
|
|
828
|
+
if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
|
|
829
|
+
stream.stream.destroy();
|
|
830
|
+
}
|
|
831
|
+
throw new Error("PLAYER_DESTROYED");
|
|
832
|
+
}
|
|
1119
833
|
if (stream?.stream) {
|
|
1120
834
|
const existingAgain = this.streamManager.getStreamByTrack(trackId);
|
|
1121
835
|
if (existingAgain && !existingAgain.destroyed) {
|
|
@@ -1124,12 +838,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1124
838
|
return { stream: existingAgain, type: "arbitrary" };
|
|
1125
839
|
}
|
|
1126
840
|
// 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}`);
|
|
841
|
+
this.debug(`[Stream] Plugin provided stream for: ${track.title}`);
|
|
1133
842
|
return stream;
|
|
1134
843
|
}
|
|
1135
844
|
if (!this.pluginManager.hasStreamCandidate(track)) {
|
|
@@ -1146,12 +855,11 @@ class Player extends events_1.EventEmitter {
|
|
|
1146
855
|
* Start playing a specific track immediately, replacing the current resource.
|
|
1147
856
|
*/
|
|
1148
857
|
async startTrack(track) {
|
|
858
|
+
if (this.destroyed)
|
|
859
|
+
return false;
|
|
1149
860
|
try {
|
|
1150
861
|
// 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) {
|
|
862
|
+
if (this.preloadManager.hasValidPreload(track)) {
|
|
1155
863
|
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
1156
864
|
// Stop current playback
|
|
1157
865
|
this.audioPlayer.stop(true);
|
|
@@ -1200,12 +908,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1200
908
|
* Swap preload slot to current slot
|
|
1201
909
|
*/
|
|
1202
910
|
async swapToCurrent(track) {
|
|
1203
|
-
|
|
1204
|
-
const newResource = this.preloadSlot.resource;
|
|
1205
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
1206
|
-
if (!newResource) {
|
|
911
|
+
if (!this.preloadManager.hasValidPreload(track)) {
|
|
1207
912
|
return false;
|
|
1208
913
|
}
|
|
914
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1209
915
|
// Stop current playback
|
|
1210
916
|
this.audioPlayer.stop(true);
|
|
1211
917
|
// Clean up old current stream (but keep it for a moment)
|
|
@@ -1249,6 +955,8 @@ class Player extends events_1.EventEmitter {
|
|
|
1249
955
|
* Load fresh stream when no preload available
|
|
1250
956
|
*/
|
|
1251
957
|
async loadFreshStream(track) {
|
|
958
|
+
if (this.destroyed)
|
|
959
|
+
return false;
|
|
1252
960
|
// Cancel preload to free resources
|
|
1253
961
|
await this.safeCancelPreload();
|
|
1254
962
|
try {
|
|
@@ -1286,9 +994,11 @@ class Player extends events_1.EventEmitter {
|
|
|
1286
994
|
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
1287
995
|
await this.applyCrossfadeIn(resource, track);
|
|
1288
996
|
// Preload next (async)
|
|
1289
|
-
this.
|
|
1290
|
-
this.
|
|
1291
|
-
|
|
997
|
+
if (!this.destroyed) {
|
|
998
|
+
this.preloadNextTrack().catch((err) => {
|
|
999
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1292
1002
|
return true;
|
|
1293
1003
|
}
|
|
1294
1004
|
catch (error) {
|
|
@@ -1300,6 +1010,8 @@ class Player extends events_1.EventEmitter {
|
|
|
1300
1010
|
* Play the next track in the queue, handling errors and edge cases gracefully
|
|
1301
1011
|
*/
|
|
1302
1012
|
async playNext() {
|
|
1013
|
+
if (this.destroyed)
|
|
1014
|
+
return false;
|
|
1303
1015
|
this.debug("[Player] playNext called");
|
|
1304
1016
|
// Don't cancel preload here unless absolutely necessary
|
|
1305
1017
|
// Let startTrack handle it
|
|
@@ -1311,6 +1023,14 @@ class Player extends events_1.EventEmitter {
|
|
|
1311
1023
|
const willnext = this.queue.willNextTrack();
|
|
1312
1024
|
if (willnext) {
|
|
1313
1025
|
this.queue.addMultiple([willnext]);
|
|
1026
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload autoplay error:", err));
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
await this.generateWillNext().catch((err) => this.debug("[Player] generateWillNext autoplay fallback error:", err));
|
|
1030
|
+
const generatedNext = this.queue.willNextTrack();
|
|
1031
|
+
if (generatedNext) {
|
|
1032
|
+
this.queue.add(generatedNext);
|
|
1033
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload autoplay generated error:", err));
|
|
1314
1034
|
continue;
|
|
1315
1035
|
}
|
|
1316
1036
|
}
|
|
@@ -1398,6 +1118,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1398
1118
|
if (!this.connection)
|
|
1399
1119
|
throw new Error("No voice connection for TTS");
|
|
1400
1120
|
const ttsPlayer = this.ensureTTSPlayer();
|
|
1121
|
+
await this.applyTrackMiddleware(track);
|
|
1401
1122
|
// Build resource from plugin stream
|
|
1402
1123
|
const streamInfo = await this.pluginManager.getStream(track);
|
|
1403
1124
|
if (!streamInfo) {
|
|
@@ -1515,6 +1236,54 @@ class Player extends events_1.EventEmitter {
|
|
|
1515
1236
|
throw error;
|
|
1516
1237
|
}
|
|
1517
1238
|
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Subscribe this player's voice connection
|
|
1241
|
+
* to another player's audio stream.
|
|
1242
|
+
*
|
|
1243
|
+
* This is primarily used for:
|
|
1244
|
+
* - playback mirroring
|
|
1245
|
+
* - radio/broadcast systems
|
|
1246
|
+
* - multi-guild synchronized playback
|
|
1247
|
+
* - forwardMode shared streaming
|
|
1248
|
+
*
|
|
1249
|
+
* Instead of creating a separate audio stream,
|
|
1250
|
+
* this player will directly receive audio packets
|
|
1251
|
+
* from the target player's AudioPlayer instance.
|
|
1252
|
+
*
|
|
1253
|
+
* Benefits:
|
|
1254
|
+
* - drastically lower CPU usage
|
|
1255
|
+
* - only one ffmpeg/extractor stream
|
|
1256
|
+
* - lower bandwidth and memory usage
|
|
1257
|
+
* - perfect sync across guilds
|
|
1258
|
+
*
|
|
1259
|
+
* Important:
|
|
1260
|
+
* - both players must already have active voice connections
|
|
1261
|
+
* - this does NOT transfer queue ownership
|
|
1262
|
+
* - this does NOT clone playback state automatically
|
|
1263
|
+
*
|
|
1264
|
+
* @param {Player} player - Source player to subscribe to
|
|
1265
|
+
*
|
|
1266
|
+
* @returns {boolean}
|
|
1267
|
+
* Returns true if subscription succeeded,
|
|
1268
|
+
* otherwise false.
|
|
1269
|
+
*
|
|
1270
|
+
* @example
|
|
1271
|
+
* follower.subscribeTo(leader);
|
|
1272
|
+
*
|
|
1273
|
+
* @example
|
|
1274
|
+
* if (!player.subscribeTo(leader)) {
|
|
1275
|
+
* console.log("Failed to subscribe");
|
|
1276
|
+
* }
|
|
1277
|
+
*/
|
|
1278
|
+
subscribeTo(player) {
|
|
1279
|
+
if (!this.connection)
|
|
1280
|
+
return false;
|
|
1281
|
+
this.connection.subscribe(player.audioPlayer);
|
|
1282
|
+
this.isPlaying = player.isPlaying;
|
|
1283
|
+
this.isPaused = player.isPaused;
|
|
1284
|
+
this.forwardMode = true;
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1518
1287
|
/**
|
|
1519
1288
|
* Pause the current track
|
|
1520
1289
|
*
|
|
@@ -1702,6 +1471,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1702
1471
|
saveOptions = options;
|
|
1703
1472
|
}
|
|
1704
1473
|
try {
|
|
1474
|
+
await this.applyTrackMiddleware(track);
|
|
1705
1475
|
// Skip extension manager for saving - we want the raw stream without filters/seek applied, and extensions may not support this
|
|
1706
1476
|
let streamInfo = await this.pluginManager.getStream(track);
|
|
1707
1477
|
if (!streamInfo || !streamInfo.stream) {
|
|
@@ -2061,6 +1831,9 @@ class Player extends events_1.EventEmitter {
|
|
|
2061
1831
|
*/
|
|
2062
1832
|
destroy() {
|
|
2063
1833
|
this.debug(`[Player] destroy called`);
|
|
1834
|
+
if (this.destroyed)
|
|
1835
|
+
return;
|
|
1836
|
+
this.destroyed = true;
|
|
2064
1837
|
if (this.leaveTimeout) {
|
|
2065
1838
|
clearTimeout(this.leaveTimeout);
|
|
2066
1839
|
this.leaveTimeout = null;
|
|
@@ -2069,10 +1842,10 @@ class Player extends events_1.EventEmitter {
|
|
|
2069
1842
|
// Destroy current stream before stopping audio
|
|
2070
1843
|
this.destroyCurrentStream();
|
|
2071
1844
|
this.clearSlot(this.currentSlot);
|
|
2072
|
-
this.
|
|
1845
|
+
this.preloadManager.clearPreloadSlot();
|
|
2073
1846
|
this.audioPlayer.removeAllListeners();
|
|
2074
1847
|
this.audioPlayer.stop(true);
|
|
2075
|
-
this.
|
|
1848
|
+
this.preloadManager.cancelPreload();
|
|
2076
1849
|
if (this.ttsPlayer) {
|
|
2077
1850
|
try {
|
|
2078
1851
|
this.ttsPlayer.stop(true);
|
|
@@ -2230,6 +2003,8 @@ class Player extends events_1.EventEmitter {
|
|
|
2230
2003
|
}
|
|
2231
2004
|
setupEventListeners() {
|
|
2232
2005
|
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
2006
|
+
if (this.destroyed)
|
|
2007
|
+
return;
|
|
2233
2008
|
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
2234
2009
|
if (newState.status === voice_1.AudioPlayerStatus.Idle && oldState.status !== voice_1.AudioPlayerStatus.Idle) {
|
|
2235
2010
|
// Track ended
|
|
@@ -2238,7 +2013,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2238
2013
|
this.debug(`[Player] Track ended: ${track.title}`);
|
|
2239
2014
|
this.emit("trackEnd", track);
|
|
2240
2015
|
}
|
|
2241
|
-
this.playNext();
|
|
2016
|
+
void this.playNext();
|
|
2242
2017
|
}
|
|
2243
2018
|
else if (newState.status === voice_1.AudioPlayerStatus.Playing &&
|
|
2244
2019
|
(oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) {
|
|
@@ -2300,18 +2075,20 @@ class Player extends events_1.EventEmitter {
|
|
|
2300
2075
|
}
|
|
2301
2076
|
});
|
|
2302
2077
|
this.audioPlayer.on("error", (error) => {
|
|
2078
|
+
if (this.destroyed)
|
|
2079
|
+
return;
|
|
2303
2080
|
this.debug(`[Player] AudioPlayer error:`, error);
|
|
2304
2081
|
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
2305
2082
|
const track = this.queue.currentTrack;
|
|
2306
2083
|
if (track && this.antiStuckEnabled) {
|
|
2307
2084
|
void this.attemptTrackRecovery(track, error).then((recovered) => {
|
|
2308
2085
|
if (!recovered) {
|
|
2309
|
-
this.playNext();
|
|
2086
|
+
void this.playNext();
|
|
2310
2087
|
}
|
|
2311
2088
|
});
|
|
2312
2089
|
return;
|
|
2313
2090
|
}
|
|
2314
|
-
this.playNext();
|
|
2091
|
+
void this.playNext();
|
|
2315
2092
|
});
|
|
2316
2093
|
this.audioPlayer.on("debug", (...args) => {
|
|
2317
2094
|
if (this.manager.debugEnabled) {
|