ziplayer 0.3.7 → 0.3.9
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/plugins/index.d.ts +1 -8
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +52 -106
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.d.ts +7 -24
- package/dist/structures/FilterManager.d.ts.map +1 -1
- package/dist/structures/FilterManager.js +123 -99
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/Player.d.ts +2 -0
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +113 -91
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PreloadManager.d.ts +1 -0
- package/dist/structures/PreloadManager.d.ts.map +1 -1
- package/dist/structures/PreloadManager.js +26 -6
- package/dist/structures/PreloadManager.js.map +1 -1
- package/dist/structures/StreamManager.d.ts +1 -0
- package/dist/structures/StreamManager.d.ts.map +1 -1
- package/dist/structures/StreamManager.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/index.ts +63 -119
- package/src/structures/FilterManager.ts +439 -380
- package/src/structures/Player.ts +133 -99
- package/src/structures/PreloadManager.ts +293 -274
- package/src/structures/StreamManager.ts +2 -0
- package/src/types/index.ts +2 -0
|
@@ -112,6 +112,7 @@ class Player extends events_1.EventEmitter {
|
|
|
112
112
|
this.ttsPlayer = null;
|
|
113
113
|
this.lastDuration = 0;
|
|
114
114
|
this.seekOffset = 0;
|
|
115
|
+
this.recoveryInProgress = false;
|
|
115
116
|
this.debug(`[Player] Constructor called for guildId: ${guildId}`);
|
|
116
117
|
this.guildId = guildId;
|
|
117
118
|
this.queue = new Queue_1.Queue();
|
|
@@ -193,7 +194,7 @@ class Player extends events_1.EventEmitter {
|
|
|
193
194
|
extractorTimeout: this.options.extractorTimeout,
|
|
194
195
|
});
|
|
195
196
|
this.streamManager = new StreamManager_1.StreamManager({
|
|
196
|
-
maxConcurrentStreams:
|
|
197
|
+
maxConcurrentStreams: this.options?.maxStreamStore ?? 4,
|
|
197
198
|
streamTimeout: 5 * 60 * 1000,
|
|
198
199
|
maxListenersPerStream: 15,
|
|
199
200
|
enableMetrics: true,
|
|
@@ -659,6 +660,7 @@ class Player extends events_1.EventEmitter {
|
|
|
659
660
|
async attemptTrackRecovery(track, reason) {
|
|
660
661
|
if (!this.antiStuckEnabled)
|
|
661
662
|
return false;
|
|
663
|
+
this.recoveryInProgress = true;
|
|
662
664
|
this.debug(`[AntiStuck] Recovery started for: ${track.title}`, reason);
|
|
663
665
|
const originalQuality = this.options.quality;
|
|
664
666
|
let attempted = 0;
|
|
@@ -676,6 +678,7 @@ class Player extends events_1.EventEmitter {
|
|
|
676
678
|
if (startedFromPreload) {
|
|
677
679
|
this.antiStuckConsecutiveFailures = 0;
|
|
678
680
|
this.options.quality = originalQuality;
|
|
681
|
+
this.recoveryInProgress = false;
|
|
679
682
|
return true;
|
|
680
683
|
}
|
|
681
684
|
}
|
|
@@ -683,6 +686,7 @@ class Player extends events_1.EventEmitter {
|
|
|
683
686
|
if (started) {
|
|
684
687
|
this.antiStuckConsecutiveFailures = 0;
|
|
685
688
|
this.options.quality = originalQuality;
|
|
689
|
+
this.recoveryInProgress = false;
|
|
686
690
|
return true;
|
|
687
691
|
}
|
|
688
692
|
}
|
|
@@ -694,8 +698,10 @@ class Player extends events_1.EventEmitter {
|
|
|
694
698
|
this.antiStuckConsecutiveFailures++;
|
|
695
699
|
if (this.antiStuckConsecutiveFailures >= this.antiStuckControlledSkipThreshold) {
|
|
696
700
|
this.debug(`[AntiStuck] Controlled skip threshold reached for ${track.title}`);
|
|
701
|
+
this.recoveryInProgress = false;
|
|
697
702
|
return false;
|
|
698
703
|
}
|
|
704
|
+
this.recoveryInProgress = false;
|
|
699
705
|
// Avoid hard skip storm by leaving track for next natural retry window.
|
|
700
706
|
this.debug(`[AntiStuck] Keeping track for controlled retry window: ${track.title}`);
|
|
701
707
|
return false;
|
|
@@ -751,25 +757,26 @@ class Player extends events_1.EventEmitter {
|
|
|
751
757
|
async createResource(streamInfo, track, position = 0) {
|
|
752
758
|
const filterString = this.filter.getFilterString();
|
|
753
759
|
this.debug(`[Player] Creating AudioResource — filters: ${filterString || "none"}, seek: ${position}ms`);
|
|
754
|
-
|
|
760
|
+
this.filter.setSourceStreamType(streamInfo.type);
|
|
755
761
|
const seekArg = position > 0 ? position : -1;
|
|
756
762
|
if (filterString || position > 0) {
|
|
757
|
-
// throws on failure — do NOT fall back to the already-piped stream
|
|
758
763
|
const processedStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, seekArg);
|
|
759
|
-
|
|
760
|
-
|
|
764
|
+
// rawStream. Always use Arbitrary so discordjs/voice doesn't re-encode.
|
|
765
|
+
const resource = (0, voice_1.createAudioResource)(processedStream, {
|
|
761
766
|
metadata: track,
|
|
762
767
|
inputType: voice_1.StreamType.Arbitrary,
|
|
763
768
|
inlineVolume: true,
|
|
764
769
|
});
|
|
770
|
+
return { resource, processedStream };
|
|
765
771
|
}
|
|
766
|
-
|
|
772
|
+
const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
767
773
|
metadata: track,
|
|
768
774
|
inputType: streamInfo.type === "webm/opus" ? voice_1.StreamType.WebmOpus
|
|
769
775
|
: streamInfo.type === "ogg/opus" ? voice_1.StreamType.OggOpus
|
|
770
776
|
: voice_1.StreamType.Arbitrary,
|
|
771
777
|
inlineVolume: true,
|
|
772
778
|
});
|
|
779
|
+
return { resource, processedStream: null };
|
|
773
780
|
}
|
|
774
781
|
mergeTrackPreserveRef(target, source) {
|
|
775
782
|
if (source === target)
|
|
@@ -870,7 +877,14 @@ class Player extends events_1.EventEmitter {
|
|
|
870
877
|
async startTrack(track) {
|
|
871
878
|
if (this.destroyed)
|
|
872
879
|
return false;
|
|
873
|
-
//
|
|
880
|
+
// Check preload BEFORE calling getStream so we never fetch a
|
|
881
|
+
// stream we're about to throw away. The original code called getStream()
|
|
882
|
+
// unconditionally at the top, then used the preload if available — leaking
|
|
883
|
+
// the just-fetched stream and running middleware twice.
|
|
884
|
+
if (this.preloadManager.hasValidPreload(track)) {
|
|
885
|
+
return await this.startFromPreload(track);
|
|
886
|
+
}
|
|
887
|
+
// Only fetch a stream when there is no usable preload.
|
|
874
888
|
let streamInfo = null;
|
|
875
889
|
try {
|
|
876
890
|
streamInfo = await this.getStream(track);
|
|
@@ -879,101 +893,103 @@ class Player extends events_1.EventEmitter {
|
|
|
879
893
|
this.debug(`[Player] Failed to get stream for track: ${track.title}`, error);
|
|
880
894
|
throw error;
|
|
881
895
|
}
|
|
882
|
-
//
|
|
896
|
+
// Remote playback
|
|
883
897
|
if (streamInfo?.remote && streamInfo.handle) {
|
|
884
898
|
return await this.playRemote(track, streamInfo);
|
|
885
899
|
}
|
|
886
|
-
//
|
|
900
|
+
// Native playback — pass the already-fetched streamInfo to avoid a second fetch
|
|
901
|
+
return await this.loadFreshStream(track, streamInfo);
|
|
902
|
+
}
|
|
903
|
+
async startFromPreload(track) {
|
|
904
|
+
if (this.destroyed)
|
|
905
|
+
return false;
|
|
906
|
+
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
907
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
908
|
+
this.promotePreloadToCurrent(track);
|
|
909
|
+
if (oldStreamId && oldStreamId !== this.currentSlot.streamId) {
|
|
910
|
+
this.streamManager.unregisterStream(oldStreamId, true);
|
|
911
|
+
this.debug(`[Player] Released old stream ${oldStreamId} after preload promotion`);
|
|
912
|
+
}
|
|
913
|
+
const currentResource = this.currentSlot.resource;
|
|
914
|
+
if (!currentResource)
|
|
915
|
+
return false;
|
|
916
|
+
this.seekOffset = 0;
|
|
917
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
918
|
+
if (currentResource.volume) {
|
|
919
|
+
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
920
|
+
}
|
|
921
|
+
await this.maybeAlignToBeatBoundary();
|
|
922
|
+
this.refreshLock = true;
|
|
887
923
|
try {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
892
|
-
if (oldStreamId && this.streamManager) {
|
|
893
|
-
setTimeout(() => {
|
|
894
|
-
if (this.currentSlot.streamId === oldStreamId) {
|
|
895
|
-
this.streamManager.unregisterStream(oldStreamId, true);
|
|
896
|
-
}
|
|
897
|
-
}, 3000);
|
|
898
|
-
}
|
|
899
|
-
this.promotePreloadToCurrent(track);
|
|
900
|
-
const currentResource = this.currentSlot.resource;
|
|
901
|
-
if (!currentResource) {
|
|
902
|
-
return false;
|
|
903
|
-
}
|
|
904
|
-
this.seekOffset = 0;
|
|
905
|
-
const targetVolume = this.getTrackTargetVolume(track);
|
|
906
|
-
if (currentResource.volume) {
|
|
907
|
-
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
908
|
-
}
|
|
909
|
-
await this.maybeAlignToBeatBoundary();
|
|
910
|
-
this.refreshLock = true;
|
|
911
|
-
try {
|
|
912
|
-
this.audioPlayer.stop(true);
|
|
913
|
-
this.audioPlayer.play(currentResource);
|
|
914
|
-
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
915
|
-
}
|
|
916
|
-
finally {
|
|
917
|
-
this.refreshLock = false;
|
|
918
|
-
}
|
|
919
|
-
await this.applyCrossfadeIn(currentResource, track);
|
|
920
|
-
this.preloadNextTrack().catch((err) => {
|
|
921
|
-
this.debug(`[Player] Preload error:`, err);
|
|
922
|
-
});
|
|
923
|
-
return true;
|
|
924
|
-
}
|
|
925
|
-
this.debug(`[Player] No preload available, loading fresh: ${track.title}`);
|
|
926
|
-
return await this.loadFreshStream(track);
|
|
924
|
+
this.audioPlayer.stop(true);
|
|
925
|
+
this.audioPlayer.play(currentResource);
|
|
926
|
+
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
927
927
|
}
|
|
928
|
-
|
|
929
|
-
this.
|
|
930
|
-
this.emit("playerError", error, track);
|
|
931
|
-
return false;
|
|
928
|
+
finally {
|
|
929
|
+
this.refreshLock = false;
|
|
932
930
|
}
|
|
931
|
+
await this.applyCrossfadeIn(currentResource, track);
|
|
932
|
+
this.preloadNextTrack().catch((err) => {
|
|
933
|
+
this.debug(`[Player] Preload error:`, err);
|
|
934
|
+
});
|
|
935
|
+
return true;
|
|
933
936
|
}
|
|
934
937
|
/**
|
|
935
938
|
* Load fresh stream when no preload available
|
|
936
939
|
*/
|
|
937
|
-
async loadFreshStream(track) {
|
|
940
|
+
async loadFreshStream(track, preloadedStreamInfo) {
|
|
938
941
|
if (this.destroyed)
|
|
939
942
|
return false;
|
|
940
|
-
// Cancel preload to free resources
|
|
941
943
|
await this.safeCancelPreload();
|
|
942
944
|
try {
|
|
943
|
-
|
|
944
|
-
//
|
|
945
|
+
// use caller-supplied streamInfo when available so we don't
|
|
946
|
+
// call getStream() a second time and run middleware twice.
|
|
947
|
+
const streamInfo = preloadedStreamInfo ?? (await this.getStream(track));
|
|
945
948
|
if (streamInfo?.remote && streamInfo.handle) {
|
|
946
949
|
return await this.playRemote(track, streamInfo);
|
|
947
950
|
}
|
|
948
951
|
if (!streamInfo?.stream) {
|
|
949
952
|
throw new Error(`No stream available`);
|
|
950
953
|
}
|
|
951
|
-
// Register
|
|
952
|
-
const
|
|
954
|
+
// Register the RAW source stream — this is what we can reuse on seek
|
|
955
|
+
const rawStreamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
953
956
|
source: track.source || "stream",
|
|
954
957
|
isPreload: false,
|
|
955
958
|
isRemote: !!streamInfo?.remote,
|
|
956
959
|
priority: 10,
|
|
957
960
|
});
|
|
958
|
-
//
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
961
|
+
// createResource now returns both the AudioResource
|
|
962
|
+
// AND the processedStream (ffmpeg stdout) when filters/seek are involved.
|
|
963
|
+
const { resource, processedStream } = await this.createResource(streamInfo, track, 0);
|
|
964
|
+
// when a processedStream exists, register it too so its
|
|
965
|
+
// lifecycle is tracked. Store its id separately in currentSlot so
|
|
966
|
+
// destroyCurrentStream() and refreshPlayerResource() clean the right object.
|
|
967
|
+
let playStreamId = rawStreamId;
|
|
968
|
+
if (processedStream && processedStream !== streamInfo.stream) {
|
|
969
|
+
playStreamId = this.streamManager.registerStream(processedStream, track, {
|
|
970
|
+
source: track.source || "stream-processed",
|
|
971
|
+
isPreload: false,
|
|
972
|
+
priority: 10,
|
|
973
|
+
});
|
|
974
|
+
this.debug(`[Player] Registered processedStream ${playStreamId} (rawStream: ${rawStreamId})`);
|
|
975
|
+
}
|
|
976
|
+
if (this.currentSlot.streamId && this.currentSlot.streamId !== rawStreamId) {
|
|
962
977
|
this.streamManager.unregisterStream(this.currentSlot.streamId, true);
|
|
963
978
|
}
|
|
964
|
-
|
|
979
|
+
if (this.currentSlot.processedStreamId && this.currentSlot.processedStreamId !== playStreamId) {
|
|
980
|
+
this.streamManager.unregisterStream(this.currentSlot.processedStreamId, true);
|
|
981
|
+
}
|
|
965
982
|
this.currentSlot.resource = resource;
|
|
966
983
|
this.currentSlot.track = track;
|
|
967
|
-
this.currentSlot.streamId =
|
|
984
|
+
this.currentSlot.streamId = rawStreamId;
|
|
985
|
+
this.currentSlot.processedStreamId = processedStream ? playStreamId : null;
|
|
968
986
|
this.currentSlot.isValid = true;
|
|
969
987
|
this.currentResource = resource;
|
|
970
|
-
this.seekOffset = 0;
|
|
971
|
-
// Apply volume
|
|
988
|
+
this.seekOffset = 0;
|
|
972
989
|
const targetVolume = this.getTrackTargetVolume(track);
|
|
973
990
|
if (resource.volume) {
|
|
974
991
|
resource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
975
992
|
}
|
|
976
|
-
// Play — lock refresh so Idle event doesn't spawn duplicate playNext
|
|
977
993
|
await this.maybeAlignToBeatBoundary();
|
|
978
994
|
this.refreshLock = true;
|
|
979
995
|
try {
|
|
@@ -985,7 +1001,6 @@ class Player extends events_1.EventEmitter {
|
|
|
985
1001
|
this.refreshLock = false;
|
|
986
1002
|
}
|
|
987
1003
|
await this.applyCrossfadeIn(resource, track);
|
|
988
|
-
// Preload next (async)
|
|
989
1004
|
if (!this.destroyed) {
|
|
990
1005
|
this.preloadNextTrack().catch((err) => {
|
|
991
1006
|
this.debug(`[Player] Preload error:`, err);
|
|
@@ -1153,7 +1168,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1153
1168
|
throw new Error(`No stream available for track: ${track.title}`);
|
|
1154
1169
|
}
|
|
1155
1170
|
ttsStream = streamInfo.stream;
|
|
1156
|
-
const resource = await this.createResource(streamInfo, track);
|
|
1171
|
+
const { resource, processedStream } = await this.createResource(streamInfo, track);
|
|
1157
1172
|
if (!resource) {
|
|
1158
1173
|
throw new Error(`No resource available for track: ${track.title}`);
|
|
1159
1174
|
}
|
|
@@ -1481,6 +1496,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1481
1496
|
this.debug("[Player] Cannot stop while subscribed to another player");
|
|
1482
1497
|
return false;
|
|
1483
1498
|
}
|
|
1499
|
+
this.recoveryInProgress = false;
|
|
1484
1500
|
this.debug(`[Player] stop called`);
|
|
1485
1501
|
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
1486
1502
|
this.cancelPreload();
|
|
@@ -1564,6 +1580,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1564
1580
|
return false;
|
|
1565
1581
|
}
|
|
1566
1582
|
this.debug(`[Player] skip called with index: ${index}`);
|
|
1583
|
+
this.recoveryInProgress = false;
|
|
1567
1584
|
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
1568
1585
|
if (typeof index === "number" && index >= 0) {
|
|
1569
1586
|
for (let i = 0; i < index; i++)
|
|
@@ -2134,10 +2151,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2134
2151
|
this.debug(`[Player] refreshPlayerResource skipped — lock held`);
|
|
2135
2152
|
return false;
|
|
2136
2153
|
}
|
|
2137
|
-
// Lock before anything so stateChange idle sees it when stop() fires.
|
|
2138
2154
|
this.refreshLock = true;
|
|
2139
|
-
// Clear any existing stuckTimer from the previous playback cycle so it
|
|
2140
|
-
// cannot fire while we are mid-refresh.
|
|
2141
2155
|
if (this.stuckTimer) {
|
|
2142
2156
|
clearTimeout(this.stuckTimer);
|
|
2143
2157
|
this.stuckTimer = null;
|
|
@@ -2149,11 +2163,9 @@ class Player extends events_1.EventEmitter {
|
|
|
2149
2163
|
this.seekOffset = currentPosition;
|
|
2150
2164
|
const wasPaused = this.isPaused;
|
|
2151
2165
|
const playbackDuration = this.currentResource?.playbackDuration ?? 0;
|
|
2152
|
-
// Reuse is only viable for forward seeks (stream is sequential).
|
|
2153
2166
|
const isForwardSeek = position < 0 || position >= playbackDuration;
|
|
2154
2167
|
const currentStreamId = this.currentSlot.streamId;
|
|
2155
|
-
// Try to grab the
|
|
2156
|
-
// getRawStream accepts "paused" streams (discordjs/voice pauses source streams on NoSubscriberBehavior); getStream would reject them.
|
|
2168
|
+
// Try to grab the raw source stream for reuse (forward seeks only)
|
|
2157
2169
|
let reuseStream = null;
|
|
2158
2170
|
if (isForwardSeek && currentStreamId) {
|
|
2159
2171
|
reuseStream = this.streamManager.getRawStream(currentStreamId);
|
|
@@ -2161,27 +2173,24 @@ class Player extends events_1.EventEmitter {
|
|
|
2161
2173
|
this.debug(`[Player] Will reuse source stream for seek (pos: ${currentPosition}ms)`);
|
|
2162
2174
|
}
|
|
2163
2175
|
}
|
|
2164
|
-
// ── CRITICAL: unpipe BEFORE stop ──────────────────────────────────────
|
|
2165
|
-
// stop() kills discordjs/voice internal FFmpeg → EPIPE on source stream.
|
|
2166
|
-
// unpipe() first disconnects our stream cleanly before that happens.
|
|
2167
2176
|
if (reuseStream) {
|
|
2168
2177
|
reuseStream.unpipe();
|
|
2169
2178
|
}
|
|
2170
|
-
//
|
|
2171
|
-
|
|
2179
|
+
// Clean up processedStream first (it's what AudioResource reads)
|
|
2180
|
+
const processedStreamId = this.currentSlot.processedStreamId;
|
|
2181
|
+
if (processedStreamId && processedStreamId !== currentStreamId) {
|
|
2182
|
+
this.streamManager.unregisterStream(processedStreamId, true);
|
|
2183
|
+
this.currentSlot.processedStreamId = null;
|
|
2184
|
+
}
|
|
2172
2185
|
if (currentStreamId) {
|
|
2173
2186
|
this.streamManager.unregisterStream(currentStreamId, !reuseStream);
|
|
2174
2187
|
this.currentSlot.streamId = null;
|
|
2175
2188
|
}
|
|
2176
|
-
// Stop the AudioPlayer.
|
|
2177
|
-
// stateChange (playing→idle) fires; refreshLock=true guards it (v4 fix).
|
|
2178
2189
|
this.audioPlayer.stop(true);
|
|
2179
2190
|
this.currentResource = null;
|
|
2180
2191
|
this.currentSlot.resource = null;
|
|
2181
2192
|
this.currentSlot.isValid = false;
|
|
2182
|
-
// One event-loop tick: lets deferred stream events settle.
|
|
2183
2193
|
await new Promise((resolve) => setImmediate(resolve));
|
|
2184
|
-
// Verify the reuse stream survived stop().
|
|
2185
2194
|
if (reuseStream) {
|
|
2186
2195
|
if (reuseStream.destroyed || reuseStream.readable === false) {
|
|
2187
2196
|
this.debug(`[Player] Source stream did not survive stop — falling back to fresh stream`);
|
|
@@ -2193,7 +2202,6 @@ class Player extends events_1.EventEmitter {
|
|
|
2193
2202
|
streaminfo = { stream: reuseStream, type: "arbitrary" };
|
|
2194
2203
|
}
|
|
2195
2204
|
else {
|
|
2196
|
-
// Clear caches so we don't get the dead Readable back.
|
|
2197
2205
|
this.pluginManager.clearStreamCache();
|
|
2198
2206
|
this.extensionManager.clearCache("stream");
|
|
2199
2207
|
this.debug(`[Player] Fetching fresh stream${!isForwardSeek ? " (backward seek)" : " (reuse failed)"}`);
|
|
@@ -2203,20 +2211,28 @@ class Player extends events_1.EventEmitter {
|
|
|
2203
2211
|
this.debug(`[Player] No stream available for refresh`);
|
|
2204
2212
|
return false;
|
|
2205
2213
|
}
|
|
2206
|
-
|
|
2207
|
-
const resource = await this.createResource(streaminfo, track,
|
|
2208
|
-
// Register
|
|
2214
|
+
const createPosition = reuseStream ? -1 : currentPosition;
|
|
2215
|
+
const { resource, processedStream } = await this.createResource(streaminfo, track, createPosition);
|
|
2216
|
+
// Register raw source stream
|
|
2209
2217
|
const newStreamId = this.streamManager.registerStream(streaminfo.stream, track, {
|
|
2210
2218
|
source: track.source || "stream",
|
|
2211
2219
|
isPreload: false,
|
|
2212
2220
|
priority: 10,
|
|
2213
2221
|
});
|
|
2222
|
+
let newProcessedStreamId = null;
|
|
2223
|
+
if (processedStream && processedStream !== streaminfo.stream) {
|
|
2224
|
+
newProcessedStreamId = this.streamManager.registerStream(processedStream, track, {
|
|
2225
|
+
source: track.source || "stream-processed",
|
|
2226
|
+
isPreload: false,
|
|
2227
|
+
priority: 10,
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2214
2230
|
this.currentSlot.resource = resource;
|
|
2215
2231
|
this.currentSlot.track = track;
|
|
2216
2232
|
this.currentSlot.streamId = newStreamId;
|
|
2233
|
+
this.currentSlot.processedStreamId = newProcessedStreamId;
|
|
2217
2234
|
this.currentSlot.isValid = true;
|
|
2218
2235
|
this.currentResource = resource;
|
|
2219
|
-
// ── Set seek flag BEFORE play so the Buffering handler sees it ────────
|
|
2220
2236
|
if (position >= 0) {
|
|
2221
2237
|
this.seekInProgress = true;
|
|
2222
2238
|
}
|
|
@@ -2231,12 +2247,12 @@ class Player extends events_1.EventEmitter {
|
|
|
2231
2247
|
}
|
|
2232
2248
|
catch (error) {
|
|
2233
2249
|
this.debug(`[Player] refreshPlayerResource error:`, error);
|
|
2234
|
-
this.seekInProgress = false;
|
|
2250
|
+
this.seekInProgress = false;
|
|
2235
2251
|
this.emit("playerError", error, this.queue.currentTrack ?? undefined);
|
|
2236
2252
|
return false;
|
|
2237
2253
|
}
|
|
2238
2254
|
finally {
|
|
2239
|
-
this.refreshLock = false;
|
|
2255
|
+
this.refreshLock = false;
|
|
2240
2256
|
}
|
|
2241
2257
|
}
|
|
2242
2258
|
/**
|
|
@@ -2293,6 +2309,10 @@ class Player extends events_1.EventEmitter {
|
|
|
2293
2309
|
this.debug(`[Player] AudioPlayer went idle during resource refresh — skipping trackEnd/playNext`);
|
|
2294
2310
|
return;
|
|
2295
2311
|
}
|
|
2312
|
+
if (this.recoveryInProgress) {
|
|
2313
|
+
this.debug(`[Player] AudioPlayer went idle during recovery — skipping playNext`);
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2296
2316
|
// Track ended
|
|
2297
2317
|
const track = this.queue.currentTrack;
|
|
2298
2318
|
if (track) {
|
|
@@ -2389,6 +2409,8 @@ class Player extends events_1.EventEmitter {
|
|
|
2389
2409
|
this.audioPlayer.on("error", (error) => {
|
|
2390
2410
|
if (this.destroyed)
|
|
2391
2411
|
return;
|
|
2412
|
+
if (this.recoveryInProgress)
|
|
2413
|
+
return;
|
|
2392
2414
|
this.debug(`[Player] AudioPlayer error:`, error);
|
|
2393
2415
|
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
2394
2416
|
const track = this.queue.currentTrack;
|