@kattowo/distube 6.2.0 → 6.2.2

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.d.mts CHANGED
@@ -343,7 +343,49 @@ type CrossfadeOptions = {
343
343
  * @default 5
344
344
  */
345
345
  warmup: number;
346
+ /**
347
+ * Prepare the fading-out song's playback input for the crossfade FFmpeg
348
+ * process. It is invoked right before the transition stream is spawned,
349
+ * with the song's stream url and the position (in seconds, counted from the
350
+ * beginning of the song) where the fade window begins.
351
+ *
352
+ * Streaming inputs that cannot fast-seek (for example HLS playlists) force
353
+ * FFmpeg to re-download the whole stream and discard everything before the
354
+ * seek position, which can miss the swap window on slow sources. The
355
+ * adapter can return a replacement input that starts near the boundary —
356
+ * a local playlist trimmed to the fade position, for example — along with
357
+ * the seek offset inside it and its own FFmpeg input options. Return `null`
358
+ * or `undefined` to use the url as-is.
359
+ */
360
+ preparePrimaryInput?: PrepareCrossfadeInput;
361
+ };
362
+ /**
363
+ * The replacement input returned by {@link CrossfadeOptions.preparePrimaryInput}.
364
+ */
365
+ type PreparedCrossfadeInput = {
366
+ /**
367
+ * The replacement input url or path, for example a local playlist file
368
+ * trimmed to start at the fade boundary.
369
+ */
370
+ url: string;
371
+ /**
372
+ * The initial seek of the replacement input, in seconds relative to it.
373
+ * When omitted, the input is assumed to start exactly at the fade boundary.
374
+ */
375
+ seek?: number;
376
+ /**
377
+ * FFmpeg input options of the replacement input. They replace the default
378
+ * input options, which are tuned for http(s) streaming urls.
379
+ */
380
+ inputArgs?: FFmpegArg;
346
381
  };
382
+ /**
383
+ * The {@link CrossfadeOptions.preparePrimaryInput} adapter type.
384
+ */
385
+ type PrepareCrossfadeInput = (url: string, boundary: {
386
+ seek: number;
387
+ song: Song;
388
+ }) => Awaitable<PreparedCrossfadeInput | null | undefined>;
347
389
  /**
348
390
  * DisTube options
349
391
  */
