@kattowo/distube 6.1.0 → 6.2.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.d.mts +177 -2
- package/dist/index.d.ts +177 -2
- package/dist/index.js +404 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +399 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34,6 +34,10 @@ __export(index_exports, {
|
|
|
34
34
|
AUDIO_CHANNELS: () => AUDIO_CHANNELS,
|
|
35
35
|
AUDIO_SAMPLE_RATE: () => AUDIO_SAMPLE_RATE,
|
|
36
36
|
BaseManager: () => BaseManager,
|
|
37
|
+
CROSSFADE_DRIFT_TOLERANCE_SECS: () => CROSSFADE_DRIFT_TOLERANCE_SECS,
|
|
38
|
+
DEFAULT_CROSSFADE_CURVE: () => DEFAULT_CROSSFADE_CURVE,
|
|
39
|
+
DEFAULT_CROSSFADE_DURATION_SECS: () => DEFAULT_CROSSFADE_DURATION_SECS,
|
|
40
|
+
DEFAULT_CROSSFADE_WARMUP_SECS: () => DEFAULT_CROSSFADE_WARMUP_SECS,
|
|
37
41
|
DEFAULT_PRELOAD_THRESHOLD_SECS: () => DEFAULT_PRELOAD_THRESHOLD_SECS,
|
|
38
42
|
DEFAULT_VOLUME: () => DEFAULT_VOLUME,
|
|
39
43
|
DisTube: () => DisTube,
|
|
@@ -50,6 +54,7 @@ __export(index_exports, {
|
|
|
50
54
|
HTTP_REDIRECT_CODES: () => HTTP_REDIRECT_CODES,
|
|
51
55
|
InfoExtractorPlugin: () => InfoExtractorPlugin,
|
|
52
56
|
JOIN_TIMEOUT_MS: () => JOIN_TIMEOUT_MS,
|
|
57
|
+
MAX_CROSSFADE_DURATION_SECS: () => MAX_CROSSFADE_DURATION_SECS,
|
|
53
58
|
MAX_REDIRECT_DEPTH: () => MAX_REDIRECT_DEPTH,
|
|
54
59
|
Options: () => Options,
|
|
55
60
|
PlayableExtractorPlugin: () => PlayableExtractorPlugin,
|
|
@@ -97,7 +102,7 @@ __export(index_exports, {
|
|
|
97
102
|
module.exports = __toCommonJS(index_exports);
|
|
98
103
|
|
|
99
104
|
// src/constant.ts
|
|
100
|
-
var version = "6.
|
|
105
|
+
var version = "6.2.0";
|
|
101
106
|
var AUDIO_SAMPLE_RATE = 48e3;
|
|
102
107
|
var AUDIO_CHANNELS = 2;
|
|
103
108
|
var DEFAULT_VOLUME = 50;
|
|
@@ -125,6 +130,11 @@ var defaultFilters = {
|
|
|
125
130
|
earwax: "earwax"
|
|
126
131
|
};
|
|
127
132
|
var DEFAULT_PRELOAD_THRESHOLD_SECS = 15;
|
|
133
|
+
var DEFAULT_CROSSFADE_DURATION_SECS = 0;
|
|
134
|
+
var MAX_CROSSFADE_DURATION_SECS = 12;
|
|
135
|
+
var DEFAULT_CROSSFADE_CURVE = "qsin";
|
|
136
|
+
var DEFAULT_CROSSFADE_WARMUP_SECS = 5;
|
|
137
|
+
var CROSSFADE_DRIFT_TOLERANCE_SECS = 0.75;
|
|
128
138
|
var defaultOptions = {
|
|
129
139
|
plugins: [],
|
|
130
140
|
emitNewSongOnly: false,
|
|
@@ -135,7 +145,12 @@ var defaultOptions = {
|
|
|
135
145
|
joinNewVoiceChannel: true,
|
|
136
146
|
unhandledError: "log",
|
|
137
147
|
stuckThreshold: STUCK_THRESHOLD_MS,
|
|
138
|
-
preload: false
|
|
148
|
+
preload: false,
|
|
149
|
+
crossfade: {
|
|
150
|
+
duration: DEFAULT_CROSSFADE_DURATION_SECS,
|
|
151
|
+
curve: DEFAULT_CROSSFADE_CURVE,
|
|
152
|
+
warmup: DEFAULT_CROSSFADE_WARMUP_SECS
|
|
153
|
+
}
|
|
139
154
|
};
|
|
140
155
|
|
|
141
156
|
// src/core/DisTubeBase.ts
|
|
@@ -858,7 +873,7 @@ var DisTubeVoice = class extends import_tiny_typed_emitter.TypedEmitter {
|
|
|
858
873
|
if (this.audioPlayer.state.status !== import_voice.AudioPlayerStatus.Paused) {
|
|
859
874
|
this.audioPlayer.play(dtStream.audioResource);
|
|
860
875
|
this.stream?.kill();
|
|
861
|
-
dtStream.spawn();
|
|
876
|
+
if (!dtStream.process) dtStream.spawn();
|
|
862
877
|
} else if (!this.pausingStream) {
|
|
863
878
|
this.pausingStream = this.stream;
|
|
864
879
|
}
|
|
@@ -1383,11 +1398,24 @@ var Queue = class extends DisTubeBase {
|
|
|
1383
1398
|
* @internal
|
|
1384
1399
|
*/
|
|
1385
1400
|
_preloadGeneration = 0;
|
|
1401
|
+
#crossfade;
|
|
1402
|
+
/**
|
|
1403
|
+
* Crossfade boundary timer
|
|
1404
|
+
* @internal
|
|
1405
|
+
*/
|
|
1406
|
+
_crossfadeTimeout;
|
|
1407
|
+
/**
|
|
1408
|
+
* The merged transition stream that is warming up before the fade window
|
|
1409
|
+
* starts
|
|
1410
|
+
* @internal
|
|
1411
|
+
*/
|
|
1412
|
+
_crossfadeWarmup;
|
|
1386
1413
|
/**
|
|
1387
1414
|
* Clear any preloaded song and cancel pending preload timer.
|
|
1388
1415
|
* @internal
|
|
1389
1416
|
*/
|
|
1390
1417
|
_clearPreload() {
|
|
1418
|
+
this._crossfadeCancel();
|
|
1391
1419
|
this._preloadGeneration++;
|
|
1392
1420
|
if (this._preloadTimeout) {
|
|
1393
1421
|
clearTimeout(this._preloadTimeout);
|
|
@@ -1399,6 +1427,20 @@ var Queue = class extends DisTubeBase {
|
|
|
1399
1427
|
}
|
|
1400
1428
|
this._preloadedSong = void 0;
|
|
1401
1429
|
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Cancel a pending crossfade and kill its warming-up transition stream.
|
|
1432
|
+
* @internal
|
|
1433
|
+
*/
|
|
1434
|
+
_crossfadeCancel() {
|
|
1435
|
+
if (this._crossfadeTimeout) {
|
|
1436
|
+
clearTimeout(this._crossfadeTimeout);
|
|
1437
|
+
this._crossfadeTimeout = void 0;
|
|
1438
|
+
}
|
|
1439
|
+
if (this._crossfadeWarmup) {
|
|
1440
|
+
this._crossfadeWarmup.kill();
|
|
1441
|
+
this._crossfadeWarmup = void 0;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1402
1444
|
/**
|
|
1403
1445
|
* Create a queue for the guild
|
|
1404
1446
|
* @param distube - DisTube
|
|
@@ -1427,6 +1469,7 @@ var Queue = class extends DisTubeBase {
|
|
|
1427
1469
|
input: { ...this.options.ffmpeg.args.input },
|
|
1428
1470
|
output: { ...this.options.ffmpeg.args.output }
|
|
1429
1471
|
};
|
|
1472
|
+
this.#crossfade = Number(this.options.crossfade?.duration) || 0;
|
|
1430
1473
|
this.#initialized = true;
|
|
1431
1474
|
}
|
|
1432
1475
|
#addToPreviousSongs(songs) {
|
|
@@ -1485,6 +1528,28 @@ var Queue = class extends DisTubeBase {
|
|
|
1485
1528
|
get currentTime() {
|
|
1486
1529
|
return this.voice.playbackTime;
|
|
1487
1530
|
}
|
|
1531
|
+
/**
|
|
1532
|
+
* The crossfade duration (in seconds) between this song and the next one.
|
|
1533
|
+
*
|
|
1534
|
+
* `0` (the default) disables the crossfade. Changing it takes effect for the
|
|
1535
|
+
* next transition; call {@link Queue#scheduleCrossfade} to re-arm the
|
|
1536
|
+
* transition for the currently playing song immediately.
|
|
1537
|
+
* @throws {@link DisTubeError} When the value is not a number between `0`
|
|
1538
|
+
* and the maximum crossfade duration
|
|
1539
|
+
*/
|
|
1540
|
+
get crossfade() {
|
|
1541
|
+
return this.#crossfade;
|
|
1542
|
+
}
|
|
1543
|
+
set crossfade(value) {
|
|
1544
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
1545
|
+
throw new DisTubeError("INVALID_TYPE", "number", value, "Queue.crossfade");
|
|
1546
|
+
}
|
|
1547
|
+
if (value < 0) throw new DisTubeError("NUMBER_COMPARE", "Crossfade", "bigger or equal to", 0);
|
|
1548
|
+
if (value > MAX_CROSSFADE_DURATION_SECS) {
|
|
1549
|
+
throw new DisTubeError("NUMBER_COMPARE", "Crossfade", "smaller or equal to", MAX_CROSSFADE_DURATION_SECS);
|
|
1550
|
+
}
|
|
1551
|
+
this.#crossfade = value;
|
|
1552
|
+
}
|
|
1488
1553
|
/**
|
|
1489
1554
|
* Formatted {@link Queue#currentTime} string.
|
|
1490
1555
|
*/
|
|
@@ -1586,6 +1651,7 @@ var Queue = class extends DisTubeBase {
|
|
|
1586
1651
|
this.voice.unpause();
|
|
1587
1652
|
this.emit("resumeSong" /* RESUME_SONG */, this, this.songs[0]);
|
|
1588
1653
|
this.queues?.schedulePreload?.(this);
|
|
1654
|
+
this.queues?.scheduleCrossfade?.(this);
|
|
1589
1655
|
return this;
|
|
1590
1656
|
} finally {
|
|
1591
1657
|
this._taskQueue.resolve();
|
|
@@ -1842,6 +1908,17 @@ var Queue = class extends DisTubeBase {
|
|
|
1842
1908
|
if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
|
|
1843
1909
|
return this.queues.playSong(this, emitPlaySong);
|
|
1844
1910
|
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Re-arm the crossfade into the next song for the currently playing one.
|
|
1913
|
+
*
|
|
1914
|
+
* Useful after changing {@link Queue#crossfade} while a song is playing: the
|
|
1915
|
+
* fade may still fit into the remaining time. This is called automatically
|
|
1916
|
+
* after every song switch.
|
|
1917
|
+
* @returns Nothing
|
|
1918
|
+
*/
|
|
1919
|
+
scheduleCrossfade() {
|
|
1920
|
+
this.queues.scheduleCrossfade(this);
|
|
1921
|
+
}
|
|
1845
1922
|
/**
|
|
1846
1923
|
* Restart the current stream with the current FFmpeg arguments, keeping the
|
|
1847
1924
|
* playback position.
|
|
@@ -2494,6 +2571,11 @@ var Options = class {
|
|
|
2494
2571
|
* @default { enabled: false, threshold: 15 }
|
|
2495
2572
|
*/
|
|
2496
2573
|
preload;
|
|
2574
|
+
/**
|
|
2575
|
+
* The crossfade options of the DisTube instance.
|
|
2576
|
+
* @default { duration: 0, curve: "qsin", warmup: 5 }
|
|
2577
|
+
*/
|
|
2578
|
+
crossfade;
|
|
2497
2579
|
/**
|
|
2498
2580
|
* The FFmpeg options of the DisTube instance.
|
|
2499
2581
|
*/
|
|
@@ -2520,6 +2602,7 @@ var Options = class {
|
|
|
2520
2602
|
this.unhandledError = opts.unhandledError;
|
|
2521
2603
|
this.stuckThreshold = opts.stuckThreshold;
|
|
2522
2604
|
this.preload = this.#preloadOption(opts.preload);
|
|
2605
|
+
this.crossfade = this.#crossfadeOption(opts.crossfade);
|
|
2523
2606
|
this.ffmpeg = this.#ffmpegOption(options);
|
|
2524
2607
|
checkInvalidKey(opts, this, "DisTubeOptions");
|
|
2525
2608
|
this.#validateOptions();
|
|
@@ -2547,6 +2630,47 @@ var Options = class {
|
|
|
2547
2630
|
threshold: threshold ?? DEFAULT_PRELOAD_THRESHOLD_SECS
|
|
2548
2631
|
};
|
|
2549
2632
|
}
|
|
2633
|
+
#crossfadeOption(value) {
|
|
2634
|
+
if (value === void 0) {
|
|
2635
|
+
return {
|
|
2636
|
+
duration: DEFAULT_CROSSFADE_DURATION_SECS,
|
|
2637
|
+
curve: DEFAULT_CROSSFADE_CURVE,
|
|
2638
|
+
warmup: DEFAULT_CROSSFADE_WARMUP_SECS
|
|
2639
|
+
};
|
|
2640
|
+
}
|
|
2641
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
2642
|
+
throw new DisTubeError("INVALID_TYPE", "object", value, "DisTubeOptions.crossfade");
|
|
2643
|
+
}
|
|
2644
|
+
checkInvalidKey(value, ["duration", "curve", "warmup"], "DisTubeOptions.crossfade");
|
|
2645
|
+
const { duration, curve, warmup } = value;
|
|
2646
|
+
if (duration !== void 0 && (typeof duration !== "number" || Number.isNaN(duration) || duration < 0)) {
|
|
2647
|
+
throw new DisTubeError(
|
|
2648
|
+
"INVALID_TYPE",
|
|
2649
|
+
`number >= 0 (max ${MAX_CROSSFADE_DURATION_SECS})`,
|
|
2650
|
+
duration,
|
|
2651
|
+
"DisTubeOptions.crossfade.duration"
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
if (duration !== void 0 && duration > MAX_CROSSFADE_DURATION_SECS) {
|
|
2655
|
+
throw new DisTubeError(
|
|
2656
|
+
"NUMBER_COMPARE",
|
|
2657
|
+
"Crossfade duration",
|
|
2658
|
+
"smaller or equal to",
|
|
2659
|
+
MAX_CROSSFADE_DURATION_SECS
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
if (curve !== void 0 && (typeof curve !== "string" || !curve)) {
|
|
2663
|
+
throw new DisTubeError("INVALID_TYPE", "string", curve, "DisTubeOptions.crossfade.curve");
|
|
2664
|
+
}
|
|
2665
|
+
if (warmup !== void 0 && (typeof warmup !== "number" || Number.isNaN(warmup) || warmup < 0)) {
|
|
2666
|
+
throw new DisTubeError("INVALID_TYPE", "number >= 0", warmup, "DisTubeOptions.crossfade.warmup");
|
|
2667
|
+
}
|
|
2668
|
+
return {
|
|
2669
|
+
duration: duration ?? DEFAULT_CROSSFADE_DURATION_SECS,
|
|
2670
|
+
curve: curve ?? DEFAULT_CROSSFADE_CURVE,
|
|
2671
|
+
warmup: warmup ?? DEFAULT_CROSSFADE_WARMUP_SECS
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2550
2674
|
#validateOptions(options = this) {
|
|
2551
2675
|
const booleanOptions = /* @__PURE__ */ new Set([
|
|
2552
2676
|
"emitNewSongOnly",
|
|
@@ -2558,7 +2682,7 @@ var Options = class {
|
|
|
2558
2682
|
]);
|
|
2559
2683
|
const numberOptions = /* @__PURE__ */ new Set(["stuckThreshold"]);
|
|
2560
2684
|
const stringOptions = /* @__PURE__ */ new Set(["unhandledError"]);
|
|
2561
|
-
const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg", "preload"]);
|
|
2685
|
+
const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg", "preload", "crossfade"]);
|
|
2562
2686
|
const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
|
|
2563
2687
|
for (const [key, value] of Object.entries(options)) {
|
|
2564
2688
|
if (value === void 0 && optionalOptions.has(key)) continue;
|
|
@@ -2676,8 +2800,40 @@ var DisTubeStream = class extends import_tiny_typed_emitter2.TypedEmitter {
|
|
|
2676
2800
|
*/
|
|
2677
2801
|
constructor(url, options) {
|
|
2678
2802
|
super();
|
|
2679
|
-
const { ffmpeg, seek } = options;
|
|
2803
|
+
const { ffmpeg, seek, crossfade } = options;
|
|
2680
2804
|
this.seekTime = typeof seek === "number" && seek > 0 ? seek : 0;
|
|
2805
|
+
this.#ffmpegPath = ffmpeg.path;
|
|
2806
|
+
this.#opts = crossfade ? this.#crossfadeArgs(url, ffmpeg, crossfade) : this.#singleInputArgs(url, ffmpeg, seek);
|
|
2807
|
+
this.stream = new VolumeTransformer();
|
|
2808
|
+
this.stream.on("close", () => this.kill()).on("error", (err) => {
|
|
2809
|
+
this.debug(`[stream] error: ${err.message}`);
|
|
2810
|
+
this.emit("error", err);
|
|
2811
|
+
}).on("finish", () => this.debug("[stream] log: stream finished"));
|
|
2812
|
+
this.audioResource = (0, import_voice2.createAudioResource)(this.stream, {
|
|
2813
|
+
inputType: import_voice2.StreamType.Raw,
|
|
2814
|
+
inlineVolume: false
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
#flattenArgs(args) {
|
|
2818
|
+
return Object.entries(args).flatMap(
|
|
2819
|
+
([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
|
|
2820
|
+
).flat();
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Translate a `file:` URL into a local filesystem path that FFmpeg accepts.
|
|
2824
|
+
*
|
|
2825
|
+
* `file:///C:/x` loses its drive-letter leading slash on Windows (`C:/x`),
|
|
2826
|
+
* UNC shares keep their `//host` prefix, POSIX paths stay untouched, and
|
|
2827
|
+
* other protocols return `undefined`.
|
|
2828
|
+
*/
|
|
2829
|
+
#localPath(url) {
|
|
2830
|
+
const fileUrl = new URL(url);
|
|
2831
|
+
if (fileUrl.protocol !== "file:") return void 0;
|
|
2832
|
+
const pathname = decodeURIComponent(fileUrl.pathname);
|
|
2833
|
+
if (fileUrl.hostname) return `//${fileUrl.hostname}${pathname}`;
|
|
2834
|
+
return /^\/[A-Za-z]:\//.test(pathname) ? pathname.slice(1) : pathname;
|
|
2835
|
+
}
|
|
2836
|
+
#singleInputArgs(url, ffmpeg, seek) {
|
|
2681
2837
|
const opts = {
|
|
2682
2838
|
reconnect: 1,
|
|
2683
2839
|
reconnect_streamed: 1,
|
|
@@ -2693,29 +2849,91 @@ var DisTubeStream = class extends import_tiny_typed_emitter2.TypedEmitter {
|
|
|
2693
2849
|
f: "s16le"
|
|
2694
2850
|
};
|
|
2695
2851
|
if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
|
|
2696
|
-
const
|
|
2697
|
-
if (
|
|
2852
|
+
const localPath = this.#localPath(url);
|
|
2853
|
+
if (localPath !== void 0) {
|
|
2698
2854
|
opts.reconnect = null;
|
|
2699
2855
|
opts.reconnect_streamed = null;
|
|
2700
2856
|
opts.reconnect_delay_max = null;
|
|
2701
|
-
opts.i =
|
|
2857
|
+
opts.i = localPath;
|
|
2702
2858
|
}
|
|
2703
|
-
this.#
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2859
|
+
return [...this.#flattenArgs(opts), "pipe:1"];
|
|
2860
|
+
}
|
|
2861
|
+
/**
|
|
2862
|
+
* Build the arguments of a crossfade transition stream: a single FFmpeg
|
|
2863
|
+
* process with two inputs (the fading out song seeked to its final
|
|
2864
|
+
* `duration` seconds and the fading in song from its beginning) mixed by an
|
|
2865
|
+
* `acrossfade` filter graph.
|
|
2866
|
+
*
|
|
2867
|
+
* Both inputs are normalized (48kHz stereo s16) before the fade, and the
|
|
2868
|
+
* queue's `af` filter chain, when present, is folded into the filter complex
|
|
2869
|
+
* after the fade (FFmpeg cannot combine `-af` with `-filter_complex`).
|
|
2870
|
+
*/
|
|
2871
|
+
#crossfadeArgs(url, ffmpeg, crossfade) {
|
|
2872
|
+
const {
|
|
2873
|
+
duration,
|
|
2874
|
+
primarySeek,
|
|
2875
|
+
secondaryUrl,
|
|
2876
|
+
outCurve = DEFAULT_CROSSFADE_CURVE,
|
|
2877
|
+
inCurve = DEFAULT_CROSSFADE_CURVE
|
|
2878
|
+
} = crossfade;
|
|
2879
|
+
const outputArgs = { ...ffmpeg.args.output };
|
|
2880
|
+
const extraFilters = typeof outputArgs.af === "string" ? outputArgs.af : Array.isArray(outputArgs.af) ? outputArgs.af.filter(Boolean).join(",") : void 0;
|
|
2881
|
+
delete outputArgs.af;
|
|
2882
|
+
const primaryOpts = {
|
|
2883
|
+
reconnect: 1,
|
|
2884
|
+
reconnect_streamed: 1,
|
|
2885
|
+
reconnect_delay_max: 5,
|
|
2886
|
+
analyzeduration: 0,
|
|
2887
|
+
hide_banner: true,
|
|
2888
|
+
...ffmpeg.args.global,
|
|
2889
|
+
...ffmpeg.args.input,
|
|
2890
|
+
ss: typeof primarySeek === "number" && primarySeek > 0 ? primarySeek.toString() : null,
|
|
2891
|
+
i: url
|
|
2892
|
+
};
|
|
2893
|
+
const secondaryOpts = {
|
|
2894
|
+
reconnect: 1,
|
|
2895
|
+
reconnect_streamed: 1,
|
|
2896
|
+
reconnect_delay_max: 5,
|
|
2897
|
+
analyzeduration: 0,
|
|
2898
|
+
...ffmpeg.args.input,
|
|
2899
|
+
i: secondaryUrl
|
|
2900
|
+
};
|
|
2901
|
+
const primaryLocal = this.#localPath(url);
|
|
2902
|
+
if (primaryLocal !== void 0) {
|
|
2903
|
+
primaryOpts.reconnect = null;
|
|
2904
|
+
primaryOpts.reconnect_streamed = null;
|
|
2905
|
+
primaryOpts.reconnect_delay_max = null;
|
|
2906
|
+
primaryOpts.i = primaryLocal;
|
|
2907
|
+
}
|
|
2908
|
+
const secondaryLocal = this.#localPath(secondaryUrl);
|
|
2909
|
+
if (secondaryLocal !== void 0) {
|
|
2910
|
+
secondaryOpts.reconnect = null;
|
|
2911
|
+
secondaryOpts.reconnect_streamed = null;
|
|
2912
|
+
secondaryOpts.reconnect_delay_max = null;
|
|
2913
|
+
secondaryOpts.i = secondaryLocal;
|
|
2914
|
+
}
|
|
2915
|
+
const normalize = `aresample=${AUDIO_SAMPLE_RATE},aformat=sample_fmts=s16:channel_layouts=stereo`;
|
|
2916
|
+
const fade = `[a0][a1]acrossfade=d=${duration}:c1=${outCurve}:c2=${inCurve}[xf]`;
|
|
2917
|
+
const graph = [
|
|
2918
|
+
`[0:a]${normalize}[a0]`,
|
|
2919
|
+
`[1:a]${normalize}[a1]`,
|
|
2920
|
+
fade,
|
|
2921
|
+
...extraFilters ? [`[xf]${extraFilters}[aout]`] : []
|
|
2922
|
+
].join(";");
|
|
2923
|
+
const outputOpts = {
|
|
2924
|
+
ar: AUDIO_SAMPLE_RATE,
|
|
2925
|
+
ac: AUDIO_CHANNELS,
|
|
2926
|
+
...outputArgs,
|
|
2927
|
+
filter_complex: graph,
|
|
2928
|
+
map: extraFilters ? "[aout]" : "[xf]",
|
|
2929
|
+
f: "s16le"
|
|
2930
|
+
};
|
|
2931
|
+
return [
|
|
2932
|
+
...this.#flattenArgs(primaryOpts),
|
|
2933
|
+
...this.#flattenArgs(secondaryOpts),
|
|
2934
|
+
...this.#flattenArgs(outputOpts),
|
|
2708
2935
|
"pipe:1"
|
|
2709
2936
|
];
|
|
2710
|
-
this.stream = new VolumeTransformer();
|
|
2711
|
-
this.stream.on("close", () => this.kill()).on("error", (err) => {
|
|
2712
|
-
this.debug(`[stream] error: ${err.message}`);
|
|
2713
|
-
this.emit("error", err);
|
|
2714
|
-
}).on("finish", () => this.debug("[stream] log: stream finished"));
|
|
2715
|
-
this.audioResource = (0, import_voice2.createAudioResource)(this.stream, {
|
|
2716
|
-
inputType: import_voice2.StreamType.Raw,
|
|
2717
|
-
inlineVolume: false
|
|
2718
|
-
});
|
|
2719
2937
|
}
|
|
2720
2938
|
/**
|
|
2721
2939
|
* Spawn the FFmpeg process that decodes the audio.
|
|
@@ -3081,6 +3299,7 @@ var QueueManager = class extends GuildIdManager {
|
|
|
3081
3299
|
await queue.voice.play(dtStream);
|
|
3082
3300
|
if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
|
|
3083
3301
|
this.schedulePreload(queue);
|
|
3302
|
+
this.scheduleCrossfade(queue);
|
|
3084
3303
|
} catch (e) {
|
|
3085
3304
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
3086
3305
|
if (options?.keepSongOnError) {
|
|
@@ -3182,6 +3401,164 @@ var QueueManager = class extends GuildIdManager {
|
|
|
3182
3401
|
this.emit("preloadError" /* PRELOAD_ERROR */, queue, nextSong, error);
|
|
3183
3402
|
}
|
|
3184
3403
|
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Schedule the crossfade into the next song: a single FFmpeg process mixes
|
|
3406
|
+
* the current song (seeked to its final fade window) with the next song, and
|
|
3407
|
+
* replaces the playback stream exactly when the fade window begins.
|
|
3408
|
+
*
|
|
3409
|
+
* Canceled automatically whenever the preloaded state is invalidated
|
|
3410
|
+
* (pause, seek, stop...), as both schedulers share the same generation.
|
|
3411
|
+
* @param queue - The guild queue
|
|
3412
|
+
*/
|
|
3413
|
+
scheduleCrossfade(queue) {
|
|
3414
|
+
queue._crossfadeCancel();
|
|
3415
|
+
if (queue.stopped || queue.paused) return;
|
|
3416
|
+
const currentSong = queue.songs[0];
|
|
3417
|
+
if (!currentSong) return;
|
|
3418
|
+
const songDuration = currentSong.duration ?? 0;
|
|
3419
|
+
if (!songDuration || !Number.isFinite(songDuration)) return;
|
|
3420
|
+
const fade = Math.min(Number(queue.crossfade) || 0, songDuration);
|
|
3421
|
+
if (fade <= 0) return;
|
|
3422
|
+
const nextSong = queue.repeatMode === 1 /* SONG */ ? currentSong : queue.songs[1];
|
|
3423
|
+
if (!nextSong) return;
|
|
3424
|
+
const currentTime = queue.currentTime;
|
|
3425
|
+
if (!Number.isFinite(currentTime)) return;
|
|
3426
|
+
const remaining = songDuration - currentTime;
|
|
3427
|
+
if (remaining <= fade) return;
|
|
3428
|
+
const delayMs = Math.max(0, (remaining - fade - this.options.crossfade.warmup) * 1e3);
|
|
3429
|
+
this.debug(
|
|
3430
|
+
`[${queue.id}] Scheduling crossfade (${fade}s fade) in ${(delayMs / 1e3).toFixed(1)}s: ${currentSong} -> ${nextSong}`
|
|
3431
|
+
);
|
|
3432
|
+
queue._crossfadeTimeout = setTimeout(() => {
|
|
3433
|
+
queue._crossfadeTimeout = void 0;
|
|
3434
|
+
void this.#crossfadeWarmupTask(queue);
|
|
3435
|
+
}, delayMs);
|
|
3436
|
+
queue._crossfadeTimeout.unref?.();
|
|
3437
|
+
}
|
|
3438
|
+
/**
|
|
3439
|
+
* Spawn the merged transition stream ahead of the fade window so both of
|
|
3440
|
+
* its inputs are buffered when the fade must start, then arm the boundary
|
|
3441
|
+
* timer that swaps the playback stream.
|
|
3442
|
+
*/
|
|
3443
|
+
async #crossfadeWarmupTask(queue) {
|
|
3444
|
+
let transitionStream;
|
|
3445
|
+
try {
|
|
3446
|
+
if (queue.stopped || queue.paused) return;
|
|
3447
|
+
const song = queue.songs[0];
|
|
3448
|
+
const generation = queue._preloadGeneration;
|
|
3449
|
+
const repeatSong = queue.repeatMode === 1 /* SONG */;
|
|
3450
|
+
const nextSong = repeatSong ? song : queue.songs[1];
|
|
3451
|
+
if (!song || !nextSong) return;
|
|
3452
|
+
const songDuration = song.duration ?? 0;
|
|
3453
|
+
const fade = Math.min(Number(queue.crossfade) || 0, songDuration);
|
|
3454
|
+
if (fade <= 0) return;
|
|
3455
|
+
const remaining = songDuration - queue.currentTime;
|
|
3456
|
+
if (remaining <= fade) return;
|
|
3457
|
+
if (!repeatSong) {
|
|
3458
|
+
if (this.options.preload.enabled) await this.preloadNextSong(queue);
|
|
3459
|
+
else await this.handler.attachStreamInfo(nextSong);
|
|
3460
|
+
}
|
|
3461
|
+
if (queue.stopped || queue.paused || queue.songs[0] !== song || generation !== queue._preloadGeneration) return;
|
|
3462
|
+
if (repeatSong ? queue.songs[0] !== song : queue.songs[1] !== nextSong) return;
|
|
3463
|
+
const primary = song.stream.playFromSource ? song : song.stream.song;
|
|
3464
|
+
const secondary = repeatSong ? primary : nextSong.stream.playFromSource ? nextSong : nextSong.stream.song;
|
|
3465
|
+
if (!primary?.stream.playFromSource || !primary.stream.url) return;
|
|
3466
|
+
if (!secondary?.stream.playFromSource || !secondary.stream.url) return;
|
|
3467
|
+
this.debug(`[${queue.id}] Warming up crossfade stream: ${song} -> ${nextSong}`);
|
|
3468
|
+
transitionStream = new DisTubeStream(primary.stream.url, {
|
|
3469
|
+
ffmpeg: {
|
|
3470
|
+
path: this.options.ffmpeg.path,
|
|
3471
|
+
args: {
|
|
3472
|
+
global: { ...queue.ffmpegArgs.global },
|
|
3473
|
+
input: { ...queue.ffmpegArgs.input },
|
|
3474
|
+
output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
|
|
3475
|
+
}
|
|
3476
|
+
},
|
|
3477
|
+
crossfade: {
|
|
3478
|
+
duration: fade,
|
|
3479
|
+
primarySeek: Math.max(0, songDuration - fade),
|
|
3480
|
+
secondaryUrl: secondary.stream.url,
|
|
3481
|
+
inCurve: this.options.crossfade.curve,
|
|
3482
|
+
outCurve: this.options.crossfade.curve
|
|
3483
|
+
}
|
|
3484
|
+
});
|
|
3485
|
+
transitionStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] [crossfade] ${data}`));
|
|
3486
|
+
if (queue.stopped || queue.paused || queue.songs[0] !== song || generation !== queue._preloadGeneration) {
|
|
3487
|
+
transitionStream.kill();
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
queue._crossfadeWarmup = transitionStream;
|
|
3491
|
+
transitionStream.spawn();
|
|
3492
|
+
const boundaryMs = Math.max(0, (remaining - fade) * 1e3);
|
|
3493
|
+
queue._crossfadeTimeout = setTimeout(() => {
|
|
3494
|
+
queue._crossfadeTimeout = void 0;
|
|
3495
|
+
void this.#crossfadeSwapTask(queue, transitionStream);
|
|
3496
|
+
}, boundaryMs);
|
|
3497
|
+
queue._crossfadeTimeout.unref?.();
|
|
3498
|
+
} catch (e) {
|
|
3499
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
3500
|
+
this.debug(`[${queue.id}] Crossfade warmup failed, using the regular transition: ${errorMessage}`);
|
|
3501
|
+
if (transitionStream) transitionStream.kill();
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Swap the playing stream for the merged transition stream and update the
|
|
3506
|
+
* queue as if the current song had just finished.
|
|
3507
|
+
*/
|
|
3508
|
+
async #crossfadeSwapTask(queue, transitionStream) {
|
|
3509
|
+
const abort = /* @__PURE__ */ __name(() => {
|
|
3510
|
+
if (queue._crossfadeWarmup === transitionStream) queue._crossfadeWarmup = void 0;
|
|
3511
|
+
transitionStream.kill();
|
|
3512
|
+
}, "abort");
|
|
3513
|
+
try {
|
|
3514
|
+
if (queue.stopped || queue.paused || queue._crossfadeWarmup !== transitionStream) return abort();
|
|
3515
|
+
const song = queue.songs[0];
|
|
3516
|
+
if (!song) return abort();
|
|
3517
|
+
const songDuration = song.duration ?? 0;
|
|
3518
|
+
const fade = Math.min(Number(queue.crossfade) || 0, songDuration);
|
|
3519
|
+
if (fade <= 0) return abort();
|
|
3520
|
+
const drift = queue.currentTime - (songDuration - fade);
|
|
3521
|
+
if (Math.abs(drift) > CROSSFADE_DRIFT_TOLERANCE_SECS) {
|
|
3522
|
+
this.debug(`[${queue.id}] Crossfade aborted: playback drifted ${drift.toFixed(2)}s from the fade boundary`);
|
|
3523
|
+
return abort();
|
|
3524
|
+
}
|
|
3525
|
+
await queue._taskQueue.queuing();
|
|
3526
|
+
try {
|
|
3527
|
+
if (queue.stopped || queue.paused || queue.songs[0] !== song || queue._crossfadeWarmup !== transitionStream) {
|
|
3528
|
+
return abort();
|
|
3529
|
+
}
|
|
3530
|
+
this.debug(`[${queue.id}] Starting crossfade: ${song} -> ${queue.songs.length > 1 ? queue.songs[1] : song}`);
|
|
3531
|
+
await queue.voice.play(transitionStream);
|
|
3532
|
+
queue._crossfadeWarmup = void 0;
|
|
3533
|
+
this.emit("finishSong" /* FINISH_SONG */, queue, song);
|
|
3534
|
+
if (queue.repeatMode === 2 /* QUEUE */) queue.songs.push(song);
|
|
3535
|
+
if (queue.repeatMode !== 1 /* SONG */) {
|
|
3536
|
+
const prev = queue.songs.shift();
|
|
3537
|
+
if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
|
|
3538
|
+
else queue.previousSongs.push({ id: prev.id });
|
|
3539
|
+
} else {
|
|
3540
|
+
}
|
|
3541
|
+
if (song !== queue.songs[0]) {
|
|
3542
|
+
const playedSong = song.stream.playFromSource ? song : song.stream.song;
|
|
3543
|
+
if (playedSong?.stream.playFromSource) delete playedSong.stream.url;
|
|
3544
|
+
}
|
|
3545
|
+
if (queue._preloadedStream) {
|
|
3546
|
+
queue._preloadedStream.kill();
|
|
3547
|
+
queue._preloadedStream = void 0;
|
|
3548
|
+
queue._preloadedSong = void 0;
|
|
3549
|
+
}
|
|
3550
|
+
this.emit("playSong" /* PLAY_SONG */, queue, queue.songs[0]);
|
|
3551
|
+
this.schedulePreload(queue);
|
|
3552
|
+
this.scheduleCrossfade(queue);
|
|
3553
|
+
} finally {
|
|
3554
|
+
queue._taskQueue.resolve();
|
|
3555
|
+
}
|
|
3556
|
+
} catch (e) {
|
|
3557
|
+
abort();
|
|
3558
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
3559
|
+
this.debug(`[${queue.id}] Crossfade swap failed, using the regular transition: ${errorMessage}`);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3185
3562
|
};
|
|
3186
3563
|
|
|
3187
3564
|
// src/core/StreamServer.ts
|
|
@@ -4196,6 +4573,10 @@ var PlayableExtractorPlugin = class extends ExtractorPlugin {
|
|
|
4196
4573
|
AUDIO_CHANNELS,
|
|
4197
4574
|
AUDIO_SAMPLE_RATE,
|
|
4198
4575
|
BaseManager,
|
|
4576
|
+
CROSSFADE_DRIFT_TOLERANCE_SECS,
|
|
4577
|
+
DEFAULT_CROSSFADE_CURVE,
|
|
4578
|
+
DEFAULT_CROSSFADE_DURATION_SECS,
|
|
4579
|
+
DEFAULT_CROSSFADE_WARMUP_SECS,
|
|
4199
4580
|
DEFAULT_PRELOAD_THRESHOLD_SECS,
|
|
4200
4581
|
DEFAULT_VOLUME,
|
|
4201
4582
|
DisTube,
|
|
@@ -4212,6 +4593,7 @@ var PlayableExtractorPlugin = class extends ExtractorPlugin {
|
|
|
4212
4593
|
HTTP_REDIRECT_CODES,
|
|
4213
4594
|
InfoExtractorPlugin,
|
|
4214
4595
|
JOIN_TIMEOUT_MS,
|
|
4596
|
+
MAX_CROSSFADE_DURATION_SECS,
|
|
4215
4597
|
MAX_REDIRECT_DEPTH,
|
|
4216
4598
|
Options,
|
|
4217
4599
|
PlayableExtractorPlugin,
|