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.
Files changed (34) hide show
  1. package/dist/plugins/index.d.ts +1 -8
  2. package/dist/plugins/index.d.ts.map +1 -1
  3. package/dist/plugins/index.js +59 -107
  4. package/dist/plugins/index.js.map +1 -1
  5. package/dist/structures/FilterManager.d.ts +9 -24
  6. package/dist/structures/FilterManager.d.ts.map +1 -1
  7. package/dist/structures/FilterManager.js +182 -93
  8. package/dist/structures/FilterManager.js.map +1 -1
  9. package/dist/structures/Player.d.ts +8 -1
  10. package/dist/structures/Player.d.ts.map +1 -1
  11. package/dist/structures/Player.js +233 -133
  12. package/dist/structures/Player.js.map +1 -1
  13. package/dist/structures/PreloadManager.d.ts +1 -0
  14. package/dist/structures/PreloadManager.d.ts.map +1 -1
  15. package/dist/structures/PreloadManager.js +26 -6
  16. package/dist/structures/PreloadManager.js.map +1 -1
  17. package/dist/structures/Queue.d.ts.map +1 -1
  18. package/dist/structures/Queue.js +4 -0
  19. package/dist/structures/Queue.js.map +1 -1
  20. package/dist/structures/StreamManager.d.ts +8 -0
  21. package/dist/structures/StreamManager.d.ts.map +1 -1
  22. package/dist/structures/StreamManager.js +23 -0
  23. package/dist/structures/StreamManager.js.map +1 -1
  24. package/dist/types/index.d.ts +1 -0
  25. package/dist/types/index.d.ts.map +1 -1
  26. package/dist/types/index.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/plugins/index.ts +70 -120
  29. package/src/structures/FilterManager.ts +439 -303
  30. package/src/structures/Player.ts +268 -140
  31. package/src/structures/PreloadManager.ts +293 -274
  32. package/src/structures/Queue.ts +5 -0
  33. package/src/structures/StreamManager.ts +585 -563
  34. 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: 2,
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: () => this.queue.nextTrack,
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 with filters: ${filterString || "none"}, seek: ${position}ms`);
747
- try {
748
- let stream = streamInfo.stream;
749
- // Apply filters and seek if needed
750
- if (filterString || position > 0) {
751
- stream = await this.filter.applyFiltersAndSeek(streamInfo.stream, position);
752
- streamInfo.type = voice_1.StreamType.Arbitrary;
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: streamInfo.type === "webm/opus" ? voice_1.StreamType.WebmOpus
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
- catch (error) {
765
- this.debug(`[Player] Error creating AudioResource with filters+seek:`, error);
766
- // Fallback to basic AudioResource
767
- try {
768
- const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
769
- metadata: track,
770
- inputType: streamInfo.type === "webm/opus" ? voice_1.StreamType.WebmOpus
771
- : streamInfo.type === "ogg/opus" ? voice_1.StreamType.OggOpus
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
- // First, get stream info (this will handle remote detection)
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
- // Handle remote playback
890
+ // Remote playback
892
891
  if (streamInfo?.remote && streamInfo.handle) {
893
892
  return await this.playRemote(track, streamInfo);
894
893
  }
895
- // Handle native playback
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
- // Try to use preloaded resource
898
- if (this.preloadManager.hasValidPreload(track)) {
899
- this.debug(`[Player] Using preloaded stream for: ${track.title}`);
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
- catch (error) {
931
- this.debug(`[Player] startTrack error:`, error);
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
- const streamInfo = await this.getStream(track);
946
- // Handle remote playback
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 with StreamManager
954
- const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
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
- // Create resource
961
- const resource = await this.createResource(streamInfo, track, 0);
962
- // Clean up old current
963
- if (this.currentSlot.streamId && this.currentSlot.streamId !== streamId) {
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
- // Set current slot
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 = 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
- // Apply volume
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.audioPlayer.stop(true);
980
- this.audioPlayer.play(resource);
981
- await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
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.selfDeaf ?? true,
1219
- selfMute: this.options.selfMute ?? false,
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
- // Get current position for seeking
2121
- const currentPosition = position > 0 ? position : this.currentResource?.playbackDuration || 0;
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.audioPlayer.stop();
2133
- // Properly destroy the old resource and stream
2134
- try {
2135
- if (this.currentResource) {
2136
- const oldStream = this.currentResource._readableState?.stream || this.currentResource.stream;
2137
- if (oldStream && typeof oldStream.destroy === "function") {
2138
- oldStream.destroy();
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
- catch (error) {
2143
- this.debug(`[Player] Error destroying old stream in refreshPlayerResource:`, error);
2168
+ if (reuseStream) {
2169
+ reuseStream.unpipe();
2144
2170
  }
2145
- finally {
2146
- this.refreshLock = false;
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
- // Subscribe to new resource
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
- // Restore playing state
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] Error applying filter to current track:`, error);
2163
- // Filter was still added to active filters, so return true
2164
- return true;
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) {