@kattowo/distube 6.0.2 → 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 +361 -4
- package/dist/index.d.ts +361 -4
- package/dist/index.js +655 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +649 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -34,6 +34,11 @@ __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,
|
|
41
|
+
DEFAULT_PRELOAD_THRESHOLD_SECS: () => DEFAULT_PRELOAD_THRESHOLD_SECS,
|
|
37
42
|
DEFAULT_VOLUME: () => DEFAULT_VOLUME,
|
|
38
43
|
DisTube: () => DisTube,
|
|
39
44
|
DisTubeBase: () => DisTubeBase,
|
|
@@ -49,6 +54,7 @@ __export(index_exports, {
|
|
|
49
54
|
HTTP_REDIRECT_CODES: () => HTTP_REDIRECT_CODES,
|
|
50
55
|
InfoExtractorPlugin: () => InfoExtractorPlugin,
|
|
51
56
|
JOIN_TIMEOUT_MS: () => JOIN_TIMEOUT_MS,
|
|
57
|
+
MAX_CROSSFADE_DURATION_SECS: () => MAX_CROSSFADE_DURATION_SECS,
|
|
52
58
|
MAX_REDIRECT_DEPTH: () => MAX_REDIRECT_DEPTH,
|
|
53
59
|
Options: () => Options,
|
|
54
60
|
PlayableExtractorPlugin: () => PlayableExtractorPlugin,
|
|
@@ -96,7 +102,7 @@ __export(index_exports, {
|
|
|
96
102
|
module.exports = __toCommonJS(index_exports);
|
|
97
103
|
|
|
98
104
|
// src/constant.ts
|
|
99
|
-
var version = "6.0
|
|
105
|
+
var version = "6.2.0";
|
|
100
106
|
var AUDIO_SAMPLE_RATE = 48e3;
|
|
101
107
|
var AUDIO_CHANNELS = 2;
|
|
102
108
|
var DEFAULT_VOLUME = 50;
|
|
@@ -123,6 +129,12 @@ var defaultFilters = {
|
|
|
123
129
|
surround: "surround",
|
|
124
130
|
earwax: "earwax"
|
|
125
131
|
};
|
|
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;
|
|
126
138
|
var defaultOptions = {
|
|
127
139
|
plugins: [],
|
|
128
140
|
emitNewSongOnly: false,
|
|
@@ -132,7 +144,13 @@ var defaultOptions = {
|
|
|
132
144
|
emitAddListWhenCreatingQueue: true,
|
|
133
145
|
joinNewVoiceChannel: true,
|
|
134
146
|
unhandledError: "log",
|
|
135
|
-
stuckThreshold: STUCK_THRESHOLD_MS
|
|
147
|
+
stuckThreshold: STUCK_THRESHOLD_MS,
|
|
148
|
+
preload: false,
|
|
149
|
+
crossfade: {
|
|
150
|
+
duration: DEFAULT_CROSSFADE_DURATION_SECS,
|
|
151
|
+
curve: DEFAULT_CROSSFADE_CURVE,
|
|
152
|
+
warmup: DEFAULT_CROSSFADE_WARMUP_SECS
|
|
153
|
+
}
|
|
136
154
|
};
|
|
137
155
|
|
|
138
156
|
// src/core/DisTubeBase.ts
|
|
@@ -702,7 +720,14 @@ var DisTubeVoice = class extends import_tiny_typed_emitter.TypedEmitter {
|
|
|
702
720
|
if (newState.reason === import_voice.VoiceConnectionDisconnectReason.Manual) {
|
|
703
721
|
this.leave();
|
|
704
722
|
} else if (newState.reason === import_voice.VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
|
|
705
|
-
(0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).
|
|
723
|
+
(0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).then(() => (0, import_voice.entersState)(this.connection, import_voice.VoiceConnectionStatus.Ready, RECONNECT_TIMEOUT_MS)).then(() => {
|
|
724
|
+
const oldChannel = this.#channel;
|
|
725
|
+
const currentChannel = this.channel;
|
|
726
|
+
if (oldChannel && currentChannel && oldChannel.id !== currentChannel.id) {
|
|
727
|
+
this.#channel = currentChannel;
|
|
728
|
+
this.emit("channelChange", oldChannel, currentChannel);
|
|
729
|
+
}
|
|
730
|
+
}).catch(() => {
|
|
706
731
|
if (![import_voice.VoiceConnectionStatus.Ready, import_voice.VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
|
|
707
732
|
this.leave();
|
|
708
733
|
}
|
|
@@ -763,8 +788,12 @@ var DisTubeVoice = class extends import_tiny_typed_emitter.TypedEmitter {
|
|
|
763
788
|
if (channel.full) throw new DisTubeError("VOICE_FULL");
|
|
764
789
|
else throw new DisTubeError("VOICE_MISSING_PERMS");
|
|
765
790
|
}
|
|
791
|
+
const oldChannel = this.#channel;
|
|
766
792
|
this.connection = this.#join(channel);
|
|
767
793
|
this.#channel = channel;
|
|
794
|
+
if (oldChannel && oldChannel.id !== channel.id) {
|
|
795
|
+
this.emit("channelChange", oldChannel, channel);
|
|
796
|
+
}
|
|
768
797
|
}
|
|
769
798
|
#join(channel) {
|
|
770
799
|
return (0, import_voice.joinVoiceChannel)({
|
|
@@ -844,7 +873,7 @@ var DisTubeVoice = class extends import_tiny_typed_emitter.TypedEmitter {
|
|
|
844
873
|
if (this.audioPlayer.state.status !== import_voice.AudioPlayerStatus.Paused) {
|
|
845
874
|
this.audioPlayer.play(dtStream.audioResource);
|
|
846
875
|
this.stream?.kill();
|
|
847
|
-
dtStream.spawn();
|
|
876
|
+
if (!dtStream.process) dtStream.spawn();
|
|
848
877
|
} else if (!this.pausingStream) {
|
|
849
878
|
this.pausingStream = this.stream;
|
|
850
879
|
}
|
|
@@ -1052,7 +1081,13 @@ var FilterManager = class extends BaseManager {
|
|
|
1052
1081
|
}
|
|
1053
1082
|
throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
|
|
1054
1083
|
}
|
|
1055
|
-
#apply() {
|
|
1084
|
+
#apply(oldFilters) {
|
|
1085
|
+
this.queue._clearPreload?.();
|
|
1086
|
+
const newFilters = this.names;
|
|
1087
|
+
const changed = oldFilters.length !== newFilters.length || !oldFilters.every((f) => newFilters.includes(f));
|
|
1088
|
+
if (changed) {
|
|
1089
|
+
this.emit("changeFilter" /* CHANGE_FILTER */, this.queue, newFilters, oldFilters);
|
|
1090
|
+
}
|
|
1056
1091
|
void this.queue._restartStream().catch((e) => {
|
|
1057
1092
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
1058
1093
|
this.queue.debug(`[${this.queue.id}] Failed to apply the filters: ${errorMessage}`);
|
|
@@ -1070,6 +1105,7 @@ var FilterManager = class extends BaseManager {
|
|
|
1070
1105
|
* ```
|
|
1071
1106
|
*/
|
|
1072
1107
|
add(filterOrFilters, override = false) {
|
|
1108
|
+
const oldFilters = this.names;
|
|
1073
1109
|
if (Array.isArray(filterOrFilters)) {
|
|
1074
1110
|
for (const filter of filterOrFilters) {
|
|
1075
1111
|
const ft = this.#resolve(filter);
|
|
@@ -1079,7 +1115,7 @@ var FilterManager = class extends BaseManager {
|
|
|
1079
1115
|
const ft = this.#resolve(filterOrFilters);
|
|
1080
1116
|
if (override || !this.has(ft)) this.collection.set(ft.name, ft);
|
|
1081
1117
|
}
|
|
1082
|
-
this.#apply();
|
|
1118
|
+
this.#apply(oldFilters);
|
|
1083
1119
|
return this;
|
|
1084
1120
|
}
|
|
1085
1121
|
/**
|
|
@@ -1098,12 +1134,13 @@ var FilterManager = class extends BaseManager {
|
|
|
1098
1134
|
*/
|
|
1099
1135
|
set(filters) {
|
|
1100
1136
|
if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
|
|
1137
|
+
const oldFilters = this.names;
|
|
1101
1138
|
this.collection.clear();
|
|
1102
1139
|
for (const f of filters) {
|
|
1103
1140
|
const filter = this.#resolve(f);
|
|
1104
1141
|
this.collection.set(filter.name, filter);
|
|
1105
1142
|
}
|
|
1106
|
-
this.#apply();
|
|
1143
|
+
this.#apply(oldFilters);
|
|
1107
1144
|
return this;
|
|
1108
1145
|
}
|
|
1109
1146
|
#removeFn(f) {
|
|
@@ -1116,9 +1153,10 @@ var FilterManager = class extends BaseManager {
|
|
|
1116
1153
|
* @throws {@link DisTubeError} When a filter is not a valid {@link FilterResolvable}
|
|
1117
1154
|
*/
|
|
1118
1155
|
remove(filterOrFilters) {
|
|
1156
|
+
const oldFilters = this.names;
|
|
1119
1157
|
if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
|
|
1120
1158
|
else this.#removeFn(filterOrFilters);
|
|
1121
|
-
this.#apply();
|
|
1159
|
+
this.#apply(oldFilters);
|
|
1122
1160
|
return this;
|
|
1123
1161
|
}
|
|
1124
1162
|
/**
|
|
@@ -1174,6 +1212,17 @@ var Events = /* @__PURE__ */ ((Events2) => {
|
|
|
1174
1212
|
Events2["DELETE_QUEUE"] = "deleteQueue";
|
|
1175
1213
|
Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
|
|
1176
1214
|
Events2["DEBUG"] = "debug";
|
|
1215
|
+
Events2["PAUSE_SONG"] = "pauseSong";
|
|
1216
|
+
Events2["RESUME_SONG"] = "resumeSong";
|
|
1217
|
+
Events2["SEEK_SONG"] = "seekSong";
|
|
1218
|
+
Events2["SET_VOLUME"] = "setVolume";
|
|
1219
|
+
Events2["SHUFFLE_QUEUE"] = "shuffleQueue";
|
|
1220
|
+
Events2["SKIP_SONG"] = "skipSong";
|
|
1221
|
+
Events2["CHANGE_FILTER"] = "changeFilter";
|
|
1222
|
+
Events2["VOICE_CHANNEL_CHANGE"] = "voiceChannelChange";
|
|
1223
|
+
Events2["PRELOAD_SONG"] = "preloadSong";
|
|
1224
|
+
Events2["PRELOADED_SONG"] = "preloadedSong";
|
|
1225
|
+
Events2["PRELOAD_ERROR"] = "preloadError";
|
|
1177
1226
|
return Events2;
|
|
1178
1227
|
})(Events || {});
|
|
1179
1228
|
var SearchResultType = /* @__PURE__ */ ((SearchResultType2) => {
|
|
@@ -1307,6 +1356,7 @@ var Queue = class extends DisTubeBase {
|
|
|
1307
1356
|
*/
|
|
1308
1357
|
_beginTime;
|
|
1309
1358
|
#filters;
|
|
1359
|
+
#initialized = false;
|
|
1310
1360
|
/**
|
|
1311
1361
|
* Whether or not the queue is being updated manually (skip, jump, previous)
|
|
1312
1362
|
* @internal
|
|
@@ -1322,6 +1372,75 @@ var Queue = class extends DisTubeBase {
|
|
|
1322
1372
|
* @internal
|
|
1323
1373
|
*/
|
|
1324
1374
|
_listeners;
|
|
1375
|
+
/**
|
|
1376
|
+
* Preload timer
|
|
1377
|
+
* @internal
|
|
1378
|
+
*/
|
|
1379
|
+
_preloadTimeout;
|
|
1380
|
+
/**
|
|
1381
|
+
* Preloaded song
|
|
1382
|
+
* @internal
|
|
1383
|
+
*/
|
|
1384
|
+
_preloadedSong;
|
|
1385
|
+
/**
|
|
1386
|
+
* Preloaded stream
|
|
1387
|
+
* @internal
|
|
1388
|
+
*/
|
|
1389
|
+
_preloadedStream;
|
|
1390
|
+
/**
|
|
1391
|
+
* In-flight preload task
|
|
1392
|
+
* @internal
|
|
1393
|
+
*/
|
|
1394
|
+
_preloading;
|
|
1395
|
+
/**
|
|
1396
|
+
* Bumped whenever the preloaded state is invalidated, so an in-flight
|
|
1397
|
+
* preload task can detect that its result is no longer needed.
|
|
1398
|
+
* @internal
|
|
1399
|
+
*/
|
|
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;
|
|
1413
|
+
/**
|
|
1414
|
+
* Clear any preloaded song and cancel pending preload timer.
|
|
1415
|
+
* @internal
|
|
1416
|
+
*/
|
|
1417
|
+
_clearPreload() {
|
|
1418
|
+
this._crossfadeCancel();
|
|
1419
|
+
this._preloadGeneration++;
|
|
1420
|
+
if (this._preloadTimeout) {
|
|
1421
|
+
clearTimeout(this._preloadTimeout);
|
|
1422
|
+
this._preloadTimeout = void 0;
|
|
1423
|
+
}
|
|
1424
|
+
if (this._preloadedStream) {
|
|
1425
|
+
this._preloadedStream.kill();
|
|
1426
|
+
this._preloadedStream = void 0;
|
|
1427
|
+
}
|
|
1428
|
+
this._preloadedSong = void 0;
|
|
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
|
+
}
|
|
1325
1444
|
/**
|
|
1326
1445
|
* Create a queue for the guild
|
|
1327
1446
|
* @param distube - DisTube
|
|
@@ -1350,6 +1469,8 @@ var Queue = class extends DisTubeBase {
|
|
|
1350
1469
|
input: { ...this.options.ffmpeg.args.input },
|
|
1351
1470
|
output: { ...this.options.ffmpeg.args.output }
|
|
1352
1471
|
};
|
|
1472
|
+
this.#crossfade = Number(this.options.crossfade?.duration) || 0;
|
|
1473
|
+
this.#initialized = true;
|
|
1353
1474
|
}
|
|
1354
1475
|
#addToPreviousSongs(songs) {
|
|
1355
1476
|
if (Array.isArray(songs)) {
|
|
@@ -1407,6 +1528,28 @@ var Queue = class extends DisTubeBase {
|
|
|
1407
1528
|
get currentTime() {
|
|
1408
1529
|
return this.voice.playbackTime;
|
|
1409
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
|
+
}
|
|
1410
1553
|
/**
|
|
1411
1554
|
* Formatted {@link Queue#currentTime} string.
|
|
1412
1555
|
*/
|
|
@@ -1432,7 +1575,11 @@ var Queue = class extends DisTubeBase {
|
|
|
1432
1575
|
* negative
|
|
1433
1576
|
*/
|
|
1434
1577
|
set volume(value) {
|
|
1578
|
+
const oldVolume = this.voice.volume;
|
|
1435
1579
|
this.voice.volume = value;
|
|
1580
|
+
if (this.#initialized && oldVolume !== value) {
|
|
1581
|
+
this.emit("setVolume" /* SET_VOLUME */, this, value, oldVolume);
|
|
1582
|
+
}
|
|
1436
1583
|
}
|
|
1437
1584
|
/**
|
|
1438
1585
|
* Add a song or a list of songs to the queue.
|
|
@@ -1461,6 +1608,7 @@ var Queue = class extends DisTubeBase {
|
|
|
1461
1608
|
} else {
|
|
1462
1609
|
this.songs.splice(position, 0, song);
|
|
1463
1610
|
}
|
|
1611
|
+
this.queues?.schedulePreload?.(this);
|
|
1464
1612
|
return this;
|
|
1465
1613
|
}
|
|
1466
1614
|
/**
|
|
@@ -1478,7 +1626,9 @@ var Queue = class extends DisTubeBase {
|
|
|
1478
1626
|
try {
|
|
1479
1627
|
if (this.paused) throw new DisTubeError("PAUSED");
|
|
1480
1628
|
this.paused = true;
|
|
1629
|
+
this._clearPreload();
|
|
1481
1630
|
this.voice.pause();
|
|
1631
|
+
this.emit("pauseSong" /* PAUSE_SONG */, this, this.songs[0]);
|
|
1482
1632
|
return this;
|
|
1483
1633
|
} finally {
|
|
1484
1634
|
this._taskQueue.resolve();
|
|
@@ -1499,6 +1649,9 @@ var Queue = class extends DisTubeBase {
|
|
|
1499
1649
|
if (!this.paused) throw new DisTubeError("RESUMED");
|
|
1500
1650
|
this.paused = false;
|
|
1501
1651
|
this.voice.unpause();
|
|
1652
|
+
this.emit("resumeSong" /* RESUME_SONG */, this, this.songs[0]);
|
|
1653
|
+
this.queues?.schedulePreload?.(this);
|
|
1654
|
+
this.queues?.scheduleCrossfade?.(this);
|
|
1502
1655
|
return this;
|
|
1503
1656
|
} finally {
|
|
1504
1657
|
this._taskQueue.resolve();
|
|
@@ -1563,11 +1716,14 @@ var Queue = class extends DisTubeBase {
|
|
|
1563
1716
|
try {
|
|
1564
1717
|
const playing = this.songs.shift();
|
|
1565
1718
|
if (playing === void 0) return this;
|
|
1719
|
+
this._clearPreload();
|
|
1566
1720
|
for (let i = this.songs.length - 1; i > 0; i--) {
|
|
1567
1721
|
const j = Math.floor(Math.random() * (i + 1));
|
|
1568
1722
|
[this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
|
|
1569
1723
|
}
|
|
1570
1724
|
this.songs.unshift(playing);
|
|
1725
|
+
this.emit("shuffleQueue" /* SHUFFLE_QUEUE */, this, this.songs);
|
|
1726
|
+
this.queues?.schedulePreload?.(this);
|
|
1571
1727
|
return this;
|
|
1572
1728
|
} finally {
|
|
1573
1729
|
this._taskQueue.resolve();
|
|
@@ -1605,12 +1761,14 @@ var Queue = class extends DisTubeBase {
|
|
|
1605
1761
|
} else {
|
|
1606
1762
|
this.#addToPreviousSongs(skipped);
|
|
1607
1763
|
}
|
|
1764
|
+
this.emit("skipSong" /* SKIP_SONG */, this, skipped[0]);
|
|
1608
1765
|
} else if (!this.options.savePreviousSongs) {
|
|
1609
1766
|
throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
|
|
1610
1767
|
} else {
|
|
1611
1768
|
const skipped = this.previousSongs.splice(position);
|
|
1612
1769
|
this.songs.unshift(...skipped);
|
|
1613
1770
|
}
|
|
1771
|
+
this._clearPreload();
|
|
1614
1772
|
this.#stop();
|
|
1615
1773
|
return this.songs[0];
|
|
1616
1774
|
} finally {
|
|
@@ -1649,8 +1807,10 @@ var Queue = class extends DisTubeBase {
|
|
|
1649
1807
|
try {
|
|
1650
1808
|
if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
|
|
1651
1809
|
if (Number.isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
|
|
1810
|
+
this._clearPreload();
|
|
1652
1811
|
this._beginTime = time;
|
|
1653
1812
|
await this.play(false);
|
|
1813
|
+
this.emit("seekSong" /* SEEK_SONG */, this, this.songs[0], time);
|
|
1654
1814
|
return this;
|
|
1655
1815
|
} finally {
|
|
1656
1816
|
this._taskQueue.resolve();
|
|
@@ -1721,6 +1881,7 @@ var Queue = class extends DisTubeBase {
|
|
|
1721
1881
|
* @returns Nothing
|
|
1722
1882
|
*/
|
|
1723
1883
|
remove() {
|
|
1884
|
+
this._clearPreload();
|
|
1724
1885
|
this.paused = true;
|
|
1725
1886
|
this.stopped = true;
|
|
1726
1887
|
this.songs = [];
|
|
@@ -1747,6 +1908,17 @@ var Queue = class extends DisTubeBase {
|
|
|
1747
1908
|
if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
|
|
1748
1909
|
return this.queues.playSong(this, emitPlaySong);
|
|
1749
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
|
+
}
|
|
1750
1922
|
/**
|
|
1751
1923
|
* Restart the current stream with the current FFmpeg arguments, keeping the
|
|
1752
1924
|
* playback position.
|
|
@@ -2394,6 +2566,16 @@ var Options = class {
|
|
|
2394
2566
|
* @default 10000
|
|
2395
2567
|
*/
|
|
2396
2568
|
stuckThreshold;
|
|
2569
|
+
/**
|
|
2570
|
+
* The preload options of the DisTube instance.
|
|
2571
|
+
* @default { enabled: false, threshold: 15 }
|
|
2572
|
+
*/
|
|
2573
|
+
preload;
|
|
2574
|
+
/**
|
|
2575
|
+
* The crossfade options of the DisTube instance.
|
|
2576
|
+
* @default { duration: 0, curve: "qsin", warmup: 5 }
|
|
2577
|
+
*/
|
|
2578
|
+
crossfade;
|
|
2397
2579
|
/**
|
|
2398
2580
|
* The FFmpeg options of the DisTube instance.
|
|
2399
2581
|
*/
|
|
@@ -2419,10 +2601,76 @@ var Options = class {
|
|
|
2419
2601
|
this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
|
|
2420
2602
|
this.unhandledError = opts.unhandledError;
|
|
2421
2603
|
this.stuckThreshold = opts.stuckThreshold;
|
|
2604
|
+
this.preload = this.#preloadOption(opts.preload);
|
|
2605
|
+
this.crossfade = this.#crossfadeOption(opts.crossfade);
|
|
2422
2606
|
this.ffmpeg = this.#ffmpegOption(options);
|
|
2423
2607
|
checkInvalidKey(opts, this, "DisTubeOptions");
|
|
2424
2608
|
this.#validateOptions();
|
|
2425
2609
|
}
|
|
2610
|
+
#preloadOption(value) {
|
|
2611
|
+
if (value === void 0 || value === false) {
|
|
2612
|
+
return { enabled: false, threshold: DEFAULT_PRELOAD_THRESHOLD_SECS };
|
|
2613
|
+
}
|
|
2614
|
+
if (value === true) {
|
|
2615
|
+
return { enabled: true, threshold: DEFAULT_PRELOAD_THRESHOLD_SECS };
|
|
2616
|
+
}
|
|
2617
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
2618
|
+
throw new DisTubeError("INVALID_TYPE", ["boolean", "object"], value, "DisTubeOptions.preload");
|
|
2619
|
+
}
|
|
2620
|
+
checkInvalidKey(value, ["enabled", "threshold"], "DisTubeOptions.preload");
|
|
2621
|
+
const { enabled, threshold } = value;
|
|
2622
|
+
if (enabled !== void 0 && typeof enabled !== "boolean") {
|
|
2623
|
+
throw new DisTubeError("INVALID_TYPE", "boolean", enabled, "DisTubeOptions.preload.enabled");
|
|
2624
|
+
}
|
|
2625
|
+
if (threshold !== void 0 && (typeof threshold !== "number" || Number.isNaN(threshold) || threshold < 0)) {
|
|
2626
|
+
throw new DisTubeError("INVALID_TYPE", "number", threshold, "DisTubeOptions.preload.threshold");
|
|
2627
|
+
}
|
|
2628
|
+
return {
|
|
2629
|
+
enabled: enabled ?? false,
|
|
2630
|
+
threshold: threshold ?? DEFAULT_PRELOAD_THRESHOLD_SECS
|
|
2631
|
+
};
|
|
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
|
+
}
|
|
2426
2674
|
#validateOptions(options = this) {
|
|
2427
2675
|
const booleanOptions = /* @__PURE__ */ new Set([
|
|
2428
2676
|
"emitNewSongOnly",
|
|
@@ -2434,7 +2682,7 @@ var Options = class {
|
|
|
2434
2682
|
]);
|
|
2435
2683
|
const numberOptions = /* @__PURE__ */ new Set(["stuckThreshold"]);
|
|
2436
2684
|
const stringOptions = /* @__PURE__ */ new Set(["unhandledError"]);
|
|
2437
|
-
const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
|
|
2685
|
+
const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg", "preload", "crossfade"]);
|
|
2438
2686
|
const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
|
|
2439
2687
|
for (const [key, value] of Object.entries(options)) {
|
|
2440
2688
|
if (value === void 0 && optionalOptions.has(key)) continue;
|
|
@@ -2552,8 +2800,40 @@ var DisTubeStream = class extends import_tiny_typed_emitter2.TypedEmitter {
|
|
|
2552
2800
|
*/
|
|
2553
2801
|
constructor(url, options) {
|
|
2554
2802
|
super();
|
|
2555
|
-
const { ffmpeg, seek } = options;
|
|
2803
|
+
const { ffmpeg, seek, crossfade } = options;
|
|
2556
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) {
|
|
2557
2837
|
const opts = {
|
|
2558
2838
|
reconnect: 1,
|
|
2559
2839
|
reconnect_streamed: 1,
|
|
@@ -2569,29 +2849,91 @@ var DisTubeStream = class extends import_tiny_typed_emitter2.TypedEmitter {
|
|
|
2569
2849
|
f: "s16le"
|
|
2570
2850
|
};
|
|
2571
2851
|
if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
|
|
2572
|
-
const
|
|
2573
|
-
if (
|
|
2852
|
+
const localPath = this.#localPath(url);
|
|
2853
|
+
if (localPath !== void 0) {
|
|
2574
2854
|
opts.reconnect = null;
|
|
2575
2855
|
opts.reconnect_streamed = null;
|
|
2576
2856
|
opts.reconnect_delay_max = null;
|
|
2577
|
-
opts.i =
|
|
2857
|
+
opts.i = localPath;
|
|
2578
2858
|
}
|
|
2579
|
-
this.#
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
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),
|
|
2584
2935
|
"pipe:1"
|
|
2585
2936
|
];
|
|
2586
|
-
this.stream = new VolumeTransformer();
|
|
2587
|
-
this.stream.on("close", () => this.kill()).on("error", (err) => {
|
|
2588
|
-
this.debug(`[stream] error: ${err.message}`);
|
|
2589
|
-
this.emit("error", err);
|
|
2590
|
-
}).on("finish", () => this.debug("[stream] log: stream finished"));
|
|
2591
|
-
this.audioResource = (0, import_voice2.createAudioResource)(this.stream, {
|
|
2592
|
-
inputType: import_voice2.StreamType.Raw,
|
|
2593
|
-
inlineVolume: false
|
|
2594
|
-
});
|
|
2595
2937
|
}
|
|
2596
2938
|
/**
|
|
2597
2939
|
* Spawn the FFmpeg process that decodes the audio.
|
|
@@ -2822,7 +3164,10 @@ var QueueManager = class extends GuildIdManager {
|
|
|
2822
3164
|
if (error) this.emitError(error, queue, queue.songs?.[0]);
|
|
2823
3165
|
}, "disconnect"),
|
|
2824
3166
|
error: /* @__PURE__ */ __name((error) => void this.#handlePlayingError(queue, error), "error"),
|
|
2825
|
-
finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish")
|
|
3167
|
+
finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish"),
|
|
3168
|
+
channelChange: /* @__PURE__ */ __name((oldChannel, newChannel) => {
|
|
3169
|
+
this.emit("voiceChannelChange" /* VOICE_CHANNEL_CHANGE */, queue, oldChannel, newChannel);
|
|
3170
|
+
}, "channelChange")
|
|
2826
3171
|
};
|
|
2827
3172
|
for (const event of objectKeys(queue._listeners)) {
|
|
2828
3173
|
queue.voice.on(event, queue._listeners[event]);
|
|
@@ -2920,12 +3265,110 @@ var QueueManager = class extends GuildIdManager {
|
|
|
2920
3265
|
}
|
|
2921
3266
|
try {
|
|
2922
3267
|
const song = queue.songs[0];
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3268
|
+
let dtStream;
|
|
3269
|
+
if (queue._preloadedSong === song && queue._preloadedStream) {
|
|
3270
|
+
this.debug(`[${queue.id}] Using preloaded stream for: ${song}`);
|
|
3271
|
+
dtStream = queue._preloadedStream;
|
|
3272
|
+
queue._preloadedSong = void 0;
|
|
3273
|
+
queue._preloadedStream = void 0;
|
|
3274
|
+
} else {
|
|
3275
|
+
queue._clearPreload();
|
|
3276
|
+
this.debug(`[${queue.id}] Getting stream from: ${song}`);
|
|
3277
|
+
await this.handler.attachStreamInfo(song);
|
|
3278
|
+
const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
|
|
3279
|
+
const stream = willPlaySong?.stream;
|
|
3280
|
+
if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
|
|
3281
|
+
this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
|
|
3282
|
+
const streamOptions = {
|
|
3283
|
+
ffmpeg: {
|
|
3284
|
+
path: this.options.ffmpeg.path,
|
|
3285
|
+
args: {
|
|
3286
|
+
global: { ...queue.ffmpegArgs.global },
|
|
3287
|
+
input: { ...queue.ffmpegArgs.input },
|
|
3288
|
+
output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
|
|
3289
|
+
}
|
|
3290
|
+
},
|
|
3291
|
+
seek: willPlaySong.duration ? queue._beginTime : void 0
|
|
3292
|
+
};
|
|
3293
|
+
queue._beginTime = 0;
|
|
3294
|
+
dtStream = new DisTubeStream(stream.url, streamOptions);
|
|
3295
|
+
dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
|
|
3296
|
+
}
|
|
3297
|
+
queue._beginTime = 0;
|
|
3298
|
+
this.debug(`[${queue.id}] Started playing: ${song}`);
|
|
3299
|
+
await queue.voice.play(dtStream);
|
|
3300
|
+
if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
|
|
3301
|
+
this.schedulePreload(queue);
|
|
3302
|
+
this.scheduleCrossfade(queue);
|
|
3303
|
+
} catch (e) {
|
|
3304
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
3305
|
+
if (options?.keepSongOnError) {
|
|
3306
|
+
this.debug(`[${queue.id}] Error while restarting the stream: ${error.stack || error.message}`);
|
|
3307
|
+
this.emitError(error, queue, queue.songs[0]);
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
await this.#handlePlayingError(queue, error);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Schedule preloading the next song in the queue before the current song finishes.
|
|
3315
|
+
* @param queue - The guild queue
|
|
3316
|
+
*/
|
|
3317
|
+
schedulePreload(queue) {
|
|
3318
|
+
if (!this.options.preload.enabled || queue.stopped || queue.paused) return;
|
|
3319
|
+
if (queue._preloadTimeout) {
|
|
3320
|
+
clearTimeout(queue._preloadTimeout);
|
|
3321
|
+
queue._preloadTimeout = void 0;
|
|
3322
|
+
}
|
|
3323
|
+
const currentSong = queue.songs[0];
|
|
3324
|
+
const nextSong = queue.songs[1];
|
|
3325
|
+
if (!currentSong || !nextSong) return;
|
|
3326
|
+
if (queue._preloadedSong === nextSong && queue._preloadedStream) return;
|
|
3327
|
+
const threshold = this.options.preload.threshold;
|
|
3328
|
+
const duration = currentSong.duration ?? 0;
|
|
3329
|
+
const currentTime = queue.currentTime;
|
|
3330
|
+
const remaining = duration > 0 && Number.isFinite(currentTime) ? duration - currentTime : 0;
|
|
3331
|
+
if (duration === 0 || remaining <= threshold) {
|
|
3332
|
+
void this.preloadNextSong(queue);
|
|
3333
|
+
} else {
|
|
3334
|
+
const delayMs = Math.max(0, (remaining - threshold) * 1e3);
|
|
3335
|
+
queue._preloadTimeout = setTimeout(() => {
|
|
3336
|
+
queue._preloadTimeout = void 0;
|
|
3337
|
+
void this.preloadNextSong(queue);
|
|
3338
|
+
}, delayMs);
|
|
3339
|
+
queue._preloadTimeout.unref?.();
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
/**
|
|
3343
|
+
* Preload the next song in the queue.
|
|
3344
|
+
* @param queue - The guild queue
|
|
3345
|
+
*/
|
|
3346
|
+
async preloadNextSong(queue) {
|
|
3347
|
+
if (queue._preloading) return queue._preloading;
|
|
3348
|
+
const task = this.#preloadTask(queue);
|
|
3349
|
+
queue._preloading = task;
|
|
3350
|
+
try {
|
|
3351
|
+
await task;
|
|
3352
|
+
} finally {
|
|
3353
|
+
if (queue._preloading === task) queue._preloading = void 0;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
async #preloadTask(queue) {
|
|
3357
|
+
if (!this.options.preload.enabled || queue.stopped || queue.paused) return;
|
|
3358
|
+
const nextSong = queue.songs[1];
|
|
3359
|
+
if (!nextSong) return;
|
|
3360
|
+
if (queue._preloadedSong === nextSong && queue._preloadedStream) return;
|
|
3361
|
+
const generation = queue._preloadGeneration;
|
|
3362
|
+
this.debug(`[${queue.id}] Preloading next song: ${nextSong}`);
|
|
3363
|
+
this.emit("preloadSong" /* PRELOAD_SONG */, queue, nextSong);
|
|
3364
|
+
try {
|
|
3365
|
+
await this.handler.attachStreamInfo(nextSong);
|
|
3366
|
+
if (queue.stopped || queue.songs[1] !== nextSong || generation !== queue._preloadGeneration) return;
|
|
3367
|
+
const willPlaySong = nextSong.stream.playFromSource ? nextSong : nextSong.stream.song;
|
|
2926
3368
|
const stream = willPlaySong?.stream;
|
|
2927
|
-
if (!willPlaySong || !stream?.playFromSource || !stream.url)
|
|
2928
|
-
|
|
3369
|
+
if (!willPlaySong || !stream?.playFromSource || !stream.url) {
|
|
3370
|
+
throw new DisTubeError("NO_STREAM_URL", `${nextSong}`);
|
|
3371
|
+
}
|
|
2929
3372
|
const streamOptions = {
|
|
2930
3373
|
ffmpeg: {
|
|
2931
3374
|
path: this.options.ffmpeg.path,
|
|
@@ -2935,22 +3378,185 @@ var QueueManager = class extends GuildIdManager {
|
|
|
2935
3378
|
output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
|
|
2936
3379
|
}
|
|
2937
3380
|
},
|
|
2938
|
-
seek:
|
|
3381
|
+
seek: void 0
|
|
2939
3382
|
};
|
|
2940
|
-
queue._beginTime = 0;
|
|
2941
3383
|
const dtStream = new DisTubeStream(stream.url, streamOptions);
|
|
2942
|
-
dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3384
|
+
dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] [preload] ${data}`));
|
|
3385
|
+
if (queue.stopped || queue.songs[1] !== nextSong || generation !== queue._preloadGeneration) {
|
|
3386
|
+
dtStream.kill();
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
queue._preloadedSong = nextSong;
|
|
3390
|
+
queue._preloadedStream = dtStream;
|
|
3391
|
+
this.debug(`[${queue.id}] Successfully preloaded next song: ${nextSong}`);
|
|
3392
|
+
this.emit("preloadedSong" /* PRELOADED_SONG */, queue, nextSong);
|
|
2946
3393
|
} catch (e) {
|
|
3394
|
+
if (queue._preloadedSong === nextSong) {
|
|
3395
|
+
queue._preloadedSong = void 0;
|
|
3396
|
+
queue._preloadedStream?.kill();
|
|
3397
|
+
queue._preloadedStream = void 0;
|
|
3398
|
+
}
|
|
2947
3399
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3400
|
+
this.debug(`[${queue.id}] Preload failed for ${nextSong}: ${error.message}`);
|
|
3401
|
+
this.emit("preloadError" /* PRELOAD_ERROR */, queue, nextSong, error);
|
|
3402
|
+
}
|
|
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();
|
|
2951
3488
|
return;
|
|
2952
3489
|
}
|
|
2953
|
-
|
|
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}`);
|
|
2954
3560
|
}
|
|
2955
3561
|
}
|
|
2956
3562
|
};
|
|
@@ -3967,6 +4573,11 @@ var PlayableExtractorPlugin = class extends ExtractorPlugin {
|
|
|
3967
4573
|
AUDIO_CHANNELS,
|
|
3968
4574
|
AUDIO_SAMPLE_RATE,
|
|
3969
4575
|
BaseManager,
|
|
4576
|
+
CROSSFADE_DRIFT_TOLERANCE_SECS,
|
|
4577
|
+
DEFAULT_CROSSFADE_CURVE,
|
|
4578
|
+
DEFAULT_CROSSFADE_DURATION_SECS,
|
|
4579
|
+
DEFAULT_CROSSFADE_WARMUP_SECS,
|
|
4580
|
+
DEFAULT_PRELOAD_THRESHOLD_SECS,
|
|
3970
4581
|
DEFAULT_VOLUME,
|
|
3971
4582
|
DisTube,
|
|
3972
4583
|
DisTubeBase,
|
|
@@ -3982,6 +4593,7 @@ var PlayableExtractorPlugin = class extends ExtractorPlugin {
|
|
|
3982
4593
|
HTTP_REDIRECT_CODES,
|
|
3983
4594
|
InfoExtractorPlugin,
|
|
3984
4595
|
JOIN_TIMEOUT_MS,
|
|
4596
|
+
MAX_CROSSFADE_DURATION_SECS,
|
|
3985
4597
|
MAX_REDIRECT_DEPTH,
|
|
3986
4598
|
Options,
|
|
3987
4599
|
PlayableExtractorPlugin,
|