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.
Files changed (41) hide show
  1. package/{AI-Guide.md → AGENTS.md} +36 -7
  2. package/README.md +113 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +3 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/plugins/index.js +1 -1
  8. package/dist/plugins/index.js.map +1 -1
  9. package/dist/structures/Player.d.ts +48 -17
  10. package/dist/structures/Player.d.ts.map +1 -1
  11. package/dist/structures/Player.js +154 -377
  12. package/dist/structures/Player.js.map +1 -1
  13. package/dist/structures/PlayerManager.d.ts +35 -6
  14. package/dist/structures/PlayerManager.d.ts.map +1 -1
  15. package/dist/structures/PlayerManager.js +215 -19
  16. package/dist/structures/PlayerManager.js.map +1 -1
  17. package/dist/structures/PreloadManager.d.ts +32 -0
  18. package/dist/structures/PreloadManager.d.ts.map +1 -0
  19. package/dist/structures/PreloadManager.js +230 -0
  20. package/dist/structures/PreloadManager.js.map +1 -0
  21. package/dist/structures/Queue.d.ts +19 -0
  22. package/dist/structures/Queue.d.ts.map +1 -1
  23. package/dist/structures/Queue.js +21 -0
  24. package/dist/structures/Queue.js.map +1 -1
  25. package/dist/structures/StreamManager.d.ts +1 -0
  26. package/dist/structures/StreamManager.d.ts.map +1 -1
  27. package/dist/structures/StreamManager.js +37 -3
  28. package/dist/structures/StreamManager.js.map +1 -1
  29. package/dist/types/index.d.ts +41 -1
  30. package/dist/types/index.d.ts.map +1 -1
  31. package/dist/types/index.js +6 -0
  32. package/dist/types/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/index.ts +1 -0
  35. package/src/plugins/index.ts +1 -1
  36. package/src/structures/Player.ts +174 -440
  37. package/src/structures/PlayerManager.ts +253 -23
  38. package/src/structures/PreloadManager.ts +274 -0
  39. package/src/structures/Queue.ts +22 -0
  40. package/src/structures/StreamManager.ts +41 -4
  41. 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 ?? 5000);
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: 20,
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
- if (!this.preloadEnabled) {
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
- if (!this.preloadSlot.abortController && !this.preloadSlot.resource) {
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
- if (!this.preloadEnabled) {
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.preloadSlot.isValid && this.preloadSlot.track?.id === track.id) {
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
- if (this.preloadSlot.abortController) {
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 promotedResource = this.preloadSlot.resource;
1034
- const promotedStreamId = this.preloadSlot.streamId;
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
- // Register with StreamManager
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
- const streamId = this.streamManager.registerStream(stream.stream, track, {
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.preloadSlot.isValid &&
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
- // Store preload resource
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.preloadNextTrack().catch((err) => {
1290
- this.debug(`[Player] Preload error:`, err);
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.clearSlot(this.preloadSlot);
1845
+ this.preloadManager.clearPreloadSlot();
2073
1846
  this.audioPlayer.removeAllListeners();
2074
1847
  this.audioPlayer.stop(true);
2075
- this.clearPreload();
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) {