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.
@@ -173,6 +173,7 @@ export class Player extends EventEmitter {
173
173
  private ttsPlayer: DiscordAudioPlayer | null = null;
174
174
  private lastDuration: number = 0;
175
175
  private seekOffset: number = 0;
176
+ private recoveryInProgress = false;
176
177
 
177
178
  constructor(guildId: string, options: PlayerOptions = {}, manager: PlayerManager) {
178
179
  super();
@@ -269,7 +270,7 @@ export class Player extends EventEmitter {
269
270
  extractorTimeout: this.options.extractorTimeout,
270
271
  });
271
272
  this.streamManager = new StreamManager({
272
- maxConcurrentStreams: 4,
273
+ maxConcurrentStreams: this.options?.maxStreamStore ?? 4,
273
274
  streamTimeout: 5 * 60 * 1000,
274
275
  maxListenersPerStream: 15,
275
276
  enableMetrics: true,
@@ -787,6 +788,7 @@ export class Player extends EventEmitter {
787
788
 
788
789
  private async attemptTrackRecovery(track: Track, reason: unknown): Promise<boolean> {
789
790
  if (!this.antiStuckEnabled) return false;
791
+ this.recoveryInProgress = true;
790
792
  this.debug(`[AntiStuck] Recovery started for: ${track.title}`, reason);
791
793
 
792
794
  const originalQuality = this.options.quality;
@@ -808,6 +810,7 @@ export class Player extends EventEmitter {
808
810
  if (startedFromPreload) {
809
811
  this.antiStuckConsecutiveFailures = 0;
810
812
  this.options.quality = originalQuality;
813
+ this.recoveryInProgress = false;
811
814
  return true;
812
815
  }
813
816
  }
@@ -816,6 +819,7 @@ export class Player extends EventEmitter {
816
819
  if (started) {
817
820
  this.antiStuckConsecutiveFailures = 0;
818
821
  this.options.quality = originalQuality;
822
+ this.recoveryInProgress = false;
819
823
  return true;
820
824
  }
821
825
  } catch (error) {
@@ -827,9 +831,10 @@ export class Player extends EventEmitter {
827
831
  this.antiStuckConsecutiveFailures++;
828
832
  if (this.antiStuckConsecutiveFailures >= this.antiStuckControlledSkipThreshold) {
829
833
  this.debug(`[AntiStuck] Controlled skip threshold reached for ${track.title}`);
834
+ this.recoveryInProgress = false;
830
835
  return false;
831
836
  }
832
-
837
+ this.recoveryInProgress = false;
833
838
  // Avoid hard skip storm by leaving track for next natural retry window.
834
839
  this.debug(`[AntiStuck] Keeping track for controlled retry window: ${track.title}`);
835
840
  return false;
@@ -900,16 +905,18 @@ export class Player extends EventEmitter {
900
905
  const seekArg = position > 0 ? position : -1;
901
906
 
902
907
  if (filterString || position > 0) {
903
- const processedStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, seekArg);
908
+ const processedStream = await this.filter.applyFiltersAndSeek(streamInfo, seekArg);
904
909
 
905
- // rawStream. Always use Arbitrary so discordjs/voice doesn't re-encode.
906
- const resource = createAudioResource(processedStream, {
910
+ const resource = createAudioResource(processedStream.stream, {
907
911
  metadata: track,
908
- inputType: StreamType.Arbitrary,
912
+ inputType:
913
+ processedStream.wasRecreated && !filterString ? StreamType.Arbitrary
914
+ : position > 0 ? StreamType.Raw
915
+ : StreamType.Arbitrary,
909
916
  inlineVolume: true,
910
917
  });
911
918
 
912
- return { resource, processedStream };
919
+ return { resource, processedStream: processedStream.stream };
913
920
  }
914
921
 
915
922
  const resource = createAudioResource(streamInfo.stream, {
@@ -1077,6 +1084,7 @@ export class Player extends EventEmitter {
1077
1084
  const currentResource = this.currentSlot.resource;
1078
1085
  if (!currentResource) return false;
1079
1086
 
1087
+ // Ensure seekOffset is always an integer (milliseconds)
1080
1088
  this.seekOffset = 0;
1081
1089
  const targetVolume = this.getTrackTargetVolume(track);
1082
1090
 
@@ -1740,6 +1748,7 @@ export class Player extends EventEmitter {
1740
1748
  this.debug("[Player] Cannot stop while subscribed to another player");
1741
1749
  return false;
1742
1750
  }
1751
+ this.recoveryInProgress = false;
1743
1752
  this.debug(`[Player] stop called`);
1744
1753
  if (this.playbackMode === PlaybackMode.REMOTE) {
1745
1754
  this.cancelPreload();
@@ -1831,7 +1840,7 @@ export class Player extends EventEmitter {
1831
1840
  return false;
1832
1841
  }
1833
1842
  this.debug(`[Player] skip called with index: ${index}`);
1834
-
1843
+ this.recoveryInProgress = false;
1835
1844
  if (this.playbackMode === PlaybackMode.REMOTE) {
1836
1845
  if (typeof index === "number" && index >= 0) {
1837
1846
  for (let i = 0; i < index; i++) this.queue.remove(0);
@@ -1953,7 +1962,7 @@ export class Player extends EventEmitter {
1953
1962
  }
1954
1963
 
1955
1964
  // Apply filters if any are active
1956
- let finalStream = streamInfo.stream;
1965
+ let finalStream = streamInfo;
1957
1966
 
1958
1967
  if (saveOptions.filter || saveOptions.seek) {
1959
1968
  try {
@@ -1964,14 +1973,14 @@ export class Player extends EventEmitter {
1964
1973
  }
1965
1974
 
1966
1975
  this.debug(`[Player] Applying filters to save stream: ${this.filter.getFilterString() || "none"}`);
1967
- finalStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, saveOptions.seek || 0).catch((err) => {
1976
+ finalStream = await this.filter.applyFiltersAndSeek(streamInfo, saveOptions.seek || 0).catch((err) => {
1968
1977
  this.debug(`[Player] Error applying filters to save stream:`, err);
1969
- return streamInfo!.stream; // Fallback to original stream
1978
+ return streamInfo; // Fallback to original stream
1970
1979
  });
1971
1980
  }
1972
1981
 
1973
1982
  // Return the stream directly - caller can pipe it to fs.createWriteStream()
1974
- return finalStream;
1983
+ return finalStream.stream;
1975
1984
  } catch (error) {
1976
1985
  this.debug(`[Player] save error:`, error);
1977
1986
  this.emit("playerError", error as Error, track);
@@ -2272,27 +2281,36 @@ export class Player extends EventEmitter {
2272
2281
  * @returns Formatted time string with leading zeros
2273
2282
  */
2274
2283
  formatTime(ms: number): string {
2275
- const totalSeconds = Math.floor(ms / 1000);
2276
- const hours = Math.floor(totalSeconds / 3600);
2277
- const minutes = Math.floor((totalSeconds % 3600) / 60);
2278
- const seconds = totalSeconds % 60;
2284
+ // Ensure ms is an integer and convert to seconds
2285
+ const totalSeconds = Math.floor(ms / 1000) | 0;
2286
+ const hours = Math.floor(totalSeconds / 3600) | 0;
2287
+ const minutes = Math.floor((totalSeconds % 3600) / 60) | 0;
2288
+ const seconds = (totalSeconds % 60) | 0;
2289
+
2279
2290
  const parts: string[] = [];
2280
- if (hours > 0) parts.push(String(hours).padStart(2, "0"));
2281
- parts.push(String(minutes).padStart(2, "0"));
2291
+
2292
+ if (hours > 0) {
2293
+ parts.push(String(hours)); // Giờ không padStart (ví dụ: 1:05:00)
2294
+ parts.push(String(minutes).padStart(2, "0"));
2295
+ } else {
2296
+ parts.push(String(minutes)); // Phút không padStart nếu là số đầu tiên (ví dụ: 0:30 thay vì 00:30)
2297
+ }
2298
+
2282
2299
  parts.push(String(seconds).padStart(2, "0"));
2283
2300
  return parts.join(":");
2284
2301
  }
2285
2302
 
2286
2303
  /**
2287
2304
  * Format time without leading zeros for hours (1:22:12 or 3:45)
2288
- * @param ms - Time in milliseconds
2305
+ * @param ms - Time in milliseconds (must be integer)
2289
2306
  * @returns Compact formatted time string
2290
2307
  */
2291
2308
  formatTimeCompact(ms: number): string {
2292
- const totalSeconds = Math.floor(ms / 1000);
2293
- const hours = Math.floor(totalSeconds / 3600);
2294
- const minutes = Math.floor((totalSeconds % 3600) / 60);
2295
- const seconds = totalSeconds % 60;
2309
+ // Ensure ms is an integer and convert to seconds
2310
+ const totalSeconds = Math.floor(ms / 1000) | 0;
2311
+ const hours = Math.floor(totalSeconds / 3600) | 0;
2312
+ const minutes = Math.floor((totalSeconds % 3600) / 60) | 0;
2313
+ const seconds = (totalSeconds % 60) | 0;
2296
2314
 
2297
2315
  if (hours > 0) {
2298
2316
  return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
@@ -2335,8 +2353,9 @@ export class Player extends EventEmitter {
2335
2353
  };
2336
2354
  }
2337
2355
 
2338
- const total = track.duration > 1000 ? track.duration : track.duration * 1000;
2339
- const current = resource.playbackDuration + this.seekOffset;
2356
+ // Ensure all time values are integers (milliseconds)
2357
+ const total = Math.floor(track.duration > 1000 ? track.duration : track.duration * 1000) | 0;
2358
+ const current = Math.floor(resource.playbackDuration + this.seekOffset) | 0;
2340
2359
 
2341
2360
  return {
2342
2361
  current: current,
@@ -2465,20 +2484,22 @@ export class Player extends EventEmitter {
2465
2484
  const track = this.queue.currentTrack;
2466
2485
  this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
2467
2486
 
2468
- const currentPosition = position >= 0 ? position : (this.currentResource?.playbackDuration ?? 0);
2469
- this.seekOffset = currentPosition;
2487
+ // Ensure all time values are integers (milliseconds)
2488
+ const currentPosition = (position >= 0 ? position : (this.currentResource?.playbackDuration ?? 0) + this.seekOffset) | 0;
2489
+
2490
+ this.seekOffset = currentPosition | 0;
2470
2491
  const wasPaused = this.isPaused;
2471
- const playbackDuration = this.currentResource?.playbackDuration ?? 0;
2492
+ const playbackDuration = (this.currentResource?.playbackDuration ?? 0) | 0;
2472
2493
 
2473
2494
  const isForwardSeek = position < 0 || position >= playbackDuration;
2474
2495
  const currentStreamId = this.currentSlot.streamId;
2475
2496
 
2476
- // Try to grab the raw source stream for reuse (forward seeks only)
2497
+ // Try to grab the raw source stream for reuse (only when not seeking, i.e. just applying filters)
2477
2498
  let reuseStream: import("stream").Readable | null = null;
2478
- if (isForwardSeek && currentStreamId) {
2499
+ if (position < 0 && isForwardSeek && currentStreamId) {
2479
2500
  reuseStream = this.streamManager.getRawStream(currentStreamId);
2480
2501
  if (reuseStream) {
2481
- this.debug(`[Player] Will reuse source stream for seek (pos: ${currentPosition}ms)`);
2502
+ this.debug(`[Player] Will reuse source stream for filter application`);
2482
2503
  }
2483
2504
  }
2484
2505
 
@@ -2636,6 +2657,10 @@ export class Player extends EventEmitter {
2636
2657
  this.debug(`[Player] AudioPlayer went idle during resource refresh — skipping trackEnd/playNext`);
2637
2658
  return;
2638
2659
  }
2660
+ if (this.recoveryInProgress) {
2661
+ this.debug(`[Player] AudioPlayer went idle during recovery — skipping playNext`);
2662
+ return;
2663
+ }
2639
2664
  // Track ended
2640
2665
  const track = this.queue.currentTrack;
2641
2666
  if (track) {
@@ -2731,6 +2756,7 @@ export class Player extends EventEmitter {
2731
2756
  });
2732
2757
  this.audioPlayer.on("error", (error) => {
2733
2758
  if (this.destroyed) return;
2759
+ if (this.recoveryInProgress) return;
2734
2760
  this.debug(`[Player] AudioPlayer error:`, error);
2735
2761
  this.emit("playerError", error, this.queue.currentTrack || undefined);
2736
2762
  const track = this.queue.currentTrack;