@kattowo/distube 6.0.2 → 6.1.0

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/index.mjs CHANGED
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/constant.ts
5
- var version = "6.0.2";
5
+ var version = "6.1.0";
6
6
  var AUDIO_SAMPLE_RATE = 48e3;
7
7
  var AUDIO_CHANNELS = 2;
8
8
  var DEFAULT_VOLUME = 50;
@@ -29,6 +29,7 @@ var defaultFilters = {
29
29
  surround: "surround",
30
30
  earwax: "earwax"
31
31
  };
32
+ var DEFAULT_PRELOAD_THRESHOLD_SECS = 15;
32
33
  var defaultOptions = {
33
34
  plugins: [],
34
35
  emitNewSongOnly: false,
@@ -38,7 +39,8 @@ var defaultOptions = {
38
39
  emitAddListWhenCreatingQueue: true,
39
40
  joinNewVoiceChannel: true,
40
41
  unhandledError: "log",
41
- stuckThreshold: STUCK_THRESHOLD_MS
42
+ stuckThreshold: STUCK_THRESHOLD_MS,
43
+ preload: false
42
44
  };
43
45
 
44
46
  // src/core/DisTubeBase.ts
@@ -615,7 +617,14 @@ var DisTubeVoice = class extends TypedEmitter {
615
617
  if (newState.reason === VoiceConnectionDisconnectReason.Manual) {
616
618
  this.leave();
617
619
  } else if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
618
- entersState(this.connection, VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).catch(() => {
620
+ entersState(this.connection, VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).then(() => entersState(this.connection, VoiceConnectionStatus.Ready, RECONNECT_TIMEOUT_MS)).then(() => {
621
+ const oldChannel = this.#channel;
622
+ const currentChannel = this.channel;
623
+ if (oldChannel && currentChannel && oldChannel.id !== currentChannel.id) {
624
+ this.#channel = currentChannel;
625
+ this.emit("channelChange", oldChannel, currentChannel);
626
+ }
627
+ }).catch(() => {
619
628
  if (![VoiceConnectionStatus.Ready, VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
620
629
  this.leave();
621
630
  }
@@ -676,8 +685,12 @@ var DisTubeVoice = class extends TypedEmitter {
676
685
  if (channel.full) throw new DisTubeError("VOICE_FULL");
677
686
  else throw new DisTubeError("VOICE_MISSING_PERMS");
678
687
  }
688
+ const oldChannel = this.#channel;
679
689
  this.connection = this.#join(channel);
680
690
  this.#channel = channel;
691
+ if (oldChannel && oldChannel.id !== channel.id) {
692
+ this.emit("channelChange", oldChannel, channel);
693
+ }
681
694
  }
682
695
  #join(channel) {
683
696
  return joinVoiceChannel({
@@ -965,7 +978,13 @@ var FilterManager = class extends BaseManager {
965
978
  }
966
979
  throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
967
980
  }
968
- #apply() {
981
+ #apply(oldFilters) {
982
+ this.queue._clearPreload?.();
983
+ const newFilters = this.names;
984
+ const changed = oldFilters.length !== newFilters.length || !oldFilters.every((f) => newFilters.includes(f));
985
+ if (changed) {
986
+ this.emit("changeFilter" /* CHANGE_FILTER */, this.queue, newFilters, oldFilters);
987
+ }
969
988
  void this.queue._restartStream().catch((e) => {
970
989
  const errorMessage = e instanceof Error ? e.message : String(e);
971
990
  this.queue.debug(`[${this.queue.id}] Failed to apply the filters: ${errorMessage}`);
@@ -983,6 +1002,7 @@ var FilterManager = class extends BaseManager {
983
1002
  * ```
984
1003
  */
985
1004
  add(filterOrFilters, override = false) {
1005
+ const oldFilters = this.names;
986
1006
  if (Array.isArray(filterOrFilters)) {
987
1007
  for (const filter of filterOrFilters) {
988
1008
  const ft = this.#resolve(filter);
@@ -992,7 +1012,7 @@ var FilterManager = class extends BaseManager {
992
1012
  const ft = this.#resolve(filterOrFilters);
993
1013
  if (override || !this.has(ft)) this.collection.set(ft.name, ft);
994
1014
  }
995
- this.#apply();
1015
+ this.#apply(oldFilters);
996
1016
  return this;
997
1017
  }
998
1018
  /**
@@ -1011,12 +1031,13 @@ var FilterManager = class extends BaseManager {
1011
1031
  */
1012
1032
  set(filters) {
1013
1033
  if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
1034
+ const oldFilters = this.names;
1014
1035
  this.collection.clear();
1015
1036
  for (const f of filters) {
1016
1037
  const filter = this.#resolve(f);
1017
1038
  this.collection.set(filter.name, filter);
1018
1039
  }
1019
- this.#apply();
1040
+ this.#apply(oldFilters);
1020
1041
  return this;
1021
1042
  }
1022
1043
  #removeFn(f) {
@@ -1029,9 +1050,10 @@ var FilterManager = class extends BaseManager {
1029
1050
  * @throws {@link DisTubeError} When a filter is not a valid {@link FilterResolvable}
1030
1051
  */
1031
1052
  remove(filterOrFilters) {
1053
+ const oldFilters = this.names;
1032
1054
  if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
1033
1055
  else this.#removeFn(filterOrFilters);
1034
- this.#apply();
1056
+ this.#apply(oldFilters);
1035
1057
  return this;
1036
1058
  }
1037
1059
  /**
@@ -1087,6 +1109,17 @@ var Events = /* @__PURE__ */ ((Events2) => {
1087
1109
  Events2["DELETE_QUEUE"] = "deleteQueue";
1088
1110
  Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
1089
1111
  Events2["DEBUG"] = "debug";
1112
+ Events2["PAUSE_SONG"] = "pauseSong";
1113
+ Events2["RESUME_SONG"] = "resumeSong";
1114
+ Events2["SEEK_SONG"] = "seekSong";
1115
+ Events2["SET_VOLUME"] = "setVolume";
1116
+ Events2["SHUFFLE_QUEUE"] = "shuffleQueue";
1117
+ Events2["SKIP_SONG"] = "skipSong";
1118
+ Events2["CHANGE_FILTER"] = "changeFilter";
1119
+ Events2["VOICE_CHANNEL_CHANGE"] = "voiceChannelChange";
1120
+ Events2["PRELOAD_SONG"] = "preloadSong";
1121
+ Events2["PRELOADED_SONG"] = "preloadedSong";
1122
+ Events2["PRELOAD_ERROR"] = "preloadError";
1090
1123
  return Events2;
1091
1124
  })(Events || {});
1092
1125
  var SearchResultType = /* @__PURE__ */ ((SearchResultType2) => {
@@ -1220,6 +1253,7 @@ var Queue = class extends DisTubeBase {
1220
1253
  */
1221
1254
  _beginTime;
1222
1255
  #filters;
1256
+ #initialized = false;
1223
1257
  /**
1224
1258
  * Whether or not the queue is being updated manually (skip, jump, previous)
1225
1259
  * @internal
@@ -1235,6 +1269,48 @@ var Queue = class extends DisTubeBase {
1235
1269
  * @internal
1236
1270
  */
1237
1271
  _listeners;
1272
+ /**
1273
+ * Preload timer
1274
+ * @internal
1275
+ */
1276
+ _preloadTimeout;
1277
+ /**
1278
+ * Preloaded song
1279
+ * @internal
1280
+ */
1281
+ _preloadedSong;
1282
+ /**
1283
+ * Preloaded stream
1284
+ * @internal
1285
+ */
1286
+ _preloadedStream;
1287
+ /**
1288
+ * In-flight preload task
1289
+ * @internal
1290
+ */
1291
+ _preloading;
1292
+ /**
1293
+ * Bumped whenever the preloaded state is invalidated, so an in-flight
1294
+ * preload task can detect that its result is no longer needed.
1295
+ * @internal
1296
+ */
1297
+ _preloadGeneration = 0;
1298
+ /**
1299
+ * Clear any preloaded song and cancel pending preload timer.
1300
+ * @internal
1301
+ */
1302
+ _clearPreload() {
1303
+ this._preloadGeneration++;
1304
+ if (this._preloadTimeout) {
1305
+ clearTimeout(this._preloadTimeout);
1306
+ this._preloadTimeout = void 0;
1307
+ }
1308
+ if (this._preloadedStream) {
1309
+ this._preloadedStream.kill();
1310
+ this._preloadedStream = void 0;
1311
+ }
1312
+ this._preloadedSong = void 0;
1313
+ }
1238
1314
  /**
1239
1315
  * Create a queue for the guild
1240
1316
  * @param distube - DisTube
@@ -1263,6 +1339,7 @@ var Queue = class extends DisTubeBase {
1263
1339
  input: { ...this.options.ffmpeg.args.input },
1264
1340
  output: { ...this.options.ffmpeg.args.output }
1265
1341
  };
1342
+ this.#initialized = true;
1266
1343
  }
1267
1344
  #addToPreviousSongs(songs) {
1268
1345
  if (Array.isArray(songs)) {
@@ -1345,7 +1422,11 @@ var Queue = class extends DisTubeBase {
1345
1422
  * negative
1346
1423
  */
1347
1424
  set volume(value) {
1425
+ const oldVolume = this.voice.volume;
1348
1426
  this.voice.volume = value;
1427
+ if (this.#initialized && oldVolume !== value) {
1428
+ this.emit("setVolume" /* SET_VOLUME */, this, value, oldVolume);
1429
+ }
1349
1430
  }
1350
1431
  /**
1351
1432
  * Add a song or a list of songs to the queue.
@@ -1374,6 +1455,7 @@ var Queue = class extends DisTubeBase {
1374
1455
  } else {
1375
1456
  this.songs.splice(position, 0, song);
1376
1457
  }
1458
+ this.queues?.schedulePreload?.(this);
1377
1459
  return this;
1378
1460
  }
1379
1461
  /**
@@ -1391,7 +1473,9 @@ var Queue = class extends DisTubeBase {
1391
1473
  try {
1392
1474
  if (this.paused) throw new DisTubeError("PAUSED");
1393
1475
  this.paused = true;
1476
+ this._clearPreload();
1394
1477
  this.voice.pause();
1478
+ this.emit("pauseSong" /* PAUSE_SONG */, this, this.songs[0]);
1395
1479
  return this;
1396
1480
  } finally {
1397
1481
  this._taskQueue.resolve();
@@ -1412,6 +1496,8 @@ var Queue = class extends DisTubeBase {
1412
1496
  if (!this.paused) throw new DisTubeError("RESUMED");
1413
1497
  this.paused = false;
1414
1498
  this.voice.unpause();
1499
+ this.emit("resumeSong" /* RESUME_SONG */, this, this.songs[0]);
1500
+ this.queues?.schedulePreload?.(this);
1415
1501
  return this;
1416
1502
  } finally {
1417
1503
  this._taskQueue.resolve();
@@ -1476,11 +1562,14 @@ var Queue = class extends DisTubeBase {
1476
1562
  try {
1477
1563
  const playing = this.songs.shift();
1478
1564
  if (playing === void 0) return this;
1565
+ this._clearPreload();
1479
1566
  for (let i = this.songs.length - 1; i > 0; i--) {
1480
1567
  const j = Math.floor(Math.random() * (i + 1));
1481
1568
  [this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
1482
1569
  }
1483
1570
  this.songs.unshift(playing);
1571
+ this.emit("shuffleQueue" /* SHUFFLE_QUEUE */, this, this.songs);
1572
+ this.queues?.schedulePreload?.(this);
1484
1573
  return this;
1485
1574
  } finally {
1486
1575
  this._taskQueue.resolve();
@@ -1518,12 +1607,14 @@ var Queue = class extends DisTubeBase {
1518
1607
  } else {
1519
1608
  this.#addToPreviousSongs(skipped);
1520
1609
  }
1610
+ this.emit("skipSong" /* SKIP_SONG */, this, skipped[0]);
1521
1611
  } else if (!this.options.savePreviousSongs) {
1522
1612
  throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
1523
1613
  } else {
1524
1614
  const skipped = this.previousSongs.splice(position);
1525
1615
  this.songs.unshift(...skipped);
1526
1616
  }
1617
+ this._clearPreload();
1527
1618
  this.#stop();
1528
1619
  return this.songs[0];
1529
1620
  } finally {
@@ -1562,8 +1653,10 @@ var Queue = class extends DisTubeBase {
1562
1653
  try {
1563
1654
  if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
1564
1655
  if (Number.isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
1656
+ this._clearPreload();
1565
1657
  this._beginTime = time;
1566
1658
  await this.play(false);
1659
+ this.emit("seekSong" /* SEEK_SONG */, this, this.songs[0], time);
1567
1660
  return this;
1568
1661
  } finally {
1569
1662
  this._taskQueue.resolve();
@@ -1634,6 +1727,7 @@ var Queue = class extends DisTubeBase {
1634
1727
  * @returns Nothing
1635
1728
  */
1636
1729
  remove() {
1730
+ this._clearPreload();
1637
1731
  this.paused = true;
1638
1732
  this.stopped = true;
1639
1733
  this.songs = [];
@@ -2307,6 +2401,11 @@ var Options = class {
2307
2401
  * @default 10000
2308
2402
  */
2309
2403
  stuckThreshold;
2404
+ /**
2405
+ * The preload options of the DisTube instance.
2406
+ * @default { enabled: false, threshold: 15 }
2407
+ */
2408
+ preload;
2310
2409
  /**
2311
2410
  * The FFmpeg options of the DisTube instance.
2312
2411
  */
@@ -2332,10 +2431,34 @@ var Options = class {
2332
2431
  this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
2333
2432
  this.unhandledError = opts.unhandledError;
2334
2433
  this.stuckThreshold = opts.stuckThreshold;
2434
+ this.preload = this.#preloadOption(opts.preload);
2335
2435
  this.ffmpeg = this.#ffmpegOption(options);
2336
2436
  checkInvalidKey(opts, this, "DisTubeOptions");
2337
2437
  this.#validateOptions();
2338
2438
  }
2439
+ #preloadOption(value) {
2440
+ if (value === void 0 || value === false) {
2441
+ return { enabled: false, threshold: DEFAULT_PRELOAD_THRESHOLD_SECS };
2442
+ }
2443
+ if (value === true) {
2444
+ return { enabled: true, threshold: DEFAULT_PRELOAD_THRESHOLD_SECS };
2445
+ }
2446
+ if (typeof value !== "object" || Array.isArray(value)) {
2447
+ throw new DisTubeError("INVALID_TYPE", ["boolean", "object"], value, "DisTubeOptions.preload");
2448
+ }
2449
+ checkInvalidKey(value, ["enabled", "threshold"], "DisTubeOptions.preload");
2450
+ const { enabled, threshold } = value;
2451
+ if (enabled !== void 0 && typeof enabled !== "boolean") {
2452
+ throw new DisTubeError("INVALID_TYPE", "boolean", enabled, "DisTubeOptions.preload.enabled");
2453
+ }
2454
+ if (threshold !== void 0 && (typeof threshold !== "number" || Number.isNaN(threshold) || threshold < 0)) {
2455
+ throw new DisTubeError("INVALID_TYPE", "number", threshold, "DisTubeOptions.preload.threshold");
2456
+ }
2457
+ return {
2458
+ enabled: enabled ?? false,
2459
+ threshold: threshold ?? DEFAULT_PRELOAD_THRESHOLD_SECS
2460
+ };
2461
+ }
2339
2462
  #validateOptions(options = this) {
2340
2463
  const booleanOptions = /* @__PURE__ */ new Set([
2341
2464
  "emitNewSongOnly",
@@ -2347,7 +2470,7 @@ var Options = class {
2347
2470
  ]);
2348
2471
  const numberOptions = /* @__PURE__ */ new Set(["stuckThreshold"]);
2349
2472
  const stringOptions = /* @__PURE__ */ new Set(["unhandledError"]);
2350
- const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
2473
+ const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg", "preload"]);
2351
2474
  const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
2352
2475
  for (const [key, value] of Object.entries(options)) {
2353
2476
  if (value === void 0 && optionalOptions.has(key)) continue;
@@ -2735,7 +2858,10 @@ var QueueManager = class extends GuildIdManager {
2735
2858
  if (error) this.emitError(error, queue, queue.songs?.[0]);
2736
2859
  }, "disconnect"),
2737
2860
  error: /* @__PURE__ */ __name((error) => void this.#handlePlayingError(queue, error), "error"),
2738
- finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish")
2861
+ finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish"),
2862
+ channelChange: /* @__PURE__ */ __name((oldChannel, newChannel) => {
2863
+ this.emit("voiceChannelChange" /* VOICE_CHANNEL_CHANGE */, queue, oldChannel, newChannel);
2864
+ }, "channelChange")
2739
2865
  };
2740
2866
  for (const event of objectKeys(queue._listeners)) {
2741
2867
  queue.voice.on(event, queue._listeners[event]);
@@ -2833,12 +2959,109 @@ var QueueManager = class extends GuildIdManager {
2833
2959
  }
2834
2960
  try {
2835
2961
  const song = queue.songs[0];
2836
- this.debug(`[${queue.id}] Getting stream from: ${song}`);
2837
- await this.handler.attachStreamInfo(song);
2838
- const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
2962
+ let dtStream;
2963
+ if (queue._preloadedSong === song && queue._preloadedStream) {
2964
+ this.debug(`[${queue.id}] Using preloaded stream for: ${song}`);
2965
+ dtStream = queue._preloadedStream;
2966
+ queue._preloadedSong = void 0;
2967
+ queue._preloadedStream = void 0;
2968
+ } else {
2969
+ queue._clearPreload();
2970
+ this.debug(`[${queue.id}] Getting stream from: ${song}`);
2971
+ await this.handler.attachStreamInfo(song);
2972
+ const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
2973
+ const stream = willPlaySong?.stream;
2974
+ if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
2975
+ this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
2976
+ const streamOptions = {
2977
+ ffmpeg: {
2978
+ path: this.options.ffmpeg.path,
2979
+ args: {
2980
+ global: { ...queue.ffmpegArgs.global },
2981
+ input: { ...queue.ffmpegArgs.input },
2982
+ output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
2983
+ }
2984
+ },
2985
+ seek: willPlaySong.duration ? queue._beginTime : void 0
2986
+ };
2987
+ queue._beginTime = 0;
2988
+ dtStream = new DisTubeStream(stream.url, streamOptions);
2989
+ dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
2990
+ }
2991
+ queue._beginTime = 0;
2992
+ this.debug(`[${queue.id}] Started playing: ${song}`);
2993
+ await queue.voice.play(dtStream);
2994
+ if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
2995
+ this.schedulePreload(queue);
2996
+ } catch (e) {
2997
+ const error = e instanceof Error ? e : new Error(String(e));
2998
+ if (options?.keepSongOnError) {
2999
+ this.debug(`[${queue.id}] Error while restarting the stream: ${error.stack || error.message}`);
3000
+ this.emitError(error, queue, queue.songs[0]);
3001
+ return;
3002
+ }
3003
+ await this.#handlePlayingError(queue, error);
3004
+ }
3005
+ }
3006
+ /**
3007
+ * Schedule preloading the next song in the queue before the current song finishes.
3008
+ * @param queue - The guild queue
3009
+ */
3010
+ schedulePreload(queue) {
3011
+ if (!this.options.preload.enabled || queue.stopped || queue.paused) return;
3012
+ if (queue._preloadTimeout) {
3013
+ clearTimeout(queue._preloadTimeout);
3014
+ queue._preloadTimeout = void 0;
3015
+ }
3016
+ const currentSong = queue.songs[0];
3017
+ const nextSong = queue.songs[1];
3018
+ if (!currentSong || !nextSong) return;
3019
+ if (queue._preloadedSong === nextSong && queue._preloadedStream) return;
3020
+ const threshold = this.options.preload.threshold;
3021
+ const duration = currentSong.duration ?? 0;
3022
+ const currentTime = queue.currentTime;
3023
+ const remaining = duration > 0 && Number.isFinite(currentTime) ? duration - currentTime : 0;
3024
+ if (duration === 0 || remaining <= threshold) {
3025
+ void this.preloadNextSong(queue);
3026
+ } else {
3027
+ const delayMs = Math.max(0, (remaining - threshold) * 1e3);
3028
+ queue._preloadTimeout = setTimeout(() => {
3029
+ queue._preloadTimeout = void 0;
3030
+ void this.preloadNextSong(queue);
3031
+ }, delayMs);
3032
+ queue._preloadTimeout.unref?.();
3033
+ }
3034
+ }
3035
+ /**
3036
+ * Preload the next song in the queue.
3037
+ * @param queue - The guild queue
3038
+ */
3039
+ async preloadNextSong(queue) {
3040
+ if (queue._preloading) return queue._preloading;
3041
+ const task = this.#preloadTask(queue);
3042
+ queue._preloading = task;
3043
+ try {
3044
+ await task;
3045
+ } finally {
3046
+ if (queue._preloading === task) queue._preloading = void 0;
3047
+ }
3048
+ }
3049
+ async #preloadTask(queue) {
3050
+ if (!this.options.preload.enabled || queue.stopped || queue.paused) return;
3051
+ const nextSong = queue.songs[1];
3052
+ if (!nextSong) return;
3053
+ if (queue._preloadedSong === nextSong && queue._preloadedStream) return;
3054
+ const generation = queue._preloadGeneration;
3055
+ this.debug(`[${queue.id}] Preloading next song: ${nextSong}`);
3056
+ this.emit("preloadSong" /* PRELOAD_SONG */, queue, nextSong);
3057
+ try {
3058
+ await this.handler.attachStreamInfo(nextSong);
3059
+ if (queue.stopped || queue.songs[1] !== nextSong || generation !== queue._preloadGeneration) return;
3060
+ const willPlaySong = nextSong.stream.playFromSource ? nextSong : nextSong.stream.song;
2839
3061
  const stream = willPlaySong?.stream;
2840
- if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
2841
- this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
3062
+ if (!willPlaySong || !stream?.playFromSource || !stream.url) {
3063
+ throw new DisTubeError("NO_STREAM_URL", `${nextSong}`);
3064
+ }
2842
3065
  const streamOptions = {
2843
3066
  ffmpeg: {
2844
3067
  path: this.options.ffmpeg.path,
@@ -2848,22 +3071,27 @@ var QueueManager = class extends GuildIdManager {
2848
3071
  output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
2849
3072
  }
2850
3073
  },
2851
- seek: willPlaySong.duration ? queue._beginTime : void 0
3074
+ seek: void 0
2852
3075
  };
2853
- queue._beginTime = 0;
2854
3076
  const dtStream = new DisTubeStream(stream.url, streamOptions);
2855
- dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
2856
- this.debug(`[${queue.id}] Started playing: ${willPlaySong}`);
2857
- await queue.voice.play(dtStream);
2858
- if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
2859
- } catch (e) {
2860
- const error = e instanceof Error ? e : new Error(String(e));
2861
- if (options?.keepSongOnError) {
2862
- this.debug(`[${queue.id}] Error while restarting the stream: ${error.stack || error.message}`);
2863
- this.emitError(error, queue, queue.songs[0]);
3077
+ dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] [preload] ${data}`));
3078
+ if (queue.stopped || queue.songs[1] !== nextSong || generation !== queue._preloadGeneration) {
3079
+ dtStream.kill();
2864
3080
  return;
2865
3081
  }
2866
- await this.#handlePlayingError(queue, error);
3082
+ queue._preloadedSong = nextSong;
3083
+ queue._preloadedStream = dtStream;
3084
+ this.debug(`[${queue.id}] Successfully preloaded next song: ${nextSong}`);
3085
+ this.emit("preloadedSong" /* PRELOADED_SONG */, queue, nextSong);
3086
+ } catch (e) {
3087
+ if (queue._preloadedSong === nextSong) {
3088
+ queue._preloadedSong = void 0;
3089
+ queue._preloadedStream?.kill();
3090
+ queue._preloadedStream = void 0;
3091
+ }
3092
+ const error = e instanceof Error ? e : new Error(String(e));
3093
+ this.debug(`[${queue.id}] Preload failed for ${nextSong}: ${error.message}`);
3094
+ this.emit("preloadError" /* PRELOAD_ERROR */, queue, nextSong, error);
2867
3095
  }
2868
3096
  }
2869
3097
  };
@@ -3879,6 +4107,7 @@ export {
3879
4107
  AUDIO_CHANNELS,
3880
4108
  AUDIO_SAMPLE_RATE,
3881
4109
  BaseManager,
4110
+ DEFAULT_PRELOAD_THRESHOLD_SECS,
3882
4111
  DEFAULT_VOLUME,
3883
4112
  DisTube,
3884
4113
  DisTubeBase,