ziplayer 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/structures/Player.d.ts +3 -16
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +74 -377
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PreloadManager.d.ts +32 -0
- package/dist/structures/PreloadManager.d.ts.map +1 -0
- package/dist/structures/PreloadManager.js +230 -0
- package/dist/structures/PreloadManager.js.map +1 -0
- package/dist/structures/StreamManager.d.ts +1 -0
- package/dist/structures/StreamManager.d.ts.map +1 -1
- package/dist/structures/StreamManager.js +37 -3
- package/dist/structures/StreamManager.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/structures/Player.ts +71 -424
- package/src/structures/PreloadManager.ts +274 -0
- package/src/structures/StreamManager.ts +41 -4
- package/src/types/index.ts +1 -1
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,12 +944,7 @@ 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
|
|
|
@@ -1327,14 +964,10 @@ export class Player extends EventEmitter {
|
|
|
1327
964
|
* Start playing a specific track immediately, replacing the current resource.
|
|
1328
965
|
*/
|
|
1329
966
|
private async startTrack(track: Track): Promise<boolean> {
|
|
967
|
+
if (this.destroyed) return false;
|
|
1330
968
|
try {
|
|
1331
969
|
// Try to use preloaded resource
|
|
1332
|
-
if (
|
|
1333
|
-
this.preloadSlot.isValid &&
|
|
1334
|
-
this.preloadSlot.track?.id === track.id &&
|
|
1335
|
-
this.preloadSlot.resource &&
|
|
1336
|
-
this.preloadSlot.resource.playStream?.readable !== false
|
|
1337
|
-
) {
|
|
970
|
+
if (this.preloadManager.hasValidPreload(track)) {
|
|
1338
971
|
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
1339
972
|
|
|
1340
973
|
// Stop current playback
|
|
@@ -1391,13 +1024,10 @@ export class Player extends EventEmitter {
|
|
|
1391
1024
|
* Swap preload slot to current slot
|
|
1392
1025
|
*/
|
|
1393
1026
|
private async swapToCurrent(track: Track): Promise<boolean> {
|
|
1394
|
-
|
|
1395
|
-
const newResource = this.preloadSlot.resource;
|
|
1396
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
1397
|
-
|
|
1398
|
-
if (!newResource) {
|
|
1027
|
+
if (!this.preloadManager.hasValidPreload(track)) {
|
|
1399
1028
|
return false;
|
|
1400
1029
|
}
|
|
1030
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1401
1031
|
|
|
1402
1032
|
// Stop current playback
|
|
1403
1033
|
this.audioPlayer.stop(true);
|
|
@@ -1449,6 +1079,7 @@ export class Player extends EventEmitter {
|
|
|
1449
1079
|
* Load fresh stream when no preload available
|
|
1450
1080
|
*/
|
|
1451
1081
|
private async loadFreshStream(track: Track): Promise<boolean> {
|
|
1082
|
+
if (this.destroyed) return false;
|
|
1452
1083
|
// Cancel preload to free resources
|
|
1453
1084
|
await this.safeCancelPreload();
|
|
1454
1085
|
|
|
@@ -1495,9 +1126,11 @@ export class Player extends EventEmitter {
|
|
|
1495
1126
|
await this.applyCrossfadeIn(resource, track);
|
|
1496
1127
|
|
|
1497
1128
|
// Preload next (async)
|
|
1498
|
-
this.
|
|
1499
|
-
this.
|
|
1500
|
-
|
|
1129
|
+
if (!this.destroyed) {
|
|
1130
|
+
this.preloadNextTrack().catch((err) => {
|
|
1131
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1501
1134
|
|
|
1502
1135
|
return true;
|
|
1503
1136
|
} catch (error) {
|
|
@@ -1510,6 +1143,7 @@ export class Player extends EventEmitter {
|
|
|
1510
1143
|
* Play the next track in the queue, handling errors and edge cases gracefully
|
|
1511
1144
|
*/
|
|
1512
1145
|
private async playNext(): Promise<boolean> {
|
|
1146
|
+
if (this.destroyed) return false;
|
|
1513
1147
|
this.debug("[Player] playNext called");
|
|
1514
1148
|
|
|
1515
1149
|
// Don't cancel preload here unless absolutely necessary
|
|
@@ -1524,6 +1158,15 @@ export class Player extends EventEmitter {
|
|
|
1524
1158
|
const willnext = this.queue.willNextTrack();
|
|
1525
1159
|
if (willnext) {
|
|
1526
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));
|
|
1527
1170
|
continue;
|
|
1528
1171
|
}
|
|
1529
1172
|
}
|
|
@@ -2340,6 +1983,8 @@ export class Player extends EventEmitter {
|
|
|
2340
1983
|
*/
|
|
2341
1984
|
destroy(): void {
|
|
2342
1985
|
this.debug(`[Player] destroy called`);
|
|
1986
|
+
if (this.destroyed) return;
|
|
1987
|
+
this.destroyed = true;
|
|
2343
1988
|
|
|
2344
1989
|
if (this.leaveTimeout) {
|
|
2345
1990
|
clearTimeout(this.leaveTimeout);
|
|
@@ -2350,11 +1995,11 @@ export class Player extends EventEmitter {
|
|
|
2350
1995
|
this.destroyCurrentStream();
|
|
2351
1996
|
|
|
2352
1997
|
this.clearSlot(this.currentSlot);
|
|
2353
|
-
this.
|
|
1998
|
+
this.preloadManager.clearPreloadSlot();
|
|
2354
1999
|
|
|
2355
2000
|
this.audioPlayer.removeAllListeners();
|
|
2356
2001
|
this.audioPlayer.stop(true);
|
|
2357
|
-
this.
|
|
2002
|
+
this.preloadManager.cancelPreload();
|
|
2358
2003
|
|
|
2359
2004
|
if (this.ttsPlayer) {
|
|
2360
2005
|
try {
|
|
@@ -2530,6 +2175,7 @@ export class Player extends EventEmitter {
|
|
|
2530
2175
|
|
|
2531
2176
|
private setupEventListeners(): void {
|
|
2532
2177
|
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
2178
|
+
if (this.destroyed) return;
|
|
2533
2179
|
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
2534
2180
|
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
|
|
2535
2181
|
// Track ended
|
|
@@ -2538,7 +2184,7 @@ export class Player extends EventEmitter {
|
|
|
2538
2184
|
this.debug(`[Player] Track ended: ${track.title}`);
|
|
2539
2185
|
this.emit("trackEnd", track);
|
|
2540
2186
|
}
|
|
2541
|
-
this.playNext();
|
|
2187
|
+
void this.playNext();
|
|
2542
2188
|
} else if (
|
|
2543
2189
|
newState.status === AudioPlayerStatus.Playing &&
|
|
2544
2190
|
(oldState.status === AudioPlayerStatus.Idle || oldState.status === AudioPlayerStatus.Buffering)
|
|
@@ -2596,18 +2242,19 @@ export class Player extends EventEmitter {
|
|
|
2596
2242
|
}
|
|
2597
2243
|
});
|
|
2598
2244
|
this.audioPlayer.on("error", (error) => {
|
|
2245
|
+
if (this.destroyed) return;
|
|
2599
2246
|
this.debug(`[Player] AudioPlayer error:`, error);
|
|
2600
2247
|
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
2601
2248
|
const track = this.queue.currentTrack;
|
|
2602
2249
|
if (track && this.antiStuckEnabled) {
|
|
2603
2250
|
void this.attemptTrackRecovery(track, error).then((recovered) => {
|
|
2604
2251
|
if (!recovered) {
|
|
2605
|
-
this.playNext();
|
|
2252
|
+
void this.playNext();
|
|
2606
2253
|
}
|
|
2607
2254
|
});
|
|
2608
2255
|
return;
|
|
2609
2256
|
}
|
|
2610
|
-
this.playNext();
|
|
2257
|
+
void this.playNext();
|
|
2611
2258
|
});
|
|
2612
2259
|
|
|
2613
2260
|
this.audioPlayer.on("debug", (...args) => {
|