@@ -447,6 +489,9 @@ type DisTubeOptions = {
447
489
  * - `curve`: the FFmpeg `acrossfade` fade curve (@default "qsin")
448
490
  * - `warmup`: seconds before the fade window when the transition stream is
449
491
  * spawned to warm up its buffers (@default 5)
492
+ * - `preparePrimaryInput`: an optional adapter preparing the fading-out
493
+ * song's FFmpeg input before the transition stream is spawned. Useful for
494
+ * streaming inputs that cannot fast-seek ({@link CrossfadeOptions.preparePrimaryInput}).
450
495
  * @default { duration: 0, curve: "qsin", warmup: 5 }
451
496
  */
452
497
  crossfade?: {
@@ -462,6 +507,11 @@ type DisTubeOptions = {
462
507
  * Seconds before the fade window when the transition stream is spawned.
463
508
  */
464
509
  warmup?: number;
510
+ /**
511
+ * Optional adapter preparing the fading-out song's FFmpeg input
512
+ * ({@link CrossfadeOptions.preparePrimaryInput}).
513
+ */
514
+ preparePrimaryInput?: PrepareCrossfadeInput;
465
515
  };
466
516
  /**
467
517
  * FFmpeg options
@@ -1408,6 +1458,14 @@ interface CrossfadeStreamOptions {
1408
1458
  * @default "qsin"
1409
1459
  */
1410
1460
  inCurve?: string;
1461
+ /**
1462
+ * FFmpeg input options of the first (fading out) input. When set, they
1463
+ * replace the default input options, which are tuned for http(s) streaming
1464
+ * urls. Use it for inputs that need different flags — a local playlist
1465
+ * file, for example, requires the `protocol_whitelist` option and no
1466
+ * reconnect flags.
1467
+ */
1468
+ primaryInputArgs?: FFmpegArg;
1411
1469
  }
1412
1470
  /**
1413
1471
  * Check that FFmpeg is installed and can be spawned.
@@ -1444,6 +1502,15 @@ declare class DisTubeStream extends TypedEmitter<{
1444
1502
  * The seek time in seconds that this stream started from
1445
1503
  */
1446
1504
  readonly seekTime: number;
1505
+ /**
1506
+ * Whether FFmpeg has already produced audio that is waiting to be read.
1507
+ *
1508
+ * The stream is spawned ahead of the playback (crossfade warmup) and the
1509
+ * pipe backs up once its buffers are full, so this is `true` as soon as the
1510
+ * first frame exists. A stream that is still `false` at playback time would
1511
+ * leave the audio player buffering until FFmpeg catches up.
1512
+ */
1513
+ get hasBufferedAudio(): boolean;
1447
1514
  /**
1448
1515
  * Create a DisTubeStream to play with {@link DisTubeVoice}
1449
1516
  * @param url - Stream URL
@@ -3614,4 +3681,4 @@ declare function handleUnhandledError(error: Error, behavior?: UnhandledErrorBeh
3614
3681
  declare const checkEncryptionLibraries: () => Promise<boolean>;
3615
3682
 
3616
3683
  export { AUDIO_CHANNELS, AUDIO_SAMPLE_RATE, BaseManager, CROSSFADE_DRIFT_TOLERANCE_SECS, DEFAULT_CROSSFADE_CURVE, DEFAULT_CROSSFADE_DURATION_SECS, DEFAULT_CROSSFADE_WARMUP_SECS, DEFAULT_PRELOAD_THRESHOLD_SECS, DEFAULT_VOLUME, DisTube, DisTubeBase, DisTubeError, DisTubeHandler, DisTubeStream, DisTubeVoice, DisTubeVoiceManager, Events, ExtractorPlugin, FilterManager, GuildIdManager, HTTP_REDIRECT_CODES, InfoExtractorPlugin, JOIN_TIMEOUT_MS, MAX_CROSSFADE_DURATION_SECS, MAX_REDIRECT_DEPTH, Options, PlayableExtractorPlugin, Playlist, Plugin, PluginError, PluginErrorCode, PluginType, Queue, QueueManager, RECONNECT_MAX_ATTEMPTS, RECONNECT_TIMEOUT_MS, RepeatMode, STUCK_THRESHOLD_MS, SearchResult, SearchResultType, Song, StreamServer, TaskQueue, checkEncryptionLibraries, checkFFmpeg, checkIntents, checkInvalidKey, DisTube as default, defaultFilters, defaultOptions, formatDuration, handleUnhandledError, isClientInstance, isGuildInstance, isMemberInstance, isMessageInstance, isNsfwChannel, isObject, isSnowflake, isSupportedVoiceChannel, isTextChannelInstance, isTruthy, isURL, isVoiceChannelEmpty, objectKeys, resolveGuildId, version };
3617
- export type { Awaitable, CrossfadeOptions, CrossfadeStreamOptions, CustomPlaylistOptions, DisTubeEvents, DisTubeOptions, DisTubePlugin, DisTubeVoiceEvents, Disposable, FFmpegArg, FFmpegArgs, FFmpegOptions, Falsy, Filter, FilterResolvable, Filters, GuildIdResolvable, JumpOptions, KeyOf, PlayHandlerOptions, PlayOptions, PlaylistInfo, PreloadOptions, RelatedSong, ResolveOptions, ResolvePlaylistOptions, SearchOptions, SongInfo, StreamHandler, StreamOptions, StreamRequest, StreamResponse, StreamServerOptions, TypedDisTubeEvents, UnhandledErrorBehavior };
3684
+ export type { Awaitable, CrossfadeOptions, CrossfadeStreamOptions, CustomPlaylistOptions, DisTubeEvents, DisTubeOptions, DisTubePlugin, DisTubeVoiceEvents, Disposable, FFmpegArg, FFmpegArgs, FFmpegOptions, Falsy, Filter, FilterResolvable, Filters, GuildIdResolvable, JumpOptions, KeyOf, PlayHandlerOptions, PlayOptions, PlaylistInfo, PreloadOptions, PrepareCrossfadeInput, PreparedCrossfadeInput, RelatedSong, ResolveOptions, ResolvePlaylistOptions, SearchOptions, SongInfo, StreamHandler, StreamOptions, StreamRequest, StreamResponse, StreamServerOptions, TypedDisTubeEvents, UnhandledErrorBehavior };
package/dist/index.d.ts CHANGED
@@ -343,7 +343,49 @@ type CrossfadeOptions = {
343
343
  * @default 5
344
344
  */
345
345
  warmup: number;
346
+ /**
347
+ * Prepare the fading-out song's playback input for the crossfade FFmpeg
348
+ * process. It is invoked right before the transition stream is spawned,
349
+ * with the song's stream url and the position (in seconds, counted from the
350
+ * beginning of the song) where the fade window begins.
351
+ *
352
+ * Streaming inputs that cannot fast-seek (for example HLS playlists) force
353
+ * FFmpeg to re-download the whole stream and discard everything before the
354
+ * seek position, which can miss the swap window on slow sources. The
355
+ * adapter can return a replacement input that starts near the boundary —
356
+ * a local playlist trimmed to the fade position, for example — along with
357
+ * the seek offset inside it and its own FFmpeg input options. Return `null`
358
+ * or `undefined` to use the url as-is.
359
+ */
360
+ preparePrimaryInput?: PrepareCrossfadeInput;
361
+ };
362
+ /**
363
+ * The replacement input returned by {@link CrossfadeOptions.preparePrimaryInput}.
364
+ */
365
+ type PreparedCrossfadeInput = {
366
+ /**
367
+ * The replacement input url or path, for example a local playlist file
368
+ * trimmed to start at the fade boundary.
369
+ */
370
+ url: string;
371
+ /**
372
+ * The initial seek of the replacement input, in seconds relative to it.
373
+ * When omitted, the input is assumed to start exactly at the fade boundary.
374
+ */
375
+ seek?: number;
376
+ /**
377
+ * FFmpeg input options of the replacement input. They replace the default
378
+ * input options, which are tuned for http(s) streaming urls.
379
+ */
380
+ inputArgs?: FFmpegArg;
346
381
  };
