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
package/src/structures/Player.ts
CHANGED
|
@@ -31,7 +31,6 @@ import type {
|
|
|
31
31
|
ExtensionPlayRequest,
|
|
32
32
|
ExtensionPlayResponse,
|
|
33
33
|
ExtensionAfterPlayPayload,
|
|
34
|
-
PreloadState,
|
|
35
34
|
StreamSlot,
|
|
36
35
|
} from "../types";
|
|
37
36
|
import type { PlayerManager } from "./PlayerManager";
|
|
@@ -39,9 +38,9 @@ import type { PlayerManager } from "./PlayerManager";
|
|
|
39
38
|
import { Queue } from "./Queue";
|
|
40
39
|
import { PluginManager } from "../plugins";
|
|
41
40
|
import { ExtensionManager } from "../extensions";
|
|
42
|
-
import { withTimeout } from "../utils/timeout";
|
|
43
41
|
import { FilterManager } from "./FilterManager";
|
|
44
42
|
import { StreamManager } from "./StreamManager";
|
|
43
|
+
import { PreloadManager } from "./PreloadManager";
|
|
45
44
|
|
|
46
45
|
export declare interface Player {
|
|
47
46
|
on<K extends keyof PlayerEvents>(event: K, listener: (...args: PlayerEvents[K]) => void): this;
|
|
@@ -96,6 +95,7 @@ export class Player extends EventEmitter {
|
|
|
96
95
|
public pluginManager: PluginManager;
|
|
97
96
|
public extensionManager: ExtensionManager;
|
|
98
97
|
public streamManager: StreamManager;
|
|
98
|
+
public preloadManager: PreloadManager;
|
|
99
99
|
|
|
100
100
|
public userdata?: Record<string, any>;
|
|
101
101
|
public _lastActivity: number = Date.now();
|
|
@@ -108,17 +108,7 @@ export class Player extends EventEmitter {
|
|
|
108
108
|
private skipLoop = false;
|
|
109
109
|
private filter!: FilterManager;
|
|
110
110
|
private refreshLock = false;
|
|
111
|
-
//preloaded resource
|
|
112
111
|
|
|
113
|
-
private preloadState: PreloadState = {
|
|
114
|
-
resource: null,
|
|
115
|
-
track: null,
|
|
116
|
-
abortController: null,
|
|
117
|
-
timeoutId: null,
|
|
118
|
-
isValid: false,
|
|
119
|
-
isBeingUsed: false,
|
|
120
|
-
};
|
|
121
|
-
private isPreloading = false;
|
|
122
112
|
private currentSlot: StreamSlot = {
|
|
123
113
|
resource: null,
|
|
124
114
|
track: null,
|
|
@@ -129,16 +119,6 @@ export class Player extends EventEmitter {
|
|
|
129
119
|
loadPromise: null,
|
|
130
120
|
};
|
|
131
121
|
|
|
132
|
-
private preloadSlot: StreamSlot = {
|
|
133
|
-
resource: null,
|
|
134
|
-
track: null,
|
|
135
|
-
streamId: null,
|
|
136
|
-
abortController: null,
|
|
137
|
-
isValid: false,
|
|
138
|
-
isLoading: false,
|
|
139
|
-
loadPromise: null,
|
|
140
|
-
};
|
|
141
|
-
private preloadLock = false;
|
|
142
122
|
private preloadEnabled = true;
|
|
143
123
|
private crossfadeEnabled = true;
|
|
144
124
|
private crossfadeDurationMs = 500;
|
|
@@ -173,6 +153,7 @@ export class Player extends EventEmitter {
|
|
|
173
153
|
private loudnessMaxBoostDb = 8;
|
|
174
154
|
private loudnessMaxCutDb = 10;
|
|
175
155
|
private loudnessLimiterCeiling = 0.95;
|
|
156
|
+
private destroyed = false;
|
|
176
157
|
|
|
177
158
|
// Cache for search results to avoid duplicate calls
|
|
178
159
|
private searchCache: LRUCache<string, SearchResult>;
|
|
@@ -223,7 +204,7 @@ export class Player extends EventEmitter {
|
|
|
223
204
|
const crossfadeOptions = this.options.crossfade || {};
|
|
224
205
|
const crossfadeAutoEnable = crossfadeOptions.autoEnable ?? true;
|
|
225
206
|
const crossfadeAutoDisable = crossfadeOptions.autoDisableInLowPerformance ?? true;
|
|
226
|
-
this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ??
|
|
207
|
+
this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ?? 500);
|
|
227
208
|
|
|
228
209
|
if (typeof crossfadeOptions.enabled === "boolean") {
|
|
229
210
|
this.crossfadeEnabled = crossfadeOptions.enabled;
|
|
@@ -272,12 +253,20 @@ export class Player extends EventEmitter {
|
|
|
272
253
|
extractorTimeout: this.options.extractorTimeout,
|
|
273
254
|
});
|
|
274
255
|
this.streamManager = new StreamManager({
|
|
275
|
-
maxConcurrentStreams:
|
|
256
|
+
maxConcurrentStreams: 2,
|
|
276
257
|
streamTimeout: 5 * 60 * 1000,
|
|
277
258
|
maxListenersPerStream: 15,
|
|
278
259
|
enableMetrics: true,
|
|
279
260
|
autoDestroy: true,
|
|
280
261
|
});
|
|
262
|
+
this.preloadManager = new PreloadManager({
|
|
263
|
+
streamManager: this.streamManager,
|
|
264
|
+
debug: this.debug.bind(this),
|
|
265
|
+
getNextTrack: () => this.queue.nextTrack,
|
|
266
|
+
getStream: (track) => this.getStream(track),
|
|
267
|
+
isDestroyed: () => this.destroyed,
|
|
268
|
+
isEnabled: () => this.preloadEnabled,
|
|
269
|
+
});
|
|
281
270
|
this.volume = this.options.volume || 100;
|
|
282
271
|
this.userdata = this.options.userdata;
|
|
283
272
|
this.searchCache = new LRUCache<string, SearchResult>({
|
|
@@ -601,6 +590,10 @@ export class Player extends EventEmitter {
|
|
|
601
590
|
this.emit("queueAdd", tracksToAdd[0]);
|
|
602
591
|
}
|
|
603
592
|
|
|
593
|
+
if (this.isPlaying && !this.destroyed) {
|
|
594
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload after queue add error:", err));
|
|
595
|
+
}
|
|
596
|
+
|
|
604
597
|
const started = !this.isPlaying ? await this.playNext() : true;
|
|
605
598
|
|
|
606
599
|
await this.extensionManager.afterPlayHooks({
|
|
@@ -632,311 +625,22 @@ export class Player extends EventEmitter {
|
|
|
632
625
|
* Main preload method - only one at a time
|
|
633
626
|
*/
|
|
634
627
|
private async preloadNextTrack(): Promise<void> {
|
|
635
|
-
|
|
636
|
-
this.debug(`[Preload] Disabled by options/runtime profile`);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Prevent concurrent preloads
|
|
641
|
-
if (this.preloadLock) {
|
|
642
|
-
this.debug(`[Preload] Already preloading, skipping`);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const nextTrack = this.queue.nextTrack;
|
|
647
|
-
if (!nextTrack) {
|
|
648
|
-
this.debug(`[Preload] No next track to preload`);
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Check if already preloaded correctly
|
|
653
|
-
if (this.preloadSlot.isValid && this.preloadSlot.track?.id === nextTrack.id && this.preloadSlot.resource) {
|
|
654
|
-
this.debug(`[Preload] Already have valid preload for: ${nextTrack.title}`);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Check if currently loading the same track
|
|
659
|
-
if (this.preloadSlot.isLoading && this.preloadSlot.track?.id === nextTrack.id) {
|
|
660
|
-
this.debug(`[Preload] Currently loading same track, waiting...`);
|
|
661
|
-
if (this.preloadSlot.loadPromise) {
|
|
662
|
-
await this.preloadSlot.loadPromise;
|
|
663
|
-
}
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Cancel old preload if different track
|
|
668
|
-
if (this.preloadSlot.isValid && this.preloadSlot.track?.id !== nextTrack.id) {
|
|
669
|
-
this.debug(`[Preload] Cancelling old preload for different track: ${this.preloadSlot.track?.title}`);
|
|
670
|
-
await this.safeCancelPreload();
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
this.preloadLock = true;
|
|
674
|
-
|
|
675
|
-
// Create new abort controller
|
|
676
|
-
const abortController = new AbortController();
|
|
677
|
-
|
|
678
|
-
// Setup preload slot
|
|
679
|
-
this.preloadSlot.track = nextTrack;
|
|
680
|
-
this.preloadSlot.abortController = abortController;
|
|
681
|
-
this.preloadSlot.isLoading = true;
|
|
682
|
-
|
|
683
|
-
// Create load promise
|
|
684
|
-
const loadPromise = this.executePreload(nextTrack, abortController);
|
|
685
|
-
this.preloadSlot.loadPromise = loadPromise;
|
|
686
|
-
|
|
687
|
-
try {
|
|
688
|
-
await loadPromise;
|
|
689
|
-
} catch (err) {
|
|
690
|
-
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
691
|
-
this.debug(`[Preload] Cancelled for ${nextTrack.title}`);
|
|
692
|
-
} else {
|
|
693
|
-
this.debug(`[Preload] Failed for ${nextTrack.title}:`, err);
|
|
694
|
-
}
|
|
695
|
-
this.clearSlot(this.preloadSlot);
|
|
696
|
-
} finally {
|
|
697
|
-
this.preloadLock = false;
|
|
698
|
-
this.preloadSlot.isLoading = false;
|
|
699
|
-
this.preloadSlot.loadPromise = null;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* Execute actual preload
|
|
705
|
-
*/
|
|
706
|
-
private async executePreload(track: Track, abortController: AbortController): Promise<void> {
|
|
707
|
-
this.debug(`[Preload] Starting preload for: ${track.title}`);
|
|
708
|
-
|
|
709
|
-
// Check for cancellation
|
|
710
|
-
if (abortController.signal.aborted) {
|
|
711
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// Check if track still relevant
|
|
715
|
-
if (this.queue.nextTrack?.id !== track.id) {
|
|
716
|
-
this.debug(`[Preload] Track changed, cancelling`);
|
|
717
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
try {
|
|
721
|
-
// Get stream with abort support - NO TIMEOUT
|
|
722
|
-
const streamInfo = await this.getStreamWithCancel(track, abortController.signal);
|
|
723
|
-
|
|
724
|
-
// Check cancellation
|
|
725
|
-
if (abortController.signal.aborted) {
|
|
726
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Check track relevance again
|
|
730
|
-
if (this.queue.nextTrack?.id !== track.id) {
|
|
731
|
-
this.debug(`[Preload] Track changed after stream fetch`);
|
|
732
|
-
throw new Error("PRELOAD_CANCELLED");
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
if (!streamInfo?.stream) {
|
|
736
|
-
throw new Error(`No stream available`);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// Register with StreamManager as preload
|
|
740
|
-
const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
741
|
-
source: track.source || "preload",
|
|
742
|
-
isPreload: true,
|
|
743
|
-
priority: 5,
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
// Create resource
|
|
747
|
-
const resource = createAudioResource(streamInfo.stream, {
|
|
748
|
-
inlineVolume: true,
|
|
749
|
-
metadata: { ...track, preloaded: true },
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
// Verify resource is valid
|
|
753
|
-
if (!resource.playStream || resource.playStream.readable === false) {
|
|
754
|
-
throw new Error("Resource not readable");
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Update preload slot
|
|
758
|
-
this.preloadSlot.resource = resource;
|
|
759
|
-
this.preloadSlot.streamId = streamId;
|
|
760
|
-
this.preloadSlot.isValid = true;
|
|
761
|
-
this.preloadSlot.track = track;
|
|
762
|
-
|
|
763
|
-
this.debug(`[Preload] Successfully preloaded: ${track.title} (Stream ID: ${streamId})`);
|
|
764
|
-
} catch (err) {
|
|
765
|
-
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
766
|
-
throw err;
|
|
767
|
-
}
|
|
768
|
-
this.debug(`[Preload] Error during preload:`, err);
|
|
769
|
-
throw err;
|
|
770
|
-
}
|
|
628
|
+
await this.preloadManager.preloadNextTrack();
|
|
771
629
|
}
|
|
772
630
|
|
|
773
631
|
/**
|
|
774
632
|
* Safe cancel preload - doesn't throw
|
|
775
633
|
*/
|
|
776
634
|
private async safeCancelPreload(): Promise<void> {
|
|
777
|
-
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
this.debug(`[Preload] Safely cancelling preload for: ${this.preloadSlot.track?.title || "unknown"}`);
|
|
782
|
-
|
|
783
|
-
// Abort the operation
|
|
784
|
-
if (this.preloadSlot.abortController) {
|
|
785
|
-
this.preloadSlot.abortController.abort();
|
|
786
|
-
this.preloadSlot.abortController = null;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// Clean up stream
|
|
790
|
-
if (this.preloadSlot.streamId && this.streamManager) {
|
|
791
|
-
this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
// Clean up resource
|
|
795
|
-
if (this.preloadSlot.resource) {
|
|
796
|
-
try {
|
|
797
|
-
const stream = this.preloadSlot.resource.playStream;
|
|
798
|
-
if (stream && typeof stream.destroy === "function" && !stream.destroyed) {
|
|
799
|
-
stream.destroy();
|
|
800
|
-
}
|
|
801
|
-
} catch (err) {
|
|
802
|
-
// Ignore destroy errors
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// Clear slot
|
|
807
|
-
this.clearSlot(this.preloadSlot);
|
|
635
|
+
await this.preloadManager.safeCancelPreload();
|
|
808
636
|
}
|
|
809
637
|
|
|
810
|
-
|
|
811
|
-
* Get stream with proper cancellation
|
|
812
|
-
*/
|
|
813
|
-
private async getStreamWithCancel(track: Track, signal: AbortSignal): Promise<StreamInfo | null> {
|
|
814
|
-
// Create abort promise
|
|
815
|
-
const abortPromise = new Promise<never>((_, reject) => {
|
|
816
|
-
if (signal.aborted) {
|
|
817
|
-
reject(new Error("PRELOAD_CANCELLED"));
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
const handler = () => {
|
|
821
|
-
signal.removeEventListener("abort", handler);
|
|
822
|
-
reject(new Error("PRELOAD_CANCELLED"));
|
|
823
|
-
};
|
|
824
|
-
signal.addEventListener("abort", handler);
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
try {
|
|
828
|
-
// Check if stream already exists and is valid
|
|
829
|
-
const existingStream = this.streamManager.getStreamByTrack(track.id || track.title);
|
|
830
|
-
if (existingStream && !existingStream.destroyed && existingStream.readable !== false) {
|
|
831
|
-
this.debug(`[Stream] Using existing stream for preload: ${track.title}`);
|
|
832
|
-
return { stream: existingStream, type: "arbitrary" };
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// Race between stream fetch and abort
|
|
836
|
-
const streamPromise = this.getStream(track);
|
|
837
|
-
const result = await Promise.race([streamPromise, abortPromise]);
|
|
838
|
-
return result as StreamInfo | null;
|
|
839
|
-
} catch (err) {
|
|
840
|
-
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
841
|
-
throw err;
|
|
842
|
-
}
|
|
843
|
-
throw err;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
638
|
+
// Preload stream fetch/cancel flow has been moved to PreloadManager.
|
|
846
639
|
/**
|
|
847
640
|
* Preload next track with proper error handling and cleanup
|
|
848
641
|
*/
|
|
849
642
|
async preloadNext(): Promise<void> {
|
|
850
|
-
|
|
851
|
-
this.debug(`[Preload] Disabled by options/runtime profile`);
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
this.cancelPreload();
|
|
856
|
-
|
|
857
|
-
const next = this.queue.nextTrack;
|
|
858
|
-
if (!next || this.isPreloading) {
|
|
859
|
-
this.debug(`[Preload] Skipped - ${!next ? "no next track" : "already preloading"}`);
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
this.isPreloading = true;
|
|
864
|
-
|
|
865
|
-
// Create new AbortController
|
|
866
|
-
const abortController = new AbortController();
|
|
867
|
-
const timeoutId = setTimeout(() => {
|
|
868
|
-
// this.debug(`[Preload] Timeout for track: ${next.title}`);
|
|
869
|
-
// abortController.abort();
|
|
870
|
-
}, 30000);
|
|
871
|
-
|
|
872
|
-
this.preloadState.abortController = abortController;
|
|
873
|
-
this.preloadState.timeoutId = timeoutId;
|
|
874
|
-
|
|
875
|
-
try {
|
|
876
|
-
this.debug(`[Preload] Starting preload for: ${next.title}`);
|
|
877
|
-
|
|
878
|
-
// Check if already aborted
|
|
879
|
-
if (abortController.signal.aborted) {
|
|
880
|
-
throw new Error("Preload aborted before start");
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// Check if this track is still the next one
|
|
884
|
-
if (this.queue.nextTrack?.id !== next.id) {
|
|
885
|
-
this.debug(`[Preload] Track changed, cancelling preload`);
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const streamInfo = await this.getStreamWithCancel(next, abortController.signal);
|
|
890
|
-
|
|
891
|
-
// Double check
|
|
892
|
-
if (abortController.signal.aborted) {
|
|
893
|
-
throw new Error("Preload aborted after stream fetch");
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
if (this.queue.nextTrack?.id !== next.id) {
|
|
897
|
-
this.debug(`[Preload] Track changed after stream fetch`);
|
|
898
|
-
return;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
if (!streamInfo?.stream) {
|
|
902
|
-
throw new Error(`No stream available`);
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// Register with StreamManager
|
|
906
|
-
const streamId = this.streamManager.registerStream(streamInfo.stream, next, {
|
|
907
|
-
source: next.source || "preload",
|
|
908
|
-
isPreload: true,
|
|
909
|
-
priority: 8,
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
// Create resource
|
|
913
|
-
const resource = createAudioResource(streamInfo.stream, {
|
|
914
|
-
inlineVolume: true,
|
|
915
|
-
metadata: { ...next, preloaded: true },
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
// Store preload state
|
|
919
|
-
this.preloadState = {
|
|
920
|
-
resource,
|
|
921
|
-
track: next,
|
|
922
|
-
abortController,
|
|
923
|
-
timeoutId,
|
|
924
|
-
isValid: true,
|
|
925
|
-
isBeingUsed: false,
|
|
926
|
-
streamId,
|
|
927
|
-
};
|
|
928
|
-
|
|
929
|
-
this.debug(`[Preload] Successfully preloaded: ${next.title} (Stream ID: ${streamId})`);
|
|
930
|
-
} catch (err) {
|
|
931
|
-
if (err instanceof Error && err.message.includes("aborted")) {
|
|
932
|
-
this.debug(`[Preload] Cancelled for ${next.title}`);
|
|
933
|
-
} else {
|
|
934
|
-
this.debug(`[Preload] Failed for ${next?.title}:`, err);
|
|
935
|
-
}
|
|
936
|
-
this.cancelPreload();
|
|
937
|
-
} finally {
|
|
938
|
-
this.isPreloading = false;
|
|
939
|
-
}
|
|
643
|
+
await this.preloadManager.preloadNextTrack();
|
|
940
644
|
}
|
|
941
645
|
|
|
942
646
|
private async fadeResourceVolume(resource: AudioResource, from: number, to: number, durationMs: number): Promise<void> {
|
|
@@ -1074,7 +778,7 @@ export class Player extends EventEmitter {
|
|
|
1074
778
|
}
|
|
1075
779
|
|
|
1076
780
|
try {
|
|
1077
|
-
if (this.antiStuckReusePreloadFirst && this.
|
|
781
|
+
if (this.antiStuckReusePreloadFirst && this.preloadManager.hasValidPreload(track)) {
|
|
1078
782
|
const startedFromPreload = await this.startTrack(track);
|
|
1079
783
|
if (startedFromPreload) {
|
|
1080
784
|
this.antiStuckConsecutiveFailures = 0;
|
|
@@ -1106,63 +810,11 @@ export class Player extends EventEmitter {
|
|
|
1106
810
|
return false;
|
|
1107
811
|
}
|
|
1108
812
|
|
|
1109
|
-
/**
|
|
1110
|
-
* Clear preloaded resource with proper cleanup
|
|
1111
|
-
*/
|
|
1112
|
-
private clearPreload(): void {
|
|
1113
|
-
// Abort ongoing preload
|
|
1114
|
-
if (this.preloadState.abortController) {
|
|
1115
|
-
this.preloadState.abortController.abort();
|
|
1116
|
-
this.preloadState.abortController = null;
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
// Clean up stream
|
|
1120
|
-
const stream = (this.preloadState as any).stream;
|
|
1121
|
-
if (stream && typeof stream.destroy === "function") {
|
|
1122
|
-
try {
|
|
1123
|
-
stream.destroy();
|
|
1124
|
-
} catch (err) {
|
|
1125
|
-
this.debug(`[Preload] Error destroying stream:`, err);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// Clean up resource
|
|
1130
|
-
if (this.preloadState.resource) {
|
|
1131
|
-
try {
|
|
1132
|
-
const playStream = this.preloadState.resource.playStream;
|
|
1133
|
-
if (playStream && typeof playStream.destroy === "function") {
|
|
1134
|
-
playStream.destroy();
|
|
1135
|
-
}
|
|
1136
|
-
} catch (err) {
|
|
1137
|
-
this.debug(`[Preload] Error destroying resource:`, err);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
this.preloadState = {
|
|
1142
|
-
resource: null,
|
|
1143
|
-
track: null,
|
|
1144
|
-
abortController: null,
|
|
1145
|
-
timeoutId: null,
|
|
1146
|
-
isValid: false,
|
|
1147
|
-
isBeingUsed: false,
|
|
1148
|
-
streamId: undefined,
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
813
|
/**
|
|
1153
814
|
* Cancel preload (when skipping or stopping)
|
|
1154
815
|
*/
|
|
1155
816
|
private cancelPreload(): void {
|
|
1156
|
-
|
|
1157
|
-
this.debug(`[Preload] Cancelling preload for: ${this.preloadSlot.track?.title}`);
|
|
1158
|
-
this.preloadSlot.abortController.abort();
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
if (this.preloadSlot.streamId && this.streamManager) {
|
|
1162
|
-
this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
this.clearSlot(this.preloadSlot);
|
|
817
|
+
this.preloadManager.cancelPreload();
|
|
1166
818
|
}
|
|
1167
819
|
|
|
1168
820
|
/**
|
|
@@ -1198,27 +850,8 @@ export class Player extends EventEmitter {
|
|
|
1198
850
|
* Promote preload slot to current slot without destroying promoted stream.
|
|
1199
851
|
*/
|
|
1200
852
|
private promotePreloadToCurrent(track: Track): void {
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
// Move ownership to current slot.
|
|
1205
|
-
this.currentSlot.resource = promotedResource;
|
|
1206
|
-
this.currentSlot.track = track;
|
|
1207
|
-
this.currentSlot.streamId = promotedStreamId;
|
|
1208
|
-
this.currentSlot.abortController = null;
|
|
1209
|
-
this.currentSlot.isValid = !!promotedResource;
|
|
1210
|
-
this.currentSlot.isLoading = false;
|
|
1211
|
-
this.currentSlot.loadPromise = null;
|
|
1212
|
-
this.currentResource = promotedResource;
|
|
1213
|
-
|
|
1214
|
-
// Reset preload slot only (do not destroy promoted resource/stream).
|
|
1215
|
-
this.preloadSlot.resource = null;
|
|
1216
|
-
this.preloadSlot.track = null;
|
|
1217
|
-
this.preloadSlot.streamId = null;
|
|
1218
|
-
this.preloadSlot.abortController = null;
|
|
1219
|
-
this.preloadSlot.isValid = false;
|
|
1220
|
-
this.preloadSlot.isLoading = false;
|
|
1221
|
-
this.preloadSlot.loadPromise = null;
|
|
853
|
+
const promoted = this.preloadManager.promoteToCurrent(track, this.currentSlot);
|
|
854
|
+
this.currentResource = promoted;
|
|
1222
855
|
}
|
|
1223
856
|
|
|
1224
857
|
/**
|
|
@@ -1274,6 +907,9 @@ export class Player extends EventEmitter {
|
|
|
1274
907
|
}
|
|
1275
908
|
|
|
1276
909
|
private async getStream(track: Track): Promise<StreamInfo | null> {
|
|
910
|
+
if (this.destroyed) {
|
|
911
|
+
throw new Error("PLAYER_DESTROYED");
|
|
912
|
+
}
|
|
1277
913
|
const trackId = track.id || track.url || track.title;
|
|
1278
914
|
const existingStream = this.streamManager.getStreamByTrack(trackId);
|
|
1279
915
|
|
|
@@ -1283,18 +919,24 @@ export class Player extends EventEmitter {
|
|
|
1283
919
|
}
|
|
1284
920
|
|
|
1285
921
|
let stream = await this.extensionManager.provideStream(track);
|
|
922
|
+
if (this.destroyed) {
|
|
923
|
+
if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
|
|
924
|
+
stream.stream.destroy();
|
|
925
|
+
}
|
|
926
|
+
throw new Error("PLAYER_DESTROYED");
|
|
927
|
+
}
|
|
1286
928
|
if (stream?.stream) {
|
|
1287
|
-
|
|
1288
|
-
const streamId = this.streamManager.registerStream(stream.stream, track, {
|
|
1289
|
-
source: "extension",
|
|
1290
|
-
isPreload: false,
|
|
1291
|
-
priority: 10,
|
|
1292
|
-
});
|
|
1293
|
-
this.debug(`[Stream] Extension stream registered with ID: ${streamId}`);
|
|
929
|
+
this.debug(`[Stream] Extension provided stream for: ${track.title}`);
|
|
1294
930
|
return stream;
|
|
1295
931
|
}
|
|
1296
932
|
|
|
1297
933
|
stream = await this.pluginManager.getStream(track);
|
|
934
|
+
if (this.destroyed) {
|
|
935
|
+
if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
|
|
936
|
+
stream.stream.destroy();
|
|
937
|
+
}
|
|
938
|
+
throw new Error("PLAYER_DESTROYED");
|
|
939
|
+
}
|
|
1298
940
|
if (stream?.stream) {
|
|
1299
941
|
const existingAgain = this.streamManager.getStreamByTrack(trackId);
|
|
1300
942
|
if (existingAgain && !existingAgain.destroyed) {
|
|
@@ -1302,30 +944,30 @@ export class Player extends EventEmitter {
|
|
|
1302
944
|
return { stream: existingAgain, type: "arbitrary" };
|
|
1303
945
|
}
|
|
1304
946
|
// Register with StreamManager
|
|
1305
|
-
|
|
1306
|
-
source: track.source || "plugin",
|
|
1307
|
-
isPreload: false,
|
|
1308
|
-
priority: 5,
|
|
1309
|
-
});
|
|
1310
|
-
this.debug(`[Stream] Plugin stream registered with ID: ${streamId}`);
|
|
947
|
+
this.debug(`[Stream] Plugin provided stream for: ${track.title}`);
|
|
1311
948
|
return stream;
|
|
1312
949
|
}
|
|
1313
950
|
|
|
951
|
+
if (!this.pluginManager.hasStreamCandidate(track)) {
|
|
952
|
+
throw new Error(`UNRECOVERABLE_NO_PLUGIN:${track.title}`);
|
|
953
|
+
}
|
|
954
|
+
|
|
1314
955
|
throw new Error(`No stream available for track: ${track.title}`);
|
|
1315
956
|
}
|
|
1316
957
|
|
|
958
|
+
private isUnrecoverableStreamError(error: unknown): boolean {
|
|
959
|
+
if (!(error instanceof Error)) return false;
|
|
960
|
+
return error.message.startsWith("UNRECOVERABLE_NO_PLUGIN:");
|
|
961
|
+
}
|
|
962
|
+
|
|
1317
963
|
/**
|
|
1318
964
|
* Start playing a specific track immediately, replacing the current resource.
|
|
1319
965
|
*/
|
|
1320
966
|
private async startTrack(track: Track): Promise<boolean> {
|
|
967
|
+
if (this.destroyed) return false;
|
|
1321
968
|
try {
|
|
1322
969
|
// Try to use preloaded resource
|
|
1323
|
-
if (
|
|
1324
|
-
this.preloadSlot.isValid &&
|
|
1325
|
-
this.preloadSlot.track?.id === track.id &&
|
|
1326
|
-
this.preloadSlot.resource &&
|
|
1327
|
-
this.preloadSlot.resource.playStream?.readable !== false
|
|
1328
|
-
) {
|
|
970
|
+
if (this.preloadManager.hasValidPreload(track)) {
|
|
1329
971
|
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
1330
972
|
|
|
1331
973
|
// Stop current playback
|
|
@@ -1382,13 +1024,10 @@ export class Player extends EventEmitter {
|
|
|
1382
1024
|
* Swap preload slot to current slot
|
|
1383
1025
|
*/
|
|
1384
1026
|
private async swapToCurrent(track: Track): Promise<boolean> {
|
|
1385
|
-
|
|
1386
|
-
const newResource = this.preloadSlot.resource;
|
|
1387
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
1388
|
-
|
|
1389
|
-
if (!newResource) {
|
|
1027
|
+
if (!this.preloadManager.hasValidPreload(track)) {
|
|
1390
1028
|
return false;
|
|
1391
1029
|
}
|
|
1030
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1392
1031
|
|
|
1393
1032
|
// Stop current playback
|
|
1394
1033
|
this.audioPlayer.stop(true);
|
|
@@ -1440,6 +1079,7 @@ export class Player extends EventEmitter {
|
|
|
1440
1079
|
* Load fresh stream when no preload available
|
|
1441
1080
|
*/
|
|
1442
1081
|
private async loadFreshStream(track: Track): Promise<boolean> {
|
|
1082
|
+
if (this.destroyed) return false;
|
|
1443
1083
|
// Cancel preload to free resources
|
|
1444
1084
|
await this.safeCancelPreload();
|
|
1445
1085
|
|
|
@@ -1486,9 +1126,11 @@ export class Player extends EventEmitter {
|
|
|
1486
1126
|
await this.applyCrossfadeIn(resource, track);
|
|
1487
1127
|
|
|
1488
1128
|
// Preload next (async)
|
|
1489
|
-
this.
|
|
1490
|
-
this.
|
|
1491
|
-
|
|
1129
|
+
if (!this.destroyed) {
|
|
1130
|
+
this.preloadNextTrack().catch((err) => {
|
|
1131
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1492
1134
|
|
|
1493
1135
|
return true;
|
|
1494
1136
|
} catch (error) {
|
|
@@ -1501,6 +1143,7 @@ export class Player extends EventEmitter {
|
|
|
1501
1143
|
* Play the next track in the queue, handling errors and edge cases gracefully
|
|
1502
1144
|
*/
|
|
1503
1145
|
private async playNext(): Promise<boolean> {
|
|
1146
|
+
if (this.destroyed) return false;
|
|
1504
1147
|
this.debug("[Player] playNext called");
|
|
1505
1148
|
|
|
1506
1149
|
// Don't cancel preload here unless absolutely necessary
|
|
@@ -1515,6 +1158,15 @@ export class Player extends EventEmitter {
|
|
|
1515
1158
|
const willnext = this.queue.willNextTrack();
|
|
1516
1159
|
if (willnext) {
|
|
1517
1160
|
this.queue.addMultiple([willnext]);
|
|
1161
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload autoplay error:", err));
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
await this.generateWillNext().catch((err) => this.debug("[Player] generateWillNext autoplay fallback error:", err));
|
|
1166
|
+
const generatedNext = this.queue.willNextTrack();
|
|
1167
|
+
if (generatedNext) {
|
|
1168
|
+
this.queue.add(generatedNext);
|
|
1169
|
+
void this.preloadNextTrack().catch((err) => this.debug("[Player] Preload autoplay generated error:", err));
|
|
1518
1170
|
continue;
|
|
1519
1171
|
}
|
|
1520
1172
|
}
|
|
@@ -1557,6 +1209,10 @@ export class Player extends EventEmitter {
|
|
|
1557
1209
|
} catch (err) {
|
|
1558
1210
|
this.debug(`[Player] playNext error:`, err);
|
|
1559
1211
|
this.emit("playerError", err as Error, track);
|
|
1212
|
+
if (this.isUnrecoverableStreamError(err)) {
|
|
1213
|
+
this.debug(`[Player] Skipping unrecoverable track (no plugin): ${track.title}`);
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1560
1216
|
const recovered = await this.attemptTrackRecovery(track, err);
|
|
1561
1217
|
if (recovered) {
|
|
1562
1218
|
return true;
|
|
@@ -2327,6 +1983,8 @@ export class Player extends EventEmitter {
|
|
|
2327
1983
|
*/
|
|
2328
1984
|
destroy(): void {
|
|
2329
1985
|
this.debug(`[Player] destroy called`);
|
|
1986
|
+
if (this.destroyed) return;
|
|
1987
|
+
this.destroyed = true;
|
|
2330
1988
|
|
|
2331
1989
|
if (this.leaveTimeout) {
|
|
2332
1990
|
clearTimeout(this.leaveTimeout);
|
|
@@ -2337,11 +1995,11 @@ export class Player extends EventEmitter {
|
|
|
2337
1995
|
this.destroyCurrentStream();
|
|
2338
1996
|
|
|
2339
1997
|
this.clearSlot(this.currentSlot);
|
|
2340
|
-
this.
|
|
1998
|
+
this.preloadManager.clearPreloadSlot();
|
|
2341
1999
|
|
|
2342
2000
|
this.audioPlayer.removeAllListeners();
|
|
2343
2001
|
this.audioPlayer.stop(true);
|
|
2344
|
-
this.
|
|
2002
|
+
this.preloadManager.cancelPreload();
|
|
2345
2003
|
|
|
2346
2004
|
if (this.ttsPlayer) {
|
|
2347
2005
|
try {
|
|
@@ -2517,6 +2175,7 @@ export class Player extends EventEmitter {
|
|
|
2517
2175
|
|
|
2518
2176
|
private setupEventListeners(): void {
|
|
2519
2177
|
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
2178
|
+
if (this.destroyed) return;
|
|
2520
2179
|
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
2521
2180
|
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
|
|
2522
2181
|
// Track ended
|
|
@@ -2525,7 +2184,7 @@ export class Player extends EventEmitter {
|
|
|
2525
2184
|
this.debug(`[Player] Track ended: ${track.title}`);
|
|
2526
2185
|
this.emit("trackEnd", track);
|
|
2527
2186
|
}
|
|
2528
|
-
this.playNext();
|
|
2187
|
+
void this.playNext();
|
|
2529
2188
|
} else if (
|
|
2530
2189
|
newState.status === AudioPlayerStatus.Playing &&
|
|
2531
2190
|
(oldState.status === AudioPlayerStatus.Idle || oldState.status === AudioPlayerStatus.Buffering)
|
|
@@ -2583,18 +2242,19 @@ export class Player extends EventEmitter {
|
|
|
2583
2242
|
}
|
|
2584
2243
|
});
|
|
2585
2244
|
this.audioPlayer.on("error", (error) => {
|
|
2245
|
+
if (this.destroyed) return;
|
|
2586
2246
|
this.debug(`[Player] AudioPlayer error:`, error);
|
|
2587
2247
|
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
2588
2248
|
const track = this.queue.currentTrack;
|
|
2589
2249
|
if (track && this.antiStuckEnabled) {
|
|
2590
2250
|
void this.attemptTrackRecovery(track, error).then((recovered) => {
|
|
2591
2251
|
if (!recovered) {
|
|
2592
|
-
this.playNext();
|
|
2252
|
+
void this.playNext();
|
|
2593
2253
|
}
|
|
2594
2254
|
});
|
|
2595
2255
|
return;
|
|
2596
2256
|
}
|
|
2597
|
-
this.playNext();
|
|
2257
|
+
void this.playNext();
|
|
2598
2258
|
});
|
|
2599
2259
|
|
|
2600
2260
|
this.audioPlayer.on("debug", (...args) => {
|