ziplayer 0.3.6 → 0.3.8
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 +59 -107
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.d.ts +9 -24
- package/dist/structures/FilterManager.d.ts.map +1 -1
- package/dist/structures/FilterManager.js +182 -93
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/Player.d.ts +8 -1
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +233 -133
- 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/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +4 -0
- package/dist/structures/Queue.js.map +1 -1
- package/dist/structures/StreamManager.d.ts +8 -0
- package/dist/structures/StreamManager.d.ts.map +1 -1
- package/dist/structures/StreamManager.js +23 -0
- 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 +70 -120
- package/src/structures/FilterManager.ts +439 -303
- package/src/structures/Player.ts +268 -140
- package/src/structures/PreloadManager.ts +293 -274
- package/src/structures/Queue.ts +5 -0
- package/src/structures/StreamManager.ts +585 -563
- package/src/types/index.ts +1 -0
|
@@ -64,6 +64,7 @@ class Player extends events_1.EventEmitter {
|
|
|
64
64
|
this.stuckTimer = null;
|
|
65
65
|
this.skipLoop = false;
|
|
66
66
|
this.refreshLock = false;
|
|
67
|
+
this.seekInProgress = false;
|
|
67
68
|
this.currentSlot = {
|
|
68
69
|
resource: null,
|
|
69
70
|
track: null,
|
|
@@ -110,6 +111,7 @@ class Player extends events_1.EventEmitter {
|
|
|
110
111
|
this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
111
112
|
this.ttsPlayer = null;
|
|
112
113
|
this.lastDuration = 0;
|
|
114
|
+
this.seekOffset = 0;
|
|
113
115
|
this.debug(`[Player] Constructor called for guildId: ${guildId}`);
|
|
114
116
|
this.guildId = guildId;
|
|
115
117
|
this.queue = new Queue_1.Queue();
|
|
@@ -191,7 +193,7 @@ class Player extends events_1.EventEmitter {
|
|
|
191
193
|
extractorTimeout: this.options.extractorTimeout,
|
|
192
194
|
});
|
|
193
195
|
this.streamManager = new StreamManager_1.StreamManager({
|
|
194
|
-
maxConcurrentStreams:
|
|
196
|
+
maxConcurrentStreams: 4,
|
|
195
197
|
streamTimeout: 5 * 60 * 1000,
|
|
196
198
|
maxListenersPerStream: 15,
|
|
197
199
|
enableMetrics: true,
|
|
@@ -200,7 +202,12 @@ class Player extends events_1.EventEmitter {
|
|
|
200
202
|
this.preloadManager = new PreloadManager_1.PreloadManager({
|
|
201
203
|
streamManager: this.streamManager,
|
|
202
204
|
debug: this.debug.bind(this),
|
|
203
|
-
getNextTrack: () =>
|
|
205
|
+
getNextTrack: () => {
|
|
206
|
+
if (this.queue.loop() === "track") {
|
|
207
|
+
return this.queue.currentTrack;
|
|
208
|
+
}
|
|
209
|
+
return this.queue.nextTrack;
|
|
210
|
+
},
|
|
204
211
|
getStream: (track) => this.getStream(track),
|
|
205
212
|
isDestroyed: () => this.destroyed,
|
|
206
213
|
isEnabled: () => this.preloadEnabled,
|
|
@@ -743,42 +750,27 @@ class Player extends events_1.EventEmitter {
|
|
|
743
750
|
*/
|
|
744
751
|
async createResource(streamInfo, track, position = 0) {
|
|
745
752
|
const filterString = this.filter.getFilterString();
|
|
746
|
-
this.debug(`[Player] Creating AudioResource
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
// Create AudioResource with better error handling
|
|
755
|
-
const resource = (0, voice_1.createAudioResource)(stream, {
|
|
753
|
+
this.debug(`[Player] Creating AudioResource — filters: ${filterString || "none"}, seek: ${position}ms`);
|
|
754
|
+
this.filter.setSourceStreamType(streamInfo.type);
|
|
755
|
+
const seekArg = position > 0 ? position : -1;
|
|
756
|
+
if (filterString || position > 0) {
|
|
757
|
+
const processedStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, seekArg);
|
|
758
|
+
// rawStream. Always use Arbitrary so discordjs/voice doesn't re-encode.
|
|
759
|
+
const resource = (0, voice_1.createAudioResource)(processedStream, {
|
|
756
760
|
metadata: track,
|
|
757
|
-
inputType:
|
|
758
|
-
: streamInfo.type === "ogg/opus" ? voice_1.StreamType.OggOpus
|
|
759
|
-
: voice_1.StreamType.Arbitrary,
|
|
761
|
+
inputType: voice_1.StreamType.Arbitrary,
|
|
760
762
|
inlineVolume: true,
|
|
761
763
|
});
|
|
762
|
-
return resource;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
: voice_1.StreamType.Arbitrary,
|
|
773
|
-
inlineVolume: true,
|
|
774
|
-
});
|
|
775
|
-
return resource;
|
|
776
|
-
}
|
|
777
|
-
catch (fallbackError) {
|
|
778
|
-
this.debug(`[Player] Fallback AudioResource creation failed:`, fallbackError);
|
|
779
|
-
throw fallbackError;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
764
|
+
return { resource, processedStream };
|
|
765
|
+
}
|
|
766
|
+
const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
767
|
+
metadata: track,
|
|
768
|
+
inputType: streamInfo.type === "webm/opus" ? voice_1.StreamType.WebmOpus
|
|
769
|
+
: streamInfo.type === "ogg/opus" ? voice_1.StreamType.OggOpus
|
|
770
|
+
: voice_1.StreamType.Arbitrary,
|
|
771
|
+
inlineVolume: true,
|
|
772
|
+
});
|
|
773
|
+
return { resource, processedStream: null };
|
|
782
774
|
}
|
|
783
775
|
mergeTrackPreserveRef(target, source) {
|
|
784
776
|
if (source === target)
|
|
@@ -879,7 +871,14 @@ class Player extends events_1.EventEmitter {
|
|
|
879
871
|
async startTrack(track) {
|
|
880
872
|
if (this.destroyed)
|
|
881
873
|
return false;
|
|
882
|
-
//
|
|
874
|
+
// Check preload BEFORE calling getStream so we never fetch a
|
|
875
|
+
// stream we're about to throw away. The original code called getStream()
|
|
876
|
+
// unconditionally at the top, then used the preload if available — leaking
|
|
877
|
+
// the just-fetched stream and running middleware twice.
|
|
878
|
+
if (this.preloadManager.hasValidPreload(track)) {
|
|
879
|
+
return await this.startFromPreload(track);
|
|
880
|
+
}
|
|
881
|
+
// Only fetch a stream when there is no usable preload.
|
|
883
882
|
let streamInfo = null;
|
|
884
883
|
try {
|
|
885
884
|
streamInfo = await this.getStream(track);
|
|
@@ -888,99 +887,114 @@ class Player extends events_1.EventEmitter {
|
|
|
888
887
|
this.debug(`[Player] Failed to get stream for track: ${track.title}`, error);
|
|
889
888
|
throw error;
|
|
890
889
|
}
|
|
891
|
-
//
|
|
890
|
+
// Remote playback
|
|
892
891
|
if (streamInfo?.remote && streamInfo.handle) {
|
|
893
892
|
return await this.playRemote(track, streamInfo);
|
|
894
893
|
}
|
|
895
|
-
//
|
|
894
|
+
// Native playback — pass the already-fetched streamInfo to avoid a second fetch
|
|
895
|
+
return await this.loadFreshStream(track, streamInfo);
|
|
896
|
+
}
|
|
897
|
+
async startFromPreload(track) {
|
|
898
|
+
if (this.destroyed)
|
|
899
|
+
return false;
|
|
900
|
+
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
901
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
902
|
+
this.promotePreloadToCurrent(track);
|
|
903
|
+
if (oldStreamId && oldStreamId !== this.currentSlot.streamId) {
|
|
904
|
+
this.streamManager.unregisterStream(oldStreamId, true);
|
|
905
|
+
this.debug(`[Player] Released old stream ${oldStreamId} after preload promotion`);
|
|
906
|
+
}
|
|
907
|
+
const currentResource = this.currentSlot.resource;
|
|
908
|
+
if (!currentResource)
|
|
909
|
+
return false;
|
|
910
|
+
this.seekOffset = 0;
|
|
911
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
912
|
+
if (currentResource.volume) {
|
|
913
|
+
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
914
|
+
}
|
|
915
|
+
await this.maybeAlignToBeatBoundary();
|
|
916
|
+
this.refreshLock = true;
|
|
896
917
|
try {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
this.audioPlayer.stop(true);
|
|
901
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
902
|
-
if (oldStreamId && this.streamManager) {
|
|
903
|
-
setTimeout(() => {
|
|
904
|
-
if (this.currentSlot.streamId === oldStreamId) {
|
|
905
|
-
this.streamManager.unregisterStream(oldStreamId, true);
|
|
906
|
-
}
|
|
907
|
-
}, 3000);
|
|
908
|
-
}
|
|
909
|
-
this.promotePreloadToCurrent(track);
|
|
910
|
-
const currentResource = this.currentSlot.resource;
|
|
911
|
-
if (!currentResource) {
|
|
912
|
-
return false;
|
|
913
|
-
}
|
|
914
|
-
const targetVolume = this.getTrackTargetVolume(track);
|
|
915
|
-
if (currentResource.volume) {
|
|
916
|
-
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
917
|
-
}
|
|
918
|
-
await this.maybeAlignToBeatBoundary();
|
|
919
|
-
this.audioPlayer.play(currentResource);
|
|
920
|
-
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
921
|
-
await this.applyCrossfadeIn(currentResource, track);
|
|
922
|
-
this.preloadNextTrack().catch((err) => {
|
|
923
|
-
this.debug(`[Player] Preload error:`, err);
|
|
924
|
-
});
|
|
925
|
-
return true;
|
|
926
|
-
}
|
|
927
|
-
this.debug(`[Player] No preload available, loading fresh: ${track.title}`);
|
|
928
|
-
return await this.loadFreshStream(track);
|
|
918
|
+
this.audioPlayer.stop(true);
|
|
919
|
+
this.audioPlayer.play(currentResource);
|
|
920
|
+
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
929
921
|
}
|
|
930
|
-
|
|
931
|
-
this.
|
|
932
|
-
this.emit("playerError", error, track);
|
|
933
|
-
return false;
|
|
922
|
+
finally {
|
|
923
|
+
this.refreshLock = false;
|
|
934
924
|
}
|
|
925
|
+
await this.applyCrossfadeIn(currentResource, track);
|
|
926
|
+
this.preloadNextTrack().catch((err) => {
|
|
927
|
+
this.debug(`[Player] Preload error:`, err);
|
|
928
|
+
});
|
|
929
|
+
return true;
|
|
935
930
|
}
|
|
936
931
|
/**
|
|
937
932
|
* Load fresh stream when no preload available
|
|
938
933
|
*/
|
|
939
|
-
async loadFreshStream(track) {
|
|
934
|
+
async loadFreshStream(track, preloadedStreamInfo) {
|
|
940
935
|
if (this.destroyed)
|
|
941
936
|
return false;
|
|
942
|
-
// Cancel preload to free resources
|
|
943
937
|
await this.safeCancelPreload();
|
|
944
938
|
try {
|
|
945
|
-
|
|
946
|
-
//
|
|
939
|
+
// use caller-supplied streamInfo when available so we don't
|
|
940
|
+
// call getStream() a second time and run middleware twice.
|
|
941
|
+
const streamInfo = preloadedStreamInfo ?? (await this.getStream(track));
|
|
947
942
|
if (streamInfo?.remote && streamInfo.handle) {
|
|
948
943
|
return await this.playRemote(track, streamInfo);
|
|
949
944
|
}
|
|
950
945
|
if (!streamInfo?.stream) {
|
|
951
946
|
throw new Error(`No stream available`);
|
|
952
947
|
}
|
|
953
|
-
// Register
|
|
954
|
-
const
|
|
948
|
+
// Register the RAW source stream — this is what we can reuse on seek
|
|
949
|
+
const rawStreamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
955
950
|
source: track.source || "stream",
|
|
956
951
|
isPreload: false,
|
|
957
952
|
isRemote: !!streamInfo?.remote,
|
|
958
953
|
priority: 10,
|
|
959
954
|
});
|
|
960
|
-
//
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
955
|
+
// createResource now returns both the AudioResource
|
|
956
|
+
// AND the processedStream (ffmpeg stdout) when filters/seek are involved.
|
|
957
|
+
const { resource, processedStream } = await this.createResource(streamInfo, track, 0);
|
|
958
|
+
// when a processedStream exists, register it too so its
|
|
959
|
+
// lifecycle is tracked. Store its id separately in currentSlot so
|
|
960
|
+
// destroyCurrentStream() and refreshPlayerResource() clean the right object.
|
|
961
|
+
let playStreamId = rawStreamId;
|
|
962
|
+
if (processedStream && processedStream !== streamInfo.stream) {
|
|
963
|
+
playStreamId = this.streamManager.registerStream(processedStream, track, {
|
|
964
|
+
source: track.source || "stream-processed",
|
|
965
|
+
isPreload: false,
|
|
966
|
+
priority: 10,
|
|
967
|
+
});
|
|
968
|
+
this.debug(`[Player] Registered processedStream ${playStreamId} (rawStream: ${rawStreamId})`);
|
|
969
|
+
}
|
|
970
|
+
if (this.currentSlot.streamId && this.currentSlot.streamId !== rawStreamId) {
|
|
964
971
|
this.streamManager.unregisterStream(this.currentSlot.streamId, true);
|
|
965
972
|
}
|
|
966
|
-
|
|
973
|
+
if (this.currentSlot.processedStreamId && this.currentSlot.processedStreamId !== playStreamId) {
|
|
974
|
+
this.streamManager.unregisterStream(this.currentSlot.processedStreamId, true);
|
|
975
|
+
}
|
|
967
976
|
this.currentSlot.resource = resource;
|
|
968
977
|
this.currentSlot.track = track;
|
|
969
|
-
this.currentSlot.streamId =
|
|
978
|
+
this.currentSlot.streamId = rawStreamId;
|
|
979
|
+
this.currentSlot.processedStreamId = processedStream ? playStreamId : null;
|
|
970
980
|
this.currentSlot.isValid = true;
|
|
971
981
|
this.currentResource = resource;
|
|
972
|
-
|
|
982
|
+
this.seekOffset = 0;
|
|
973
983
|
const targetVolume = this.getTrackTargetVolume(track);
|
|
974
984
|
if (resource.volume) {
|
|
975
985
|
resource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
976
986
|
}
|
|
977
|
-
// Play
|
|
978
987
|
await this.maybeAlignToBeatBoundary();
|
|
979
|
-
this.
|
|
980
|
-
|
|
981
|
-
|
|
988
|
+
this.refreshLock = true;
|
|
989
|
+
try {
|
|
990
|
+
this.audioPlayer.stop(true);
|
|
991
|
+
this.audioPlayer.play(resource);
|
|
992
|
+
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
993
|
+
}
|
|
994
|
+
finally {
|
|
995
|
+
this.refreshLock = false;
|
|
996
|
+
}
|
|
982
997
|
await this.applyCrossfadeIn(resource, track);
|
|
983
|
-
// Preload next (async)
|
|
984
998
|
if (!this.destroyed) {
|
|
985
999
|
this.preloadNextTrack().catch((err) => {
|
|
986
1000
|
this.debug(`[Player] Preload error:`, err);
|
|
@@ -1053,6 +1067,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1053
1067
|
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1054
1068
|
}
|
|
1055
1069
|
}
|
|
1070
|
+
else {
|
|
1071
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
1072
|
+
this.skipLoop = true;
|
|
1073
|
+
}
|
|
1056
1074
|
}
|
|
1057
1075
|
catch (err) {
|
|
1058
1076
|
this.debug(`[Player] playNext error:`, err);
|
|
@@ -1076,6 +1094,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1076
1094
|
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1077
1095
|
}
|
|
1078
1096
|
}
|
|
1097
|
+
else {
|
|
1098
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
1099
|
+
this.skipLoop = true;
|
|
1100
|
+
}
|
|
1079
1101
|
continue;
|
|
1080
1102
|
}
|
|
1081
1103
|
}
|
|
@@ -1140,7 +1162,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1140
1162
|
throw new Error(`No stream available for track: ${track.title}`);
|
|
1141
1163
|
}
|
|
1142
1164
|
ttsStream = streamInfo.stream;
|
|
1143
|
-
const resource = await this.createResource(streamInfo, track);
|
|
1165
|
+
const { resource, processedStream } = await this.createResource(streamInfo, track);
|
|
1144
1166
|
if (!resource) {
|
|
1145
1167
|
throw new Error(`No resource available for track: ${track.title}`);
|
|
1146
1168
|
}
|
|
@@ -1208,15 +1230,17 @@ class Player extends events_1.EventEmitter {
|
|
|
1208
1230
|
* @example
|
|
1209
1231
|
* await player.connect(voiceChannel);
|
|
1210
1232
|
*/
|
|
1211
|
-
async connect(channel) {
|
|
1233
|
+
async connect(channel, options) {
|
|
1212
1234
|
try {
|
|
1213
1235
|
this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
|
|
1214
1236
|
const connection = (0, voice_1.joinVoiceChannel)({
|
|
1237
|
+
...options,
|
|
1215
1238
|
channelId: channel.id,
|
|
1216
1239
|
guildId: channel.guildId,
|
|
1217
1240
|
adapterCreator: channel.guild.voiceAdapterCreator,
|
|
1218
|
-
selfDeaf: this.options
|
|
1219
|
-
selfMute: this.options
|
|
1241
|
+
selfDeaf: options?.selfDeaf ?? this.options?.selfDeaf ?? true,
|
|
1242
|
+
selfMute: options?.selfMute ?? this.options?.selfMute ?? false,
|
|
1243
|
+
group: options?.group ?? this.options?.group ?? "Ziplayer",
|
|
1220
1244
|
});
|
|
1221
1245
|
await (0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Ready, 50_000);
|
|
1222
1246
|
this.connection = connection;
|
|
@@ -1526,7 +1550,11 @@ class Player extends events_1.EventEmitter {
|
|
|
1526
1550
|
this.debug(`[Player] Invalid seek position: ${position}ms (track duration: ${totalDuration}ms)`);
|
|
1527
1551
|
return false;
|
|
1528
1552
|
}
|
|
1529
|
-
await this.refreshPlayerResource(true, position);
|
|
1553
|
+
const ok = await this.refreshPlayerResource(true, position);
|
|
1554
|
+
if (!ok) {
|
|
1555
|
+
this.debug(`[Player] Seek failed at position: ${position}ms`);
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1530
1558
|
return true;
|
|
1531
1559
|
}
|
|
1532
1560
|
/**
|
|
@@ -1697,9 +1725,9 @@ class Player extends events_1.EventEmitter {
|
|
|
1697
1725
|
* console.log(`Loop mode: ${loopMode}`);
|
|
1698
1726
|
*/
|
|
1699
1727
|
loop(mode) {
|
|
1700
|
-
this.debug(`[Player] loop called with mode: ${mode}`);
|
|
1701
1728
|
if (typeof mode === "number") {
|
|
1702
1729
|
// Number mode: convert to text mode
|
|
1730
|
+
this.debug(`[Player] loop called with mode: ${mode}`);
|
|
1703
1731
|
switch (mode) {
|
|
1704
1732
|
case 0:
|
|
1705
1733
|
return this.queue.loop("off");
|
|
@@ -1909,8 +1937,8 @@ class Player extends events_1.EventEmitter {
|
|
|
1909
1937
|
}
|
|
1910
1938
|
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
1911
1939
|
if (!total)
|
|
1912
|
-
return this.formatTimeCompact(resource.playbackDuration);
|
|
1913
|
-
const current = resource.playbackDuration;
|
|
1940
|
+
return this.formatTimeCompact(resource.playbackDuration + this.seekOffset);
|
|
1941
|
+
const current = resource.playbackDuration + this.seekOffset;
|
|
1914
1942
|
const ratio = Math.min(Math.max(current / total, 0), 1);
|
|
1915
1943
|
const progress = Math.round(ratio * size);
|
|
1916
1944
|
// Build progress bar
|
|
@@ -2012,7 +2040,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2012
2040
|
};
|
|
2013
2041
|
}
|
|
2014
2042
|
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
2015
|
-
const current = resource.playbackDuration;
|
|
2043
|
+
const current = resource.playbackDuration + this.seekOffset;
|
|
2016
2044
|
return {
|
|
2017
2045
|
current: current,
|
|
2018
2046
|
total: total,
|
|
@@ -2111,57 +2139,112 @@ class Player extends events_1.EventEmitter {
|
|
|
2111
2139
|
if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
|
|
2112
2140
|
return false;
|
|
2113
2141
|
}
|
|
2114
|
-
if (this.refreshLock)
|
|
2142
|
+
if (this.refreshLock) {
|
|
2143
|
+
this.debug(`[Player] refreshPlayerResource skipped — lock held`);
|
|
2115
2144
|
return false;
|
|
2145
|
+
}
|
|
2116
2146
|
this.refreshLock = true;
|
|
2147
|
+
if (this.stuckTimer) {
|
|
2148
|
+
clearTimeout(this.stuckTimer);
|
|
2149
|
+
this.stuckTimer = null;
|
|
2150
|
+
}
|
|
2117
2151
|
try {
|
|
2118
2152
|
const track = this.queue.currentTrack;
|
|
2119
2153
|
this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
const streaminfo = await this.getStream(track);
|
|
2123
|
-
if (!streaminfo?.stream) {
|
|
2124
|
-
this.debug(`[Player] No stream to refresh`);
|
|
2125
|
-
return false;
|
|
2126
|
-
}
|
|
2127
|
-
// Create AudioResource with filters and seek to current position
|
|
2128
|
-
const resource = await this.createResource(streaminfo, track, currentPosition);
|
|
2129
|
-
// Stop current playback and destroy old resource/stream
|
|
2130
|
-
const wasPlaying = this.isPlaying;
|
|
2154
|
+
const currentPosition = position >= 0 ? position : (this.currentResource?.playbackDuration ?? 0);
|
|
2155
|
+
this.seekOffset = currentPosition;
|
|
2131
2156
|
const wasPaused = this.isPaused;
|
|
2132
|
-
this.
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2157
|
+
const playbackDuration = this.currentResource?.playbackDuration ?? 0;
|
|
2158
|
+
const isForwardSeek = position < 0 || position >= playbackDuration;
|
|
2159
|
+
const currentStreamId = this.currentSlot.streamId;
|
|
2160
|
+
// Try to grab the raw source stream for reuse (forward seeks only)
|
|
2161
|
+
let reuseStream = null;
|
|
2162
|
+
if (isForwardSeek && currentStreamId) {
|
|
2163
|
+
reuseStream = this.streamManager.getRawStream(currentStreamId);
|
|
2164
|
+
if (reuseStream) {
|
|
2165
|
+
this.debug(`[Player] Will reuse source stream for seek (pos: ${currentPosition}ms)`);
|
|
2140
2166
|
}
|
|
2141
2167
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2168
|
+
if (reuseStream) {
|
|
2169
|
+
reuseStream.unpipe();
|
|
2144
2170
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2171
|
+
// Clean up processedStream first (it's what AudioResource reads)
|
|
2172
|
+
const processedStreamId = this.currentSlot.processedStreamId;
|
|
2173
|
+
if (processedStreamId && processedStreamId !== currentStreamId) {
|
|
2174
|
+
this.streamManager.unregisterStream(processedStreamId, true);
|
|
2175
|
+
this.currentSlot.processedStreamId = null;
|
|
2176
|
+
}
|
|
2177
|
+
if (currentStreamId) {
|
|
2178
|
+
this.streamManager.unregisterStream(currentStreamId, !reuseStream);
|
|
2179
|
+
this.currentSlot.streamId = null;
|
|
2180
|
+
}
|
|
2181
|
+
this.audioPlayer.stop(true);
|
|
2182
|
+
this.currentResource = null;
|
|
2183
|
+
this.currentSlot.resource = null;
|
|
2184
|
+
this.currentSlot.isValid = false;
|
|
2185
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2186
|
+
if (reuseStream) {
|
|
2187
|
+
if (reuseStream.destroyed || reuseStream.readable === false) {
|
|
2188
|
+
this.debug(`[Player] Source stream did not survive stop — falling back to fresh stream`);
|
|
2189
|
+
reuseStream = null;
|
|
2190
|
+
}
|
|
2147
2191
|
}
|
|
2192
|
+
let streaminfo = null;
|
|
2193
|
+
if (reuseStream) {
|
|
2194
|
+
streaminfo = { stream: reuseStream, type: "arbitrary" };
|
|
2195
|
+
}
|
|
2196
|
+
else {
|
|
2197
|
+
this.pluginManager.clearStreamCache();
|
|
2198
|
+
this.extensionManager.clearCache("stream");
|
|
2199
|
+
this.debug(`[Player] Fetching fresh stream${!isForwardSeek ? " (backward seek)" : " (reuse failed)"}`);
|
|
2200
|
+
streaminfo = await this.getStream(track);
|
|
2201
|
+
}
|
|
2202
|
+
if (!streaminfo?.stream) {
|
|
2203
|
+
this.debug(`[Player] No stream available for refresh`);
|
|
2204
|
+
return false;
|
|
2205
|
+
}
|
|
2206
|
+
const createPosition = reuseStream ? -1 : currentPosition;
|
|
2207
|
+
const { resource, processedStream } = await this.createResource(streaminfo, track, createPosition);
|
|
2208
|
+
// Register raw source stream
|
|
2209
|
+
const newStreamId = this.streamManager.registerStream(streaminfo.stream, track, {
|
|
2210
|
+
source: track.source || "stream",
|
|
2211
|
+
isPreload: false,
|
|
2212
|
+
priority: 10,
|
|
2213
|
+
});
|
|
2214
|
+
let newProcessedStreamId = null;
|
|
2215
|
+
if (processedStream && processedStream !== streaminfo.stream) {
|
|
2216
|
+
newProcessedStreamId = this.streamManager.registerStream(processedStream, track, {
|
|
2217
|
+
source: track.source || "stream-processed",
|
|
2218
|
+
isPreload: false,
|
|
2219
|
+
priority: 10,
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
this.currentSlot.resource = resource;
|
|
2223
|
+
this.currentSlot.track = track;
|
|
2224
|
+
this.currentSlot.streamId = newStreamId;
|
|
2225
|
+
this.currentSlot.processedStreamId = newProcessedStreamId;
|
|
2226
|
+
this.currentSlot.isValid = true;
|
|
2148
2227
|
this.currentResource = resource;
|
|
2149
|
-
|
|
2228
|
+
if (position >= 0) {
|
|
2229
|
+
this.seekInProgress = true;
|
|
2230
|
+
}
|
|
2150
2231
|
if (this.connection) {
|
|
2151
2232
|
this.connection.subscribe(this.audioPlayer);
|
|
2152
2233
|
this.audioPlayer.play(resource);
|
|
2153
2234
|
}
|
|
2154
|
-
|
|
2155
|
-
if (wasPaused) {
|
|
2235
|
+
if (wasPaused)
|
|
2156
2236
|
this.audioPlayer.pause();
|
|
2157
|
-
}
|
|
2158
|
-
this.debug(`[Player] Successfully applied filter to current track at position ${currentPosition}ms`);
|
|
2237
|
+
this.debug(`[Player] Resource refreshed at position ${currentPosition}ms`);
|
|
2159
2238
|
return true;
|
|
2160
2239
|
}
|
|
2161
2240
|
catch (error) {
|
|
2162
|
-
this.debug(`[Player]
|
|
2163
|
-
|
|
2164
|
-
|
|
2241
|
+
this.debug(`[Player] refreshPlayerResource error:`, error);
|
|
2242
|
+
this.seekInProgress = false;
|
|
2243
|
+
this.emit("playerError", error, this.queue.currentTrack ?? undefined);
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
finally {
|
|
2247
|
+
this.refreshLock = false;
|
|
2165
2248
|
}
|
|
2166
2249
|
}
|
|
2167
2250
|
/**
|
|
@@ -2212,7 +2295,12 @@ class Player extends events_1.EventEmitter {
|
|
|
2212
2295
|
if (this.destroyed)
|
|
2213
2296
|
return;
|
|
2214
2297
|
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
2298
|
+
// ── Idle: track ended naturally ───────────────────────────────────────
|
|
2215
2299
|
if (newState.status === voice_1.AudioPlayerStatus.Idle && oldState.status !== voice_1.AudioPlayerStatus.Idle) {
|
|
2300
|
+
if (this.refreshLock) {
|
|
2301
|
+
this.debug(`[Player] AudioPlayer went idle during resource refresh — skipping trackEnd/playNext`);
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2216
2304
|
// Track ended
|
|
2217
2305
|
const track = this.queue.currentTrack;
|
|
2218
2306
|
if (track) {
|
|
@@ -2223,11 +2311,16 @@ class Player extends events_1.EventEmitter {
|
|
|
2223
2311
|
}
|
|
2224
2312
|
}
|
|
2225
2313
|
void this.playNext();
|
|
2314
|
+
// ── Playing: started or resumed ───────────────────────────────────────
|
|
2226
2315
|
}
|
|
2227
2316
|
else if (newState.status === voice_1.AudioPlayerStatus.Playing &&
|
|
2228
2317
|
(oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) {
|
|
2229
2318
|
// Track started
|
|
2230
2319
|
this.clearLeaveTimeout();
|
|
2320
|
+
if (this.seekInProgress) {
|
|
2321
|
+
this.debug(`[Player] Seek complete — audio output started`);
|
|
2322
|
+
this.seekInProgress = false;
|
|
2323
|
+
}
|
|
2231
2324
|
const track = this.queue.currentTrack;
|
|
2232
2325
|
if (track) {
|
|
2233
2326
|
this.debug(`[Player] Track started: ${track.title}`);
|
|
@@ -2244,6 +2337,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2244
2337
|
}
|
|
2245
2338
|
}
|
|
2246
2339
|
}
|
|
2340
|
+
// ── Paused ────────────────────────────────────────────────────────────
|
|
2247
2341
|
}
|
|
2248
2342
|
else if (newState.status === voice_1.AudioPlayerStatus.Paused && oldState.status !== voice_1.AudioPlayerStatus.Paused) {
|
|
2249
2343
|
const track = this.queue.currentTrack;
|
|
@@ -2254,6 +2348,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2254
2348
|
fp.emit("playerPause", track);
|
|
2255
2349
|
}
|
|
2256
2350
|
}
|
|
2351
|
+
// ── Resumed from pause ────────────────────────────────────────────────
|
|
2257
2352
|
}
|
|
2258
2353
|
else if (newState.status !== voice_1.AudioPlayerStatus.Paused && oldState.status === voice_1.AudioPlayerStatus.Paused) {
|
|
2259
2354
|
const track = this.queue.currentTrack;
|
|
@@ -2267,9 +2362,14 @@ class Player extends events_1.EventEmitter {
|
|
|
2267
2362
|
}
|
|
2268
2363
|
else if (newState.status === voice_1.AudioPlayerStatus.AutoPaused) {
|
|
2269
2364
|
this.debug(`[Player] AudioPlayerStatus.AutoPaused`);
|
|
2365
|
+
// ── Buffering: start stuck detector ───────────────────────────────────
|
|
2270
2366
|
}
|
|
2271
2367
|
else if (newState.status === voice_1.AudioPlayerStatus.Buffering) {
|
|
2272
2368
|
this.debug(`[Player] AudioPlayerStatus.Buffering`);
|
|
2369
|
+
if (this.seekInProgress) {
|
|
2370
|
+
this.debug(`[Player] Buffering during seek — stuckTimer suppressed`);
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2273
2373
|
this.lastDuration = this.currentResource?.playbackDuration || 0;
|
|
2274
2374
|
this.stuckTimer = setTimeout(() => {
|
|
2275
2375
|
if (this.currentResource?.playbackDuration === this.lastDuration) {
|