382
+ /**
383
+ * The {@link CrossfadeOptions.preparePrimaryInput} adapter type.
384
+ */
385
+ type PrepareCrossfadeInput = (url: string, boundary: {
386
+ seek: number;
387
+ song: Song;
388
+ }) => Awaitable<PreparedCrossfadeInput | null | undefined>;
347
389
  /**
348
390
  * DisTube options
349
391
  */
@@ -447,6 +489,9 @@ type DisTubeOptions = {
447
489
  * - `curve`: the FFmpeg `acrossfade` fade curve (@default "qsin")
448
490
  * - `warmup`: seconds before the fade window when the transition stream is
449
491
  * spawned to warm up its buffers (@default 5)
492
+ * - `preparePrimaryInput`: an optional adapter preparing the fading-out
493
+ * song's FFmpeg input before the transition stream is spawned. Useful for
494
+ * streaming inputs that cannot fast-seek ({@link CrossfadeOptions.preparePrimaryInput}).
450
495
  * @default { duration: 0, curve: "qsin", warmup: 5 }
451
496
  */
452
497
  crossfade?: {
@@ -462,6 +507,11 @@ type DisTubeOptions = {
462
507
  * Seconds before the fade window when the transition stream is spawned.
463
508
  */
464
509
  warmup?: number;
510
+ /**
511
+ * Optional adapter preparing the fading-out song's FFmpeg input
512
+ * ({@link CrossfadeOptions.preparePrimaryInput}).
513
+ */
514
+ preparePrimaryInput?: PrepareCrossfadeInput;
465
515
  };
466
516
  /**
467
517
  * FFmpeg options
@@ -1408,6 +1458,14 @@ interface CrossfadeStreamOptions {
1408
1458
  * @default "qsin"
1409
1459
  */
1410
1460
  inCurve?: string;
1461
+ /**
1462
+ * FFmpeg input options of the first (fading out) input. When set, they
1463
+ * replace the default input options, which are tuned for http(s) streaming
1464
+ * urls. Use it for inputs that need different flags — a local playlist
1465
+ * file, for example, requires the `protocol_whitelist` option and no
1466
+ * reconnect flags.
1467
+ */
1468
+ primaryInputArgs?: FFmpegArg;
1411
1469
  }
1412
1470
  /**
1413
1471
  * Check that FFmpeg is installed and can be spawned.
@@ -1444,6 +1502,15 @@ declare class DisTubeStream extends TypedEmitter<{
1444
1502
  * The seek time in seconds that this stream started from
1445
1503
  */
1446
1504
  readonly seekTime: number;
1505
+ /**
1506
+ * Whether FFmpeg has already produced audio that is waiting to be read.
1507
+ *
1508
+ * The stream is spawned ahead of the playback (crossfade warmup) and the
1509
+ * pipe backs up once its buffers are full, so this is `true` as soon as the
1510
+ * first frame exists. A stream that is still `false` at playback time would
1511
+ * leave the audio player buffering until FFmpeg catches up.
1512
+ */
1513
+ get hasBufferedAudio(): boolean;
1447
1514
  /**
1448
1515
  * Create a DisTubeStream to play with {@link DisTubeVoice}
1449
1516
  * @param url - Stream URL
@@ -3616,4 +3683,4 @@ declare const checkEncryptionLibraries: () => Promise<boolean>;
3616
3683
  // @ts-ignore
3617
3684
  export = DisTube;
3618
3685
  export { AUDIO_CHANNELS, AUDIO_SAMPLE_RATE, BaseManager, CROSSFADE_DRIFT_TOLERANCE_SECS, DEFAULT_CROSSFADE_CURVE, DEFAULT_CROSSFADE_DURATION_SECS, DEFAULT_CROSSFADE_WARMUP_SECS, DEFAULT_PRELOAD_THRESHOLD_SECS, DEFAULT_VOLUME, DisTube, DisTubeBase, DisTubeError, DisTubeHandler, DisTubeStream, DisTubeVoice, DisTubeVoiceManager, Events, ExtractorPlugin, FilterManager, GuildIdManager, HTTP_REDIRECT_CODES, InfoExtractorPlugin, JOIN_TIMEOUT_MS, MAX_CROSSFADE_DURATION_SECS, MAX_REDIRECT_DEPTH, Options, PlayableExtractorPlugin, Playlist, Plugin, PluginError, PluginErrorCode, PluginType, Queue, QueueManager, RECONNECT_MAX_ATTEMPTS, RECONNECT_TIMEOUT_MS, RepeatMode, STUCK_THRESHOLD_MS, SearchResult, SearchResultType, Song, StreamServer, TaskQueue, checkEncryptionLibraries, checkFFmpeg, checkIntents, checkInvalidKey, defaultFilters, defaultOptions, formatDuration, handleUnhandledError, isClientInstance, isGuildInstance, isMemberInstance, isMessageInstance, isNsfwChannel, isObject, isSnowflake, isSupportedVoiceChannel, isTextChannelInstance, isTruthy, isURL, isVoiceChannelEmpty, objectKeys, resolveGuildId, version };
3619
- export type { Awaitable, CrossfadeOptions, CrossfadeStreamOptions, CustomPlaylistOptions, DisTubeEvents, DisTubeOptions, DisTubePlugin, DisTubeVoiceEvents, Disposable, FFmpegArg, FFmpegArgs, FFmpegOptions, Falsy, Filter, FilterResolvable, Filters, GuildIdResolvable, JumpOptions, KeyOf, PlayHandlerOptions, PlayOptions, PlaylistInfo, PreloadOptions, RelatedSong, ResolveOptions, ResolvePlaylistOptions, SearchOptions, SongInfo, StreamHandler, StreamOptions, StreamRequest, StreamResponse, StreamServerOptions, TypedDisTubeEvents, UnhandledErrorBehavior };
3686
+ export type { Awaitable, CrossfadeOptions, CrossfadeStreamOptions, CustomPlaylistOptions, DisTubeEvents, DisTubeOptions, DisTubePlugin, DisTubeVoiceEvents, Disposable, FFmpegArg, FFmpegArgs, FFmpegOptions, Falsy, Filter, FilterResolvable, Filters, GuildIdResolvable, JumpOptions, KeyOf, PlayHandlerOptions, PlayOptions, PlaylistInfo, PreloadOptions, PrepareCrossfadeInput, PreparedCrossfadeInput, RelatedSong, ResolveOptions, ResolvePlaylistOptions, SearchOptions, SongInfo, StreamHandler, StreamOptions, StreamRequest, StreamResponse, StreamServerOptions, TypedDisTubeEvents, UnhandledErrorBehavior };
package/dist/index.js CHANGED
@@ -102,7 +102,7 @@ __export(index_exports, {
102
102
  module.exports = __toCommonJS(index_exports);
103
103
 
104
104
  // src/constant.ts
105
- var version = "6.2.0";
105
+ var version = "6.2.2";
106
106
  var AUDIO_SAMPLE_RATE = 48e3;
107
107
  var AUDIO_CHANNELS = 2;
108
108
  var DEFAULT_VOLUME = 50;
@@ -2641,8 +2641,8 @@ var Options = class {
2641
2641
  if (typeof value !== "object" || Array.isArray(value)) {
2642
2642
  throw new DisTubeError("INVALID_TYPE", "object", value, "DisTubeOptions.crossfade");
2643
2643
  }
2644
- checkInvalidKey(value, ["duration", "curve", "warmup"], "DisTubeOptions.crossfade");
2645
- const { duration, curve, warmup } = value;
2644
+ checkInvalidKey(value, ["duration", "curve", "warmup", "preparePrimaryInput"], "DisTubeOptions.crossfade");
2645
+ const { duration, curve, warmup, preparePrimaryInput } = value;
2646
2646
  if (duration !== void 0 && (typeof duration !== "number" || Number.isNaN(duration) || duration < 0)) {
2647
2647
  throw new DisTubeError(
2648
2648
  "INVALID_TYPE",
@@ -2665,10 +2665,19 @@ var Options = class {
2665
2665
  if (warmup !== void 0 && (typeof warmup !== "number" || Number.isNaN(warmup) || warmup < 0)) {
2666
2666
  throw new DisTubeError("INVALID_TYPE", "number >= 0", warmup, "DisTubeOptions.crossfade.warmup");
2667
2667
  }
2668
+ if (preparePrimaryInput !== void 0 && typeof preparePrimaryInput !== "function") {
2669
+ throw new DisTubeError(
2670
+ "INVALID_TYPE",
2671
+ "function",
2672
+ preparePrimaryInput,
2673
+ "DisTubeOptions.crossfade.preparePrimaryInput"
2674
+ );
2675
+ }
2668
2676
  return {
2669
2677
  duration: duration ?? DEFAULT_CROSSFADE_DURATION_SECS,
2670
2678
  curve: curve ?? DEFAULT_CROSSFADE_CURVE,
2671
- warmup: warmup ?? DEFAULT_CROSSFADE_WARMUP_SECS
2679
+ warmup: warmup ?? DEFAULT_CROSSFADE_WARMUP_SECS,
2680
+ preparePrimaryInput
2672
2681
  };
2673
2682
  }
2674
2683
  #validateOptions(options = this) {
@@ -2793,6 +2802,17 @@ var DisTubeStream = class extends import_tiny_typed_emitter2.TypedEmitter {
2793
2802
  * The seek time in seconds that this stream started from
2794
2803
  */
2795
2804
  seekTime;
2805
+ /**
2806
+ * Whether FFmpeg has already produced audio that is waiting to be read.
2807
+ *
2808
+ * The stream is spawned ahead of the playback (crossfade warmup) and the
2809
+ * pipe backs up once its buffers are full, so this is `true` as soon as the
2810
+ * first frame exists. A stream that is still `false` at playback time would
2811
+ * leave the audio player buffering until FFmpeg catches up.
2812
+ */
2813
+ get hasBufferedAudio() {
2814
+ return this.stream.readableLength > 0;
2815
+ }
2796
2816
  /**
2797
2817
  * Create a DisTubeStream to play with {@link DisTubeVoice}
2798
2818
  * @param url - Stream URL
@@ -2880,13 +2900,15 @@ var DisTubeStream = class extends import_tiny_typed_emitter2.TypedEmitter {
2880
2900
  const extraFilters = typeof outputArgs.af === "string" ? outputArgs.af : Array.isArray(outputArgs.af) ? outputArgs.af.filter(Boolean).join(",") : void 0;
2881
2901
  delete outputArgs.af;
2882
2902
  const primaryOpts = {
2883
- reconnect: 1,
2884
- reconnect_streamed: 1,
2885
- reconnect_delay_max: 5,
2886
- analyzeduration: 0,
2887
2903
  hide_banner: true,
2888
2904
  ...ffmpeg.args.global,
2889
- ...ffmpeg.args.input,
2905
+ ...crossfade.primaryInputArgs ?? {
2906
+ reconnect: 1,
2907
+ reconnect_streamed: 1,
2908
+ reconnect_delay_max: 5,
2909
+ analyzeduration: 0,
2910
+ ...ffmpeg.args.input
2911
+ },
2890
2912
  ss: typeof primarySeek === "number" && primarySeek > 0 ? primarySeek.toString() : null,
2891
2913
  i: url
2892
2914
  };
@@ -3465,7 +3487,28 @@ var QueueManager = class extends GuildIdManager {
3465
3487
  if (!primary?.stream.playFromSource || !primary.stream.url) return;
3466
3488
  if (!secondary?.stream.playFromSource || !secondary.stream.url) return;
3467
3489
  this.debug(`[${queue.id}] Warming up crossfade stream: ${song} -> ${nextSong}`);
3468
- transitionStream = new DisTubeStream(primary.stream.url, {
3490
+ const boundarySeek = Math.max(0, songDuration - fade);
3491
+ let primaryUrl = primary.stream.url;
3492
+ let primarySeek = boundarySeek;
3493
+ let primaryInputArgs;
3494
+ const prepare = this.options.crossfade.preparePrimaryInput;
3495
+ if (prepare) {
3496
+ try {
3497
+ const prepared = await prepare(primary.stream.url, { seek: boundarySeek, song });
3498
+ if (prepared?.url) {
3499
+ primaryUrl = prepared.url;
3500
+ if (typeof prepared.seek === "number" && Number.isFinite(prepared.seek) && prepared.seek >= 0) {
3501
+ primarySeek = prepared.seek;
3502
+ }
3503
+ primaryInputArgs = prepared.inputArgs;
3504
+ this.debug(`[${queue.id}] Crossfade primary input prepared by the adapter`);
3505
+ }
3506
+ } catch (e) {
3507
+ const errorMessage = e instanceof Error ? e.message : String(e);
3508
+ this.debug(`[${queue.id}] Crossfade primary input preparation failed: ${errorMessage}`);
3509
+ }
3510
+ }
3511
+ transitionStream = new DisTubeStream(primaryUrl, {
3469
3512
  ffmpeg: {
3470
3513
  path: this.options.ffmpeg.path,
3471
3514
  args: {
@@ -3476,10 +3519,11 @@ var QueueManager = class extends GuildIdManager {
3476
3519
  },
3477
3520
  crossfade: {
3478
3521
  duration: fade,
3479
- primarySeek: Math.max(0, songDuration - fade),
3522
+ primarySeek,
3480
3523
  secondaryUrl: secondary.stream.url,
3481
3524
  inCurve: this.options.crossfade.curve,
3482
- outCurve: this.options.crossfade.curve
3525
+ outCurve: this.options.crossfade.curve,
3526
+ primaryInputArgs
3483
3527
  }
3484
3528
  });
3485
3529
  transitionStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] [crossfade] ${data}`));
@@ -3489,7 +3533,7 @@ var QueueManager = class extends GuildIdManager {
3489
3533
  }
3490
3534
  queue._crossfadeWarmup = transitionStream;
3491
3535
  transitionStream.spawn();
3492
- const boundaryMs = Math.max(0, (remaining - fade) * 1e3);
3536
+ const boundaryMs = Math.max(0, (songDuration - queue.currentTime - fade) * 1e3);
3493
3537
  queue._crossfadeTimeout = setTimeout(() => {
3494
3538
  queue._crossfadeTimeout = void 0;
3495
3539
  void this.#crossfadeSwapTask(queue, transitionStream);
@@ -3527,6 +3571,10 @@ var QueueManager = class extends GuildIdManager {
3527
3571
  if (queue.stopped || queue.paused || queue.songs[0] !== song || queue._crossfadeWarmup !== transitionStream) {
3528
3572
  return abort();
3529
3573
  }
3574
+ if (!transitionStream.hasBufferedAudio) {
3575
+ this.debug(`[${queue.id}] Crossfade aborted: the transition stream is not ready`);
3576
+ return abort();
3577
+ }
3530
3578
  this.debug(`[${queue.id}] Starting crossfade: ${song} -> ${queue.songs.length > 1 ? queue.songs[1] : song}`);
3531
3579
  await queue.voice.play(transitionStream);
3532
3580
  queue._crossfadeWarmup = void 0;