ziplayer 0.3.8 → 0.3.10

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.
@@ -48,70 +48,86 @@ const PreloadManager_1 = require("./PreloadManager");
48
48
  *
49
49
  */
50
50
  class Player extends events_1.EventEmitter {
51
+ guildId;
52
+ connection = null;
53
+ audioPlayer;
54
+ queue;
55
+ volume = 100;
56
+ options;
57
+ userdata;
58
+ _lastActivity = Date.now();
59
+ _remotePaused = false;
60
+ currentResource = null;
61
+ destroyed = false;
62
+ manager;
63
+ pluginManager;
64
+ extensionManager;
65
+ streamManager;
66
+ preloadManager;
67
+ filter;
68
+ playbackMode = types_1.PlaybackMode.NATIVE;
69
+ forwardFollowers = new Set();
70
+ forwardLeader = null;
71
+ leaveTimeout = null;
72
+ volumeInterval = null;
73
+ stuckTimer = null;
74
+ skipLoop = false;
75
+ refreshLock = false;
76
+ seekInProgress = false;
77
+ remoteHandle;
78
+ currentSlot = {
79
+ resource: null,
80
+ track: null,
81
+ streamId: null,
82
+ abortController: null,
83
+ isValid: false,
84
+ isLoading: false,
85
+ loadPromise: null,
86
+ };
87
+ preloadEnabled = true;
88
+ crossfadeEnabled = true;
89
+ crossfadeDurationMs = 500;
90
+ lowPerformanceMode = false;
91
+ crossfadeTransitionLock = false;
92
+ smartTransitionEnabled = true;
93
+ smartTransitionGenreAware = true;
94
+ smartTransitionBeatAlign = true;
95
+ smartTransitionBaseMs = 800;
96
+ smartTransitionMinMs = 120;
97
+ smartTransitionMaxMs = 8000;
98
+ smartTransitionGenreDurations = {
99
+ chill: 700,
100
+ ambient: 750,
101
+ lofi: 650,
102
+ pop: 450,
103
+ rock: 350,
104
+ edm: 220,
105
+ house: 250,
106
+ techno: 200,
107
+ };
108
+ smartTransitionBeatAlignMaxWaitMs = 180;
109
+ antiStuckEnabled = true;
110
+ antiStuckMaxRetries = 2;
111
+ antiStuckRetryDelayMs = 900;
112
+ antiStuckReusePreloadFirst = true;
113
+ antiStuckReduceQualityOnRetry = true;
114
+ antiStuckControlledSkipThreshold = 3;
115
+ antiStuckConsecutiveFailures = 0;
116
+ loudnessNormalizationEnabled = false;
117
+ loudnessTargetLUFS = -14;
118
+ loudnessMaxBoostDb = 8;
119
+ loudnessMaxCutDb = 10;
120
+ loudnessLimiterCeiling = 0.95;
121
+ trackMiddlewareChain;
122
+ // Cache for search results to avoid duplicate calls
123
+ searchCache;
124
+ SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
125
+ ttsPlayer = null;
126
+ lastDuration = 0;
127
+ seekOffset = 0;
128
+ recoveryInProgress = false;
51
129
  constructor(guildId, options = {}, manager) {
52
130
  super();
53
- this.connection = null;
54
- this.volume = 100;
55
- this._lastActivity = Date.now();
56
- this._remotePaused = false;
57
- this.currentResource = null;
58
- this.destroyed = false;
59
- this.playbackMode = types_1.PlaybackMode.NATIVE;
60
- this.forwardFollowers = new Set();
61
- this.forwardLeader = null;
62
- this.leaveTimeout = null;
63
- this.volumeInterval = null;
64
- this.stuckTimer = null;
65
- this.skipLoop = false;
66
- this.refreshLock = false;
67
- this.seekInProgress = false;
68
- this.currentSlot = {
69
- resource: null,
70
- track: null,
71
- streamId: null,
72
- abortController: null,
73
- isValid: false,
74
- isLoading: false,
75
- loadPromise: null,
76
- };
77
- this.preloadEnabled = true;
78
- this.crossfadeEnabled = true;
79
- this.crossfadeDurationMs = 500;
80
- this.lowPerformanceMode = false;
81
- this.crossfadeTransitionLock = false;
82
- this.smartTransitionEnabled = true;
83
- this.smartTransitionGenreAware = true;
84
- this.smartTransitionBeatAlign = true;
85
- this.smartTransitionBaseMs = 800;
86
- this.smartTransitionMinMs = 120;
87
- this.smartTransitionMaxMs = 8000;
88
- this.smartTransitionGenreDurations = {
89
- chill: 700,
90
- ambient: 750,
91
- lofi: 650,
92
- pop: 450,
93
- rock: 350,
94
- edm: 220,
95
- house: 250,
96
- techno: 200,
97
- };
98
- this.smartTransitionBeatAlignMaxWaitMs = 180;
99
- this.antiStuckEnabled = true;
100
- this.antiStuckMaxRetries = 2;
101
- this.antiStuckRetryDelayMs = 900;
102
- this.antiStuckReusePreloadFirst = true;
103
- this.antiStuckReduceQualityOnRetry = true;
104
- this.antiStuckControlledSkipThreshold = 3;
105
- this.antiStuckConsecutiveFailures = 0;
106
- this.loudnessNormalizationEnabled = false;
107
- this.loudnessTargetLUFS = -14;
108
- this.loudnessMaxBoostDb = 8;
109
- this.loudnessMaxCutDb = 10;
110
- this.loudnessLimiterCeiling = 0.95;
111
- this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
112
- this.ttsPlayer = null;
113
- this.lastDuration = 0;
114
- this.seekOffset = 0;
115
131
  this.debug(`[Player] Constructor called for guildId: ${guildId}`);
116
132
  this.guildId = guildId;
117
133
  this.queue = new Queue_1.Queue();
@@ -193,7 +209,7 @@ class Player extends events_1.EventEmitter {
193
209
  extractorTimeout: this.options.extractorTimeout,
194
210
  });
195
211
  this.streamManager = new StreamManager_1.StreamManager({
196
- maxConcurrentStreams: 4,
212
+ maxConcurrentStreams: this.options?.maxStreamStore ?? 4,
197
213
  streamTimeout: 5 * 60 * 1000,
198
214
  maxListenersPerStream: 15,
199
215
  enableMetrics: true,
@@ -659,6 +675,7 @@ class Player extends events_1.EventEmitter {
659
675
  async attemptTrackRecovery(track, reason) {
660
676
  if (!this.antiStuckEnabled)
661
677
  return false;
678
+ this.recoveryInProgress = true;
662
679
  this.debug(`[AntiStuck] Recovery started for: ${track.title}`, reason);
663
680
  const originalQuality = this.options.quality;
664
681
  let attempted = 0;
@@ -676,6 +693,7 @@ class Player extends events_1.EventEmitter {
676
693
  if (startedFromPreload) {
677
694
  this.antiStuckConsecutiveFailures = 0;
678
695
  this.options.quality = originalQuality;
696
+ this.recoveryInProgress = false;
679
697
  return true;
680
698
  }
681
699
  }
@@ -683,6 +701,7 @@ class Player extends events_1.EventEmitter {
683
701
  if (started) {
684
702
  this.antiStuckConsecutiveFailures = 0;
685
703
  this.options.quality = originalQuality;
704
+ this.recoveryInProgress = false;
686
705
  return true;
687
706
  }
688
707
  }
@@ -694,8 +713,10 @@ class Player extends events_1.EventEmitter {
694
713
  this.antiStuckConsecutiveFailures++;
695
714
  if (this.antiStuckConsecutiveFailures >= this.antiStuckControlledSkipThreshold) {
696
715
  this.debug(`[AntiStuck] Controlled skip threshold reached for ${track.title}`);
716
+ this.recoveryInProgress = false;
697
717
  return false;
698
718
  }
719
+ this.recoveryInProgress = false;
699
720
  // Avoid hard skip storm by leaving track for next natural retry window.
700
721
  this.debug(`[AntiStuck] Keeping track for controlled retry window: ${track.title}`);
701
722
  return false;
@@ -754,14 +775,15 @@ class Player extends events_1.EventEmitter {
754
775
  this.filter.setSourceStreamType(streamInfo.type);
755
776
  const seekArg = position > 0 ? position : -1;
756
777
  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, {
778
+ const processedStream = await this.filter.applyFiltersAndSeek(streamInfo, seekArg);
779
+ const resource = (0, voice_1.createAudioResource)(processedStream.stream, {
760
780
  metadata: track,
761
- inputType: voice_1.StreamType.Arbitrary,
781
+ inputType: processedStream.wasRecreated && !filterString ? voice_1.StreamType.Arbitrary
782
+ : position > 0 ? voice_1.StreamType.Raw
783
+ : voice_1.StreamType.Arbitrary,
762
784
  inlineVolume: true,
763
785
  });
764
- return { resource, processedStream };
786
+ return { resource, processedStream: processedStream.stream };
765
787
  }
766
788
  const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
767
789
  metadata: track,
@@ -907,6 +929,7 @@ class Player extends events_1.EventEmitter {
907
929
  const currentResource = this.currentSlot.resource;
908
930
  if (!currentResource)
909
931
  return false;
932
+ // Ensure seekOffset is always an integer (milliseconds)
910
933
  this.seekOffset = 0;
911
934
  const targetVolume = this.getTrackTargetVolume(track);
912
935
  if (currentResource.volume) {
@@ -1490,6 +1513,7 @@ class Player extends events_1.EventEmitter {
1490
1513
  this.debug("[Player] Cannot stop while subscribed to another player");
1491
1514
  return false;
1492
1515
  }
1516
+ this.recoveryInProgress = false;
1493
1517
  this.debug(`[Player] stop called`);
1494
1518
  if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
1495
1519
  this.cancelPreload();
@@ -1573,6 +1597,7 @@ class Player extends events_1.EventEmitter {
1573
1597
  return false;
1574
1598
  }
1575
1599
  this.debug(`[Player] skip called with index: ${index}`);
1600
+ this.recoveryInProgress = false;
1576
1601
  if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
1577
1602
  if (typeof index === "number" && index >= 0) {
1578
1603
  for (let i = 0; i < index; i++)
@@ -1686,7 +1711,7 @@ class Player extends events_1.EventEmitter {
1686
1711
  this.debug(`[Player] Save options - filename: ${saveOptions.filename}, quality: ${saveOptions.quality || "default"}`);
1687
1712
  }
1688
1713
  // Apply filters if any are active
1689
- let finalStream = streamInfo.stream;
1714
+ let finalStream = streamInfo;
1690
1715
  if (saveOptions.filter || saveOptions.seek) {
1691
1716
  try {
1692
1717
  this.filter.clearAll();
@@ -1696,13 +1721,13 @@ class Player extends events_1.EventEmitter {
1696
1721
  this.debug(`[Player] Error applying save filters:`, err);
1697
1722
  }
1698
1723
  this.debug(`[Player] Applying filters to save stream: ${this.filter.getFilterString() || "none"}`);
1699
- finalStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, saveOptions.seek || 0).catch((err) => {
1724
+ finalStream = await this.filter.applyFiltersAndSeek(streamInfo, saveOptions.seek || 0).catch((err) => {
1700
1725
  this.debug(`[Player] Error applying filters to save stream:`, err);
1701
- return streamInfo.stream; // Fallback to original stream
1726
+ return streamInfo; // Fallback to original stream
1702
1727
  });
1703
1728
  }
1704
1729
  // Return the stream directly - caller can pipe it to fs.createWriteStream()
1705
- return finalStream;
1730
+ return finalStream.stream;
1706
1731
  }
1707
1732
  catch (error) {
1708
1733
  this.debug(`[Player] save error:`, error);
@@ -1980,27 +2005,33 @@ class Player extends events_1.EventEmitter {
1980
2005
  * @returns Formatted time string with leading zeros
1981
2006
  */
1982
2007
  formatTime(ms) {
1983
- const totalSeconds = Math.floor(ms / 1000);
1984
- const hours = Math.floor(totalSeconds / 3600);
1985
- const minutes = Math.floor((totalSeconds % 3600) / 60);
1986
- const seconds = totalSeconds % 60;
2008
+ // Ensure ms is an integer and convert to seconds
2009
+ const totalSeconds = Math.floor(ms / 1000) | 0;
2010
+ const hours = Math.floor(totalSeconds / 3600) | 0;
2011
+ const minutes = Math.floor((totalSeconds % 3600) / 60) | 0;
2012
+ const seconds = (totalSeconds % 60) | 0;
1987
2013
  const parts = [];
1988
- if (hours > 0)
1989
- parts.push(String(hours).padStart(2, "0"));
1990
- parts.push(String(minutes).padStart(2, "0"));
2014
+ if (hours > 0) {
2015
+ parts.push(String(hours)); // Giờ không padStart ( dụ: 1:05:00)
2016
+ parts.push(String(minutes).padStart(2, "0"));
2017
+ }
2018
+ else {
2019
+ parts.push(String(minutes)); // Phút không padStart nếu là số đầu tiên (ví dụ: 0:30 thay vì 00:30)
2020
+ }
1991
2021
  parts.push(String(seconds).padStart(2, "0"));
1992
2022
  return parts.join(":");
1993
2023
  }
1994
2024
  /**
1995
2025
  * Format time without leading zeros for hours (1:22:12 or 3:45)
1996
- * @param ms - Time in milliseconds
2026
+ * @param ms - Time in milliseconds (must be integer)
1997
2027
  * @returns Compact formatted time string
1998
2028
  */
1999
2029
  formatTimeCompact(ms) {
2000
- const totalSeconds = Math.floor(ms / 1000);
2001
- const hours = Math.floor(totalSeconds / 3600);
2002
- const minutes = Math.floor((totalSeconds % 3600) / 60);
2003
- const seconds = totalSeconds % 60;
2030
+ // Ensure ms is an integer and convert to seconds
2031
+ const totalSeconds = Math.floor(ms / 1000) | 0;
2032
+ const hours = Math.floor(totalSeconds / 3600) | 0;
2033
+ const minutes = Math.floor((totalSeconds % 3600) / 60) | 0;
2034
+ const seconds = (totalSeconds % 60) | 0;
2004
2035
  if (hours > 0) {
2005
2036
  return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
2006
2037
  }
@@ -2039,8 +2070,9 @@ class Player extends events_1.EventEmitter {
2039
2070
  },
2040
2071
  };
2041
2072
  }
2042
- const total = track.duration > 1000 ? track.duration : track.duration * 1000;
2043
- const current = resource.playbackDuration + this.seekOffset;
2073
+ // Ensure all time values are integers (milliseconds)
2074
+ const total = Math.floor(track.duration > 1000 ? track.duration : track.duration * 1000) | 0;
2075
+ const current = Math.floor(resource.playbackDuration + this.seekOffset) | 0;
2044
2076
  return {
2045
2077
  current: current,
2046
2078
  total: total,
@@ -2151,18 +2183,19 @@ class Player extends events_1.EventEmitter {
2151
2183
  try {
2152
2184
  const track = this.queue.currentTrack;
2153
2185
  this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
2154
- const currentPosition = position >= 0 ? position : (this.currentResource?.playbackDuration ?? 0);
2155
- this.seekOffset = currentPosition;
2186
+ // Ensure all time values are integers (milliseconds)
2187
+ const currentPosition = (position >= 0 ? position : (this.currentResource?.playbackDuration ?? 0) + this.seekOffset) | 0;
2188
+ this.seekOffset = currentPosition | 0;
2156
2189
  const wasPaused = this.isPaused;
2157
- const playbackDuration = this.currentResource?.playbackDuration ?? 0;
2190
+ const playbackDuration = (this.currentResource?.playbackDuration ?? 0) | 0;
2158
2191
  const isForwardSeek = position < 0 || position >= playbackDuration;
2159
2192
  const currentStreamId = this.currentSlot.streamId;
2160
- // Try to grab the raw source stream for reuse (forward seeks only)
2193
+ // Try to grab the raw source stream for reuse (only when not seeking, i.e. just applying filters)
2161
2194
  let reuseStream = null;
2162
- if (isForwardSeek && currentStreamId) {
2195
+ if (position < 0 && isForwardSeek && currentStreamId) {
2163
2196
  reuseStream = this.streamManager.getRawStream(currentStreamId);
2164
2197
  if (reuseStream) {
2165
- this.debug(`[Player] Will reuse source stream for seek (pos: ${currentPosition}ms)`);
2198
+ this.debug(`[Player] Will reuse source stream for filter application`);
2166
2199
  }
2167
2200
  }
2168
2201
  if (reuseStream) {
@@ -2301,6 +2334,10 @@ class Player extends events_1.EventEmitter {
2301
2334
  this.debug(`[Player] AudioPlayer went idle during resource refresh — skipping trackEnd/playNext`);
2302
2335
  return;
2303
2336
  }
2337
+ if (this.recoveryInProgress) {
2338
+ this.debug(`[Player] AudioPlayer went idle during recovery — skipping playNext`);
2339
+ return;
2340
+ }
2304
2341
  // Track ended
2305
2342
  const track = this.queue.currentTrack;
2306
2343
  if (track) {
@@ -2397,6 +2434,8 @@ class Player extends events_1.EventEmitter {
2397
2434
  this.audioPlayer.on("error", (error) => {
2398
2435
  if (this.destroyed)
2399
2436
  return;
2437
+ if (this.recoveryInProgress)
2438
+ return;
2400
2439
  this.debug(`[Player] AudioPlayer error:`, error);
2401
2440
  this.emit("playerError", error, this.queue.currentTrack || undefined);
2402
2441
  const track = this.queue.currentTrack;