@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.mjs
CHANGED
|
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
4
|
// src/constant.ts
|
|
5
|
-
var version = "6.0
|
|
5
|
+
var version = "6.2.0";
|
|
6
6
|
var AUDIO_SAMPLE_RATE = 48e3;
|
|
7
7
|
var AUDIO_CHANNELS = 2;
|
|
8
8
|
var DEFAULT_VOLUME = 50;
|
|
@@ -29,6 +29,12 @@ var defaultFilters = {
|
|
|
29
29
|
surround: "surround",
|
|
30
30
|
earwax: "earwax"
|
|
31
31
|
};
|
|
32
|
+
var DEFAULT_PRELOAD_THRESHOLD_SECS = 15;
|
|
33
|
+
var DEFAULT_CROSSFADE_DURATION_SECS = 0;
|
|
34
|
+
var MAX_CROSSFADE_DURATION_SECS = 12;
|
|
35
|
+
var DEFAULT_CROSSFADE_CURVE = "qsin";
|
|
36
|
+
var DEFAULT_CROSSFADE_WARMUP_SECS = 5;
|
|
37
|
+
var CROSSFADE_DRIFT_TOLERANCE_SECS = 0.75;
|
|
32
38
|
var defaultOptions = {
|
|
33
39
|
plugins: [],
|
|
34
40
|
emitNewSongOnly: false,
|
|
@@ -38,7 +44,13 @@ var defaultOptions = {
|
|
|
38
44
|
emitAddListWhenCreatingQueue: true,
|
|
39
45
|
joinNewVoiceChannel: true,
|
|
40
46
|
unhandledError: "log",
|
|
41
|
-
stuckThreshold: STUCK_THRESHOLD_MS
|
|
47
|
+
stuckThreshold: STUCK_THRESHOLD_MS,
|
|
48
|
+
preload: false,
|
|
49
|
+
crossfade: {
|
|
50
|
+
duration: DEFAULT_CROSSFADE_DURATION_SECS,
|
|
51
|
+
curve: DEFAULT_CROSSFADE_CURVE,
|
|
52
|
+
warmup: DEFAULT_CROSSFADE_WARMUP_SECS
|
|
53
|
+
}
|
|
42
54
|
};
|
|
43
55
|
|
|
44
56
|
// src/core/DisTubeBase.ts
|
|
@@ -615,7 +627,14 @@ var DisTubeVoice = class extends TypedEmitter {
|
|
|
615
627
|
if (newState.reason === VoiceConnectionDisconnectReason.Manual) {
|
|
616
628
|
this.leave();
|
|
617
629
|
} else if (newState.reason === VoiceConnectionDisconnectReason.WebSocketClose && newState.closeCode === 4014) {
|
|
618
|
-
entersState(this.connection, VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).
|
|
630
|
+
entersState(this.connection, VoiceConnectionStatus.Connecting, RECONNECT_TIMEOUT_MS).then(() => entersState(this.connection, VoiceConnectionStatus.Ready, RECONNECT_TIMEOUT_MS)).then(() => {
|
|
631
|
+
const oldChannel = this.#channel;
|
|
632
|
+
const currentChannel = this.channel;
|
|
633
|
+
if (oldChannel && currentChannel && oldChannel.id !== currentChannel.id) {
|
|
634
|
+
this.#channel = currentChannel;
|
|
635
|
+
this.emit("channelChange", oldChannel, currentChannel);
|
|
636
|
+
}
|
|
637
|
+
}).catch(() => {
|
|
619
638
|
if (![VoiceConnectionStatus.Ready, VoiceConnectionStatus.Connecting].includes(this.connection.state.status)) {
|
|
620
639
|
this.leave();
|
|
621
640
|
}
|
|
@@ -676,8 +695,12 @@ var DisTubeVoice = class extends TypedEmitter {
|
|
|
676
695
|
if (channel.full) throw new DisTubeError("VOICE_FULL");
|
|
677
696
|
else throw new DisTubeError("VOICE_MISSING_PERMS");
|
|
678
697
|
}
|
|
698
|
+
const oldChannel = this.#channel;
|
|
679
699
|
this.connection = this.#join(channel);
|
|
680
700
|
this.#channel = channel;
|
|
701
|
+
if (oldChannel && oldChannel.id !== channel.id) {
|
|
702
|
+
this.emit("channelChange", oldChannel, channel);
|
|
703
|
+
}
|
|
681
704
|
}
|
|
682
705
|
#join(channel) {
|
|
683
706
|
return joinVoiceChannel({
|
|
@@ -757,7 +780,7 @@ var DisTubeVoice = class extends TypedEmitter {
|
|
|
757
780
|
if (this.audioPlayer.state.status !== AudioPlayerStatus.Paused) {
|
|
758
781
|
this.audioPlayer.play(dtStream.audioResource);
|
|
759
782
|
this.stream?.kill();
|
|
760
|
-
dtStream.spawn();
|
|
783
|
+
if (!dtStream.process) dtStream.spawn();
|
|
761
784
|
} else if (!this.pausingStream) {
|
|
762
785
|
this.pausingStream = this.stream;
|
|
763
786
|
}
|
|
@@ -965,7 +988,13 @@ var FilterManager = class extends BaseManager {
|
|
|
965
988
|
}
|
|
966
989
|
throw new DisTubeError("INVALID_TYPE", "FilterResolvable", filter, "filter");
|
|
967
990
|
}
|
|
968
|
-
#apply() {
|
|
991
|
+
#apply(oldFilters) {
|
|
992
|
+
this.queue._clearPreload?.();
|
|
993
|
+
const newFilters = this.names;
|
|
994
|
+
const changed = oldFilters.length !== newFilters.length || !oldFilters.every((f) => newFilters.includes(f));
|
|
995
|
+
if (changed) {
|
|
996
|
+
this.emit("changeFilter" /* CHANGE_FILTER */, this.queue, newFilters, oldFilters);
|
|
997
|
+
}
|
|
969
998
|
void this.queue._restartStream().catch((e) => {
|
|
970
999
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
971
1000
|
this.queue.debug(`[${this.queue.id}] Failed to apply the filters: ${errorMessage}`);
|
|
@@ -983,6 +1012,7 @@ var FilterManager = class extends BaseManager {
|
|
|
983
1012
|
* ```
|
|
984
1013
|
*/
|
|
985
1014
|
add(filterOrFilters, override = false) {
|
|
1015
|
+
const oldFilters = this.names;
|
|
986
1016
|
if (Array.isArray(filterOrFilters)) {
|
|
987
1017
|
for (const filter of filterOrFilters) {
|
|
988
1018
|
const ft = this.#resolve(filter);
|
|
@@ -992,7 +1022,7 @@ var FilterManager = class extends BaseManager {
|
|
|
992
1022
|
const ft = this.#resolve(filterOrFilters);
|
|
993
1023
|
if (override || !this.has(ft)) this.collection.set(ft.name, ft);
|
|
994
1024
|
}
|
|
995
|
-
this.#apply();
|
|
1025
|
+
this.#apply(oldFilters);
|
|
996
1026
|
return this;
|
|
997
1027
|
}
|
|
998
1028
|
/**
|
|
@@ -1011,12 +1041,13 @@ var FilterManager = class extends BaseManager {
|
|
|
1011
1041
|
*/
|
|
1012
1042
|
set(filters) {
|
|
1013
1043
|
if (!Array.isArray(filters)) throw new DisTubeError("INVALID_TYPE", "Array<FilterResolvable>", filters, "filters");
|
|
1044
|
+
const oldFilters = this.names;
|
|
1014
1045
|
this.collection.clear();
|
|
1015
1046
|
for (const f of filters) {
|
|
1016
1047
|
const filter = this.#resolve(f);
|
|
1017
1048
|
this.collection.set(filter.name, filter);
|
|
1018
1049
|
}
|
|
1019
|
-
this.#apply();
|
|
1050
|
+
this.#apply(oldFilters);
|
|
1020
1051
|
return this;
|
|
1021
1052
|
}
|
|
1022
1053
|
#removeFn(f) {
|
|
@@ -1029,9 +1060,10 @@ var FilterManager = class extends BaseManager {
|
|
|
1029
1060
|
* @throws {@link DisTubeError} When a filter is not a valid {@link FilterResolvable}
|
|
1030
1061
|
*/
|
|
1031
1062
|
remove(filterOrFilters) {
|
|
1063
|
+
const oldFilters = this.names;
|
|
1032
1064
|
if (Array.isArray(filterOrFilters)) filterOrFilters.forEach((f) => this.#removeFn(f));
|
|
1033
1065
|
else this.#removeFn(filterOrFilters);
|
|
1034
|
-
this.#apply();
|
|
1066
|
+
this.#apply(oldFilters);
|
|
1035
1067
|
return this;
|
|
1036
1068
|
}
|
|
1037
1069
|
/**
|
|
@@ -1087,6 +1119,17 @@ var Events = /* @__PURE__ */ ((Events2) => {
|
|
|
1087
1119
|
Events2["DELETE_QUEUE"] = "deleteQueue";
|
|
1088
1120
|
Events2["FFMPEG_DEBUG"] = "ffmpegDebug";
|
|
1089
1121
|
Events2["DEBUG"] = "debug";
|
|
1122
|
+
Events2["PAUSE_SONG"] = "pauseSong";
|
|
1123
|
+
Events2["RESUME_SONG"] = "resumeSong";
|
|
1124
|
+
Events2["SEEK_SONG"] = "seekSong";
|
|
1125
|
+
Events2["SET_VOLUME"] = "setVolume";
|
|
1126
|
+
Events2["SHUFFLE_QUEUE"] = "shuffleQueue";
|
|
1127
|
+
Events2["SKIP_SONG"] = "skipSong";
|
|
1128
|
+
Events2["CHANGE_FILTER"] = "changeFilter";
|
|
1129
|
+
Events2["VOICE_CHANNEL_CHANGE"] = "voiceChannelChange";
|
|
1130
|
+
Events2["PRELOAD_SONG"] = "preloadSong";
|
|
1131
|
+
Events2["PRELOADED_SONG"] = "preloadedSong";
|
|
1132
|
+
Events2["PRELOAD_ERROR"] = "preloadError";
|
|
1090
1133
|
return Events2;
|
|
1091
1134
|
})(Events || {});
|
|
1092
1135
|
var SearchResultType = /* @__PURE__ */ ((SearchResultType2) => {
|
|
@@ -1220,6 +1263,7 @@ var Queue = class extends DisTubeBase {
|
|
|
1220
1263
|
*/
|
|
1221
1264
|
_beginTime;
|
|
1222
1265
|
#filters;
|
|
1266
|
+
#initialized = false;
|
|
1223
1267
|
/**
|
|
1224
1268
|
* Whether or not the queue is being updated manually (skip, jump, previous)
|
|
1225
1269
|
* @internal
|
|
@@ -1235,6 +1279,75 @@ var Queue = class extends DisTubeBase {
|
|
|
1235
1279
|
* @internal
|
|
1236
1280
|
*/
|
|
1237
1281
|
_listeners;
|
|
1282
|
+
/**
|
|
1283
|
+
* Preload timer
|
|
1284
|
+
* @internal
|
|
1285
|
+
*/
|
|
1286
|
+
_preloadTimeout;
|
|
1287
|
+
/**
|
|
1288
|
+
* Preloaded song
|
|
1289
|
+
* @internal
|
|
1290
|
+
*/
|
|
1291
|
+
_preloadedSong;
|
|
1292
|
+
/**
|
|
1293
|
+
* Preloaded stream
|
|
1294
|
+
* @internal
|
|
1295
|
+
*/
|
|
1296
|
+
_preloadedStream;
|
|
1297
|
+
/**
|
|
1298
|
+
* In-flight preload task
|
|
1299
|
+
* @internal
|
|
1300
|
+
*/
|
|
1301
|
+
_preloading;
|
|
1302
|
+
/**
|
|
1303
|
+
* Bumped whenever the preloaded state is invalidated, so an in-flight
|
|
1304
|
+
* preload task can detect that its result is no longer needed.
|
|
1305
|
+
* @internal
|
|
1306
|
+
*/
|
|
1307
|
+
_preloadGeneration = 0;
|
|
1308
|
+
#crossfade;
|
|
1309
|
+
/**
|
|
1310
|
+
* Crossfade boundary timer
|
|
1311
|
+
* @internal
|
|
1312
|
+
*/
|
|
1313
|
+
_crossfadeTimeout;
|
|
1314
|
+
/**
|
|
1315
|
+
* The merged transition stream that is warming up before the fade window
|
|
1316
|
+
* starts
|
|
1317
|
+
* @internal
|
|
1318
|
+
*/
|
|
1319
|
+
_crossfadeWarmup;
|
|
1320
|
+
/**
|
|
1321
|
+
* Clear any preloaded song and cancel pending preload timer.
|
|
1322
|
+
* @internal
|
|
1323
|
+
*/
|
|
1324
|
+
_clearPreload() {
|
|
1325
|
+
this._crossfadeCancel();
|
|
1326
|
+
this._preloadGeneration++;
|
|
1327
|
+
if (this._preloadTimeout) {
|
|
1328
|
+
clearTimeout(this._preloadTimeout);
|
|
1329
|
+
this._preloadTimeout = void 0;
|
|
1330
|
+
}
|
|
1331
|
+
if (this._preloadedStream) {
|
|
1332
|
+
this._preloadedStream.kill();
|
|
1333
|
+
this._preloadedStream = void 0;
|
|
1334
|
+
}
|
|
1335
|
+
this._preloadedSong = void 0;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Cancel a pending crossfade and kill its warming-up transition stream.
|
|
1339
|
+
* @internal
|
|
1340
|
+
*/
|
|
1341
|
+
_crossfadeCancel() {
|
|
1342
|
+
if (this._crossfadeTimeout) {
|
|
1343
|
+
clearTimeout(this._crossfadeTimeout);
|
|
1344
|
+
this._crossfadeTimeout = void 0;
|
|
1345
|
+
}
|
|
1346
|
+
if (this._crossfadeWarmup) {
|
|
1347
|
+
this._crossfadeWarmup.kill();
|
|
1348
|
+
this._crossfadeWarmup = void 0;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1238
1351
|
/**
|
|
1239
1352
|
* Create a queue for the guild
|
|
1240
1353
|
* @param distube - DisTube
|
|
@@ -1263,6 +1376,8 @@ var Queue = class extends DisTubeBase {
|
|
|
1263
1376
|
input: { ...this.options.ffmpeg.args.input },
|
|
1264
1377
|
output: { ...this.options.ffmpeg.args.output }
|
|
1265
1378
|
};
|
|
1379
|
+
this.#crossfade = Number(this.options.crossfade?.duration) || 0;
|
|
1380
|
+
this.#initialized = true;
|
|
1266
1381
|
}
|
|
1267
1382
|
#addToPreviousSongs(songs) {
|
|
1268
1383
|
if (Array.isArray(songs)) {
|
|
@@ -1320,6 +1435,28 @@ var Queue = class extends DisTubeBase {
|
|
|
1320
1435
|
get currentTime() {
|
|
1321
1436
|
return this.voice.playbackTime;
|
|
1322
1437
|
}
|
|
1438
|
+
/**
|
|
1439
|
+
* The crossfade duration (in seconds) between this song and the next one.
|
|
1440
|
+
*
|
|
1441
|
+
* `0` (the default) disables the crossfade. Changing it takes effect for the
|
|
1442
|
+
* next transition; call {@link Queue#scheduleCrossfade} to re-arm the
|
|
1443
|
+
* transition for the currently playing song immediately.
|
|
1444
|
+
* @throws {@link DisTubeError} When the value is not a number between `0`
|
|
1445
|
+
* and the maximum crossfade duration
|
|
1446
|
+
*/
|
|
1447
|
+
get crossfade() {
|
|
1448
|
+
return this.#crossfade;
|
|
1449
|
+
}
|
|
1450
|
+
set crossfade(value) {
|
|
1451
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
1452
|
+
throw new DisTubeError("INVALID_TYPE", "number", value, "Queue.crossfade");
|
|
1453
|
+
}
|
|
1454
|
+
if (value < 0) throw new DisTubeError("NUMBER_COMPARE", "Crossfade", "bigger or equal to", 0);
|
|
1455
|
+
if (value > MAX_CROSSFADE_DURATION_SECS) {
|
|
1456
|
+
throw new DisTubeError("NUMBER_COMPARE", "Crossfade", "smaller or equal to", MAX_CROSSFADE_DURATION_SECS);
|
|
1457
|
+
}
|
|
1458
|
+
this.#crossfade = value;
|
|
1459
|
+
}
|
|
1323
1460
|
/**
|
|
1324
1461
|
* Formatted {@link Queue#currentTime} string.
|
|
1325
1462
|
*/
|
|
@@ -1345,7 +1482,11 @@ var Queue = class extends DisTubeBase {
|
|
|
1345
1482
|
* negative
|
|
1346
1483
|
*/
|
|
1347
1484
|
set volume(value) {
|
|
1485
|
+
const oldVolume = this.voice.volume;
|
|
1348
1486
|
this.voice.volume = value;
|
|
1487
|
+
if (this.#initialized && oldVolume !== value) {
|
|
1488
|
+
this.emit("setVolume" /* SET_VOLUME */, this, value, oldVolume);
|
|
1489
|
+
}
|
|
1349
1490
|
}
|
|
1350
1491
|
/**
|
|
1351
1492
|
* Add a song or a list of songs to the queue.
|
|
@@ -1374,6 +1515,7 @@ var Queue = class extends DisTubeBase {
|
|
|
1374
1515
|
} else {
|
|
1375
1516
|
this.songs.splice(position, 0, song);
|
|
1376
1517
|
}
|
|
1518
|
+
this.queues?.schedulePreload?.(this);
|
|
1377
1519
|
return this;
|
|
1378
1520
|
}
|
|
1379
1521
|
/**
|
|
@@ -1391,7 +1533,9 @@ var Queue = class extends DisTubeBase {
|
|
|
1391
1533
|
try {
|
|
1392
1534
|
if (this.paused) throw new DisTubeError("PAUSED");
|
|
1393
1535
|
this.paused = true;
|
|
1536
|
+
this._clearPreload();
|
|
1394
1537
|
this.voice.pause();
|
|
1538
|
+
this.emit("pauseSong" /* PAUSE_SONG */, this, this.songs[0]);
|
|
1395
1539
|
return this;
|
|
1396
1540
|
} finally {
|
|
1397
1541
|
this._taskQueue.resolve();
|
|
@@ -1412,6 +1556,9 @@ var Queue = class extends DisTubeBase {
|
|
|
1412
1556
|
if (!this.paused) throw new DisTubeError("RESUMED");
|
|
1413
1557
|
this.paused = false;
|
|
1414
1558
|
this.voice.unpause();
|
|
1559
|
+
this.emit("resumeSong" /* RESUME_SONG */, this, this.songs[0]);
|
|
1560
|
+
this.queues?.schedulePreload?.(this);
|
|
1561
|
+
this.queues?.scheduleCrossfade?.(this);
|
|
1415
1562
|
return this;
|
|
1416
1563
|
} finally {
|
|
1417
1564
|
this._taskQueue.resolve();
|
|
@@ -1476,11 +1623,14 @@ var Queue = class extends DisTubeBase {
|
|
|
1476
1623
|
try {
|
|
1477
1624
|
const playing = this.songs.shift();
|
|
1478
1625
|
if (playing === void 0) return this;
|
|
1626
|
+
this._clearPreload();
|
|
1479
1627
|
for (let i = this.songs.length - 1; i > 0; i--) {
|
|
1480
1628
|
const j = Math.floor(Math.random() * (i + 1));
|
|
1481
1629
|
[this.songs[i], this.songs[j]] = [this.songs[j], this.songs[i]];
|
|
1482
1630
|
}
|
|
1483
1631
|
this.songs.unshift(playing);
|
|
1632
|
+
this.emit("shuffleQueue" /* SHUFFLE_QUEUE */, this, this.songs);
|
|
1633
|
+
this.queues?.schedulePreload?.(this);
|
|
1484
1634
|
return this;
|
|
1485
1635
|
} finally {
|
|
1486
1636
|
this._taskQueue.resolve();
|
|
@@ -1518,12 +1668,14 @@ var Queue = class extends DisTubeBase {
|
|
|
1518
1668
|
} else {
|
|
1519
1669
|
this.#addToPreviousSongs(skipped);
|
|
1520
1670
|
}
|
|
1671
|
+
this.emit("skipSong" /* SKIP_SONG */, this, skipped[0]);
|
|
1521
1672
|
} else if (!this.options.savePreviousSongs) {
|
|
1522
1673
|
throw new DisTubeError("DISABLED_OPTION", "savePreviousSongs");
|
|
1523
1674
|
} else {
|
|
1524
1675
|
const skipped = this.previousSongs.splice(position);
|
|
1525
1676
|
this.songs.unshift(...skipped);
|
|
1526
1677
|
}
|
|
1678
|
+
this._clearPreload();
|
|
1527
1679
|
this.#stop();
|
|
1528
1680
|
return this.songs[0];
|
|
1529
1681
|
} finally {
|
|
@@ -1562,8 +1714,10 @@ var Queue = class extends DisTubeBase {
|
|
|
1562
1714
|
try {
|
|
1563
1715
|
if (typeof time !== "number") throw new DisTubeError("INVALID_TYPE", "number", time, "time");
|
|
1564
1716
|
if (Number.isNaN(time) || time < 0) throw new DisTubeError("NUMBER_COMPARE", "time", "bigger or equal to", 0);
|
|
1717
|
+
this._clearPreload();
|
|
1565
1718
|
this._beginTime = time;
|
|
1566
1719
|
await this.play(false);
|
|
1720
|
+
this.emit("seekSong" /* SEEK_SONG */, this, this.songs[0], time);
|
|
1567
1721
|
return this;
|
|
1568
1722
|
} finally {
|
|
1569
1723
|
this._taskQueue.resolve();
|
|
@@ -1634,6 +1788,7 @@ var Queue = class extends DisTubeBase {
|
|
|
1634
1788
|
* @returns Nothing
|
|
1635
1789
|
*/
|
|
1636
1790
|
remove() {
|
|
1791
|
+
this._clearPreload();
|
|
1637
1792
|
this.paused = true;
|
|
1638
1793
|
this.stopped = true;
|
|
1639
1794
|
this.songs = [];
|
|
@@ -1660,6 +1815,17 @@ var Queue = class extends DisTubeBase {
|
|
|
1660
1815
|
if (this.stopped) throw new DisTubeError("QUEUE_STOPPED");
|
|
1661
1816
|
return this.queues.playSong(this, emitPlaySong);
|
|
1662
1817
|
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Re-arm the crossfade into the next song for the currently playing one.
|
|
1820
|
+
*
|
|
1821
|
+
* Useful after changing {@link Queue#crossfade} while a song is playing: the
|
|
1822
|
+
* fade may still fit into the remaining time. This is called automatically
|
|
1823
|
+
* after every song switch.
|
|
1824
|
+
* @returns Nothing
|
|
1825
|
+
*/
|
|
1826
|
+
scheduleCrossfade() {
|
|
1827
|
+
this.queues.scheduleCrossfade(this);
|
|
1828
|
+
}
|
|
1663
1829
|
/**
|
|
1664
1830
|
* Restart the current stream with the current FFmpeg arguments, keeping the
|
|
1665
1831
|
* playback position.
|
|
@@ -2307,6 +2473,16 @@ var Options = class {
|
|
|
2307
2473
|
* @default 10000
|
|
2308
2474
|
*/
|
|
2309
2475
|
stuckThreshold;
|
|
2476
|
+
/**
|
|
2477
|
+
* The preload options of the DisTube instance.
|
|
2478
|
+
* @default { enabled: false, threshold: 15 }
|
|
2479
|
+
*/
|
|
2480
|
+
preload;
|
|
2481
|
+
/**
|
|
2482
|
+
* The crossfade options of the DisTube instance.
|
|
2483
|
+
* @default { duration: 0, curve: "qsin", warmup: 5 }
|
|
2484
|
+
*/
|
|
2485
|
+
crossfade;
|
|
2310
2486
|
/**
|
|
2311
2487
|
* The FFmpeg options of the DisTube instance.
|
|
2312
2488
|
*/
|
|
@@ -2332,10 +2508,76 @@ var Options = class {
|
|
|
2332
2508
|
this.joinNewVoiceChannel = opts.joinNewVoiceChannel;
|
|
2333
2509
|
this.unhandledError = opts.unhandledError;
|
|
2334
2510
|
this.stuckThreshold = opts.stuckThreshold;
|
|
2511
|
+
this.preload = this.#preloadOption(opts.preload);
|
|
2512
|
+
this.crossfade = this.#crossfadeOption(opts.crossfade);
|
|
2335
2513
|
this.ffmpeg = this.#ffmpegOption(options);
|
|
2336
2514
|
checkInvalidKey(opts, this, "DisTubeOptions");
|
|
2337
2515
|
this.#validateOptions();
|
|
2338
2516
|
}
|
|
2517
|
+
#preloadOption(value) {
|
|
2518
|
+
if (value === void 0 || value === false) {
|
|
2519
|
+
return { enabled: false, threshold: DEFAULT_PRELOAD_THRESHOLD_SECS };
|
|
2520
|
+
}
|
|
2521
|
+
if (value === true) {
|
|
2522
|
+
return { enabled: true, threshold: DEFAULT_PRELOAD_THRESHOLD_SECS };
|
|
2523
|
+
}
|
|
2524
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
2525
|
+
throw new DisTubeError("INVALID_TYPE", ["boolean", "object"], value, "DisTubeOptions.preload");
|
|
2526
|
+
}
|
|
2527
|
+
checkInvalidKey(value, ["enabled", "threshold"], "DisTubeOptions.preload");
|
|
2528
|
+
const { enabled, threshold } = value;
|
|
2529
|
+
if (enabled !== void 0 && typeof enabled !== "boolean") {
|
|
2530
|
+
throw new DisTubeError("INVALID_TYPE", "boolean", enabled, "DisTubeOptions.preload.enabled");
|
|
2531
|
+
}
|
|
2532
|
+
if (threshold !== void 0 && (typeof threshold !== "number" || Number.isNaN(threshold) || threshold < 0)) {
|
|
2533
|
+
throw new DisTubeError("INVALID_TYPE", "number", threshold, "DisTubeOptions.preload.threshold");
|
|
2534
|
+
}
|
|
2535
|
+
return {
|
|
2536
|
+
enabled: enabled ?? false,
|
|
2537
|
+
threshold: threshold ?? DEFAULT_PRELOAD_THRESHOLD_SECS
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
#crossfadeOption(value) {
|
|
2541
|
+
if (value === void 0) {
|
|
2542
|
+
return {
|
|
2543
|
+
duration: DEFAULT_CROSSFADE_DURATION_SECS,
|
|
2544
|
+
curve: DEFAULT_CROSSFADE_CURVE,
|
|
2545
|
+
warmup: DEFAULT_CROSSFADE_WARMUP_SECS
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
2549
|
+
throw new DisTubeError("INVALID_TYPE", "object", value, "DisTubeOptions.crossfade");
|
|
2550
|
+
}
|
|
2551
|
+
checkInvalidKey(value, ["duration", "curve", "warmup"], "DisTubeOptions.crossfade");
|
|
2552
|
+
const { duration, curve, warmup } = value;
|
|
2553
|
+
if (duration !== void 0 && (typeof duration !== "number" || Number.isNaN(duration) || duration < 0)) {
|
|
2554
|
+
throw new DisTubeError(
|
|
2555
|
+
"INVALID_TYPE",
|
|
2556
|
+
`number >= 0 (max ${MAX_CROSSFADE_DURATION_SECS})`,
|
|
2557
|
+
duration,
|
|
2558
|
+
"DisTubeOptions.crossfade.duration"
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
if (duration !== void 0 && duration > MAX_CROSSFADE_DURATION_SECS) {
|
|
2562
|
+
throw new DisTubeError(
|
|
2563
|
+
"NUMBER_COMPARE",
|
|
2564
|
+
"Crossfade duration",
|
|
2565
|
+
"smaller or equal to",
|
|
2566
|
+
MAX_CROSSFADE_DURATION_SECS
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
if (curve !== void 0 && (typeof curve !== "string" || !curve)) {
|
|
2570
|
+
throw new DisTubeError("INVALID_TYPE", "string", curve, "DisTubeOptions.crossfade.curve");
|
|
2571
|
+
}
|
|
2572
|
+
if (warmup !== void 0 && (typeof warmup !== "number" || Number.isNaN(warmup) || warmup < 0)) {
|
|
2573
|
+
throw new DisTubeError("INVALID_TYPE", "number >= 0", warmup, "DisTubeOptions.crossfade.warmup");
|
|
2574
|
+
}
|
|
2575
|
+
return {
|
|
2576
|
+
duration: duration ?? DEFAULT_CROSSFADE_DURATION_SECS,
|
|
2577
|
+
curve: curve ?? DEFAULT_CROSSFADE_CURVE,
|
|
2578
|
+
warmup: warmup ?? DEFAULT_CROSSFADE_WARMUP_SECS
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2339
2581
|
#validateOptions(options = this) {
|
|
2340
2582
|
const booleanOptions = /* @__PURE__ */ new Set([
|
|
2341
2583
|
"emitNewSongOnly",
|
|
@@ -2347,7 +2589,7 @@ var Options = class {
|
|
|
2347
2589
|
]);
|
|
2348
2590
|
const numberOptions = /* @__PURE__ */ new Set(["stuckThreshold"]);
|
|
2349
2591
|
const stringOptions = /* @__PURE__ */ new Set(["unhandledError"]);
|
|
2350
|
-
const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg"]);
|
|
2592
|
+
const objectOptions = /* @__PURE__ */ new Set(["customFilters", "ffmpeg", "preload", "crossfade"]);
|
|
2351
2593
|
const optionalOptions = /* @__PURE__ */ new Set(["customFilters"]);
|
|
2352
2594
|
for (const [key, value] of Object.entries(options)) {
|
|
2353
2595
|
if (value === void 0 && optionalOptions.has(key)) continue;
|
|
@@ -2465,8 +2707,40 @@ var DisTubeStream = class extends TypedEmitter2 {
|
|
|
2465
2707
|
*/
|
|
2466
2708
|
constructor(url, options) {
|
|
2467
2709
|
super();
|
|
2468
|
-
const { ffmpeg, seek } = options;
|
|
2710
|
+
const { ffmpeg, seek, crossfade } = options;
|
|
2469
2711
|
this.seekTime = typeof seek === "number" && seek > 0 ? seek : 0;
|
|
2712
|
+
this.#ffmpegPath = ffmpeg.path;
|
|
2713
|
+
this.#opts = crossfade ? this.#crossfadeArgs(url, ffmpeg, crossfade) : this.#singleInputArgs(url, ffmpeg, seek);
|
|
2714
|
+
this.stream = new VolumeTransformer();
|
|
2715
|
+
this.stream.on("close", () => this.kill()).on("error", (err) => {
|
|
2716
|
+
this.debug(`[stream] error: ${err.message}`);
|
|
2717
|
+
this.emit("error", err);
|
|
2718
|
+
}).on("finish", () => this.debug("[stream] log: stream finished"));
|
|
2719
|
+
this.audioResource = createAudioResource(this.stream, {
|
|
2720
|
+
inputType: StreamType.Raw,
|
|
2721
|
+
inlineVolume: false
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
#flattenArgs(args) {
|
|
2725
|
+
return Object.entries(args).flatMap(
|
|
2726
|
+
([key, value]) => Array.isArray(value) ? value.filter(Boolean).map((v) => [`-${key}`, String(v)]) : value == null || value === false ? [] : [value === true ? `-${key}` : [`-${key}`, String(value)]]
|
|
2727
|
+
).flat();
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Translate a `file:` URL into a local filesystem path that FFmpeg accepts.
|
|
2731
|
+
*
|
|
2732
|
+
* `file:///C:/x` loses its drive-letter leading slash on Windows (`C:/x`),
|
|
2733
|
+
* UNC shares keep their `//host` prefix, POSIX paths stay untouched, and
|
|
2734
|
+
* other protocols return `undefined`.
|
|
2735
|
+
*/
|
|
2736
|
+
#localPath(url) {
|
|
2737
|
+
const fileUrl = new URL(url);
|
|
2738
|
+
if (fileUrl.protocol !== "file:") return void 0;
|
|
2739
|
+
const pathname = decodeURIComponent(fileUrl.pathname);
|
|
2740
|
+
if (fileUrl.hostname) return `//${fileUrl.hostname}${pathname}`;
|
|
2741
|
+
return /^\/[A-Za-z]:\//.test(pathname) ? pathname.slice(1) : pathname;
|
|
2742
|
+
}
|
|
2743
|
+
#singleInputArgs(url, ffmpeg, seek) {
|
|
2470
2744
|
const opts = {
|
|
2471
2745
|
reconnect: 1,
|
|
2472
2746
|
reconnect_streamed: 1,
|
|
@@ -2482,29 +2756,91 @@ var DisTubeStream = class extends TypedEmitter2 {
|
|
|
2482
2756
|
f: "s16le"
|
|
2483
2757
|
};
|
|
2484
2758
|
if (typeof seek === "number" && seek > 0) opts.ss = seek.toString();
|
|
2485
|
-
const
|
|
2486
|
-
if (
|
|
2759
|
+
const localPath = this.#localPath(url);
|
|
2760
|
+
if (localPath !== void 0) {
|
|
2487
2761
|
opts.reconnect = null;
|
|
2488
2762
|
opts.reconnect_streamed = null;
|
|
2489
2763
|
opts.reconnect_delay_max = null;
|
|
2490
|
-
opts.i =
|
|
2764
|
+
opts.i = localPath;
|
|
2491
2765
|
}
|
|
2492
|
-
this.#
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2766
|
+
return [...this.#flattenArgs(opts), "pipe:1"];
|
|
2767
|
+
}
|
|
2768
|
+
/**
|
|
2769
|
+
* Build the arguments of a crossfade transition stream: a single FFmpeg
|
|
2770
|
+
* process with two inputs (the fading out song seeked to its final
|
|
2771
|
+
* `duration` seconds and the fading in song from its beginning) mixed by an
|
|
2772
|
+
* `acrossfade` filter graph.
|
|
2773
|
+
*
|
|
2774
|
+
* Both inputs are normalized (48kHz stereo s16) before the fade, and the
|
|
2775
|
+
* queue's `af` filter chain, when present, is folded into the filter complex
|
|
2776
|
+
* after the fade (FFmpeg cannot combine `-af` with `-filter_complex`).
|
|
2777
|
+
*/
|
|
2778
|
+
#crossfadeArgs(url, ffmpeg, crossfade) {
|
|
2779
|
+
const {
|
|
2780
|
+
duration,
|
|
2781
|
+
primarySeek,
|
|
2782
|
+
secondaryUrl,
|
|
2783
|
+
outCurve = DEFAULT_CROSSFADE_CURVE,
|
|
2784
|
+
inCurve = DEFAULT_CROSSFADE_CURVE
|
|
2785
|
+
} = crossfade;
|
|
2786
|
+
const outputArgs = { ...ffmpeg.args.output };
|
|
2787
|
+
const extraFilters = typeof outputArgs.af === "string" ? outputArgs.af : Array.isArray(outputArgs.af) ? outputArgs.af.filter(Boolean).join(",") : void 0;
|
|
2788
|
+
delete outputArgs.af;
|
|
2789
|
+
const primaryOpts = {
|
|
2790
|
+
reconnect: 1,
|
|
2791
|
+
reconnect_streamed: 1,
|
|
2792
|
+
reconnect_delay_max: 5,
|
|
2793
|
+
analyzeduration: 0,
|
|
2794
|
+
hide_banner: true,
|
|
2795
|
+
...ffmpeg.args.global,
|
|
2796
|
+
...ffmpeg.args.input,
|
|
2797
|
+
ss: typeof primarySeek === "number" && primarySeek > 0 ? primarySeek.toString() : null,
|
|
2798
|
+
i: url
|
|
2799
|
+
};
|
|
2800
|
+
const secondaryOpts = {
|
|
2801
|
+
reconnect: 1,
|
|
2802
|
+
reconnect_streamed: 1,
|
|
2803
|
+
reconnect_delay_max: 5,
|
|
2804
|
+
analyzeduration: 0,
|
|
2805
|
+
...ffmpeg.args.input,
|
|
2806
|
+
i: secondaryUrl
|
|
2807
|
+
};
|
|
2808
|
+
const primaryLocal = this.#localPath(url);
|
|
2809
|
+
if (primaryLocal !== void 0) {
|
|
2810
|
+
primaryOpts.reconnect = null;
|
|
2811
|
+
primaryOpts.reconnect_streamed = null;
|
|
2812
|
+
primaryOpts.reconnect_delay_max = null;
|
|
2813
|
+
primaryOpts.i = primaryLocal;
|
|
2814
|
+
}
|
|
2815
|
+
const secondaryLocal = this.#localPath(secondaryUrl);
|
|
2816
|
+
if (secondaryLocal !== void 0) {
|
|
2817
|
+
secondaryOpts.reconnect = null;
|
|
2818
|
+
secondaryOpts.reconnect_streamed = null;
|
|
2819
|
+
secondaryOpts.reconnect_delay_max = null;
|
|
2820
|
+
secondaryOpts.i = secondaryLocal;
|
|
2821
|
+
}
|
|
2822
|
+
const normalize = `aresample=${AUDIO_SAMPLE_RATE},aformat=sample_fmts=s16:channel_layouts=stereo`;
|
|
2823
|
+
const fade = `[a0][a1]acrossfade=d=${duration}:c1=${outCurve}:c2=${inCurve}[xf]`;
|
|
2824
|
+
const graph = [
|
|
2825
|
+
`[0:a]${normalize}[a0]`,
|
|
2826
|
+
`[1:a]${normalize}[a1]`,
|
|
2827
|
+
fade,
|
|
2828
|
+
...extraFilters ? [`[xf]${extraFilters}[aout]`] : []
|
|
2829
|
+
].join(";");
|
|
2830
|
+
const outputOpts = {
|
|
2831
|
+
ar: AUDIO_SAMPLE_RATE,
|
|
2832
|
+
ac: AUDIO_CHANNELS,
|
|
2833
|
+
...outputArgs,
|
|
2834
|
+
filter_complex: graph,
|
|
2835
|
+
map: extraFilters ? "[aout]" : "[xf]",
|
|
2836
|
+
f: "s16le"
|
|
2837
|
+
};
|
|
2838
|
+
return [
|
|
2839
|
+
...this.#flattenArgs(primaryOpts),
|
|
2840
|
+
...this.#flattenArgs(secondaryOpts),
|
|
2841
|
+
...this.#flattenArgs(outputOpts),
|
|
2497
2842
|
"pipe:1"
|
|
2498
2843
|
];
|
|
2499
|
-
this.stream = new VolumeTransformer();
|
|
2500
|
-
this.stream.on("close", () => this.kill()).on("error", (err) => {
|
|
2501
|
-
this.debug(`[stream] error: ${err.message}`);
|
|
2502
|
-
this.emit("error", err);
|
|
2503
|
-
}).on("finish", () => this.debug("[stream] log: stream finished"));
|
|
2504
|
-
this.audioResource = createAudioResource(this.stream, {
|
|
2505
|
-
inputType: StreamType.Raw,
|
|
2506
|
-
inlineVolume: false
|
|
2507
|
-
});
|
|
2508
2844
|
}
|
|
2509
2845
|
/**
|
|
2510
2846
|
* Spawn the FFmpeg process that decodes the audio.
|
|
@@ -2735,7 +3071,10 @@ var QueueManager = class extends GuildIdManager {
|
|
|
2735
3071
|
if (error) this.emitError(error, queue, queue.songs?.[0]);
|
|
2736
3072
|
}, "disconnect"),
|
|
2737
3073
|
error: /* @__PURE__ */ __name((error) => void this.#handlePlayingError(queue, error), "error"),
|
|
2738
|
-
finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish")
|
|
3074
|
+
finish: /* @__PURE__ */ __name(() => void this.handleSongFinish(queue), "finish"),
|
|
3075
|
+
channelChange: /* @__PURE__ */ __name((oldChannel, newChannel) => {
|
|
3076
|
+
this.emit("voiceChannelChange" /* VOICE_CHANNEL_CHANGE */, queue, oldChannel, newChannel);
|
|
3077
|
+
}, "channelChange")
|
|
2739
3078
|
};
|
|
2740
3079
|
for (const event of objectKeys(queue._listeners)) {
|
|
2741
3080
|
queue.voice.on(event, queue._listeners[event]);
|
|
@@ -2833,12 +3172,110 @@ var QueueManager = class extends GuildIdManager {
|
|
|
2833
3172
|
}
|
|
2834
3173
|
try {
|
|
2835
3174
|
const song = queue.songs[0];
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
3175
|
+
let dtStream;
|
|
3176
|
+
if (queue._preloadedSong === song && queue._preloadedStream) {
|
|
3177
|
+
this.debug(`[${queue.id}] Using preloaded stream for: ${song}`);
|
|
3178
|
+
dtStream = queue._preloadedStream;
|
|
3179
|
+
queue._preloadedSong = void 0;
|
|
3180
|
+
queue._preloadedStream = void 0;
|
|
3181
|
+
} else {
|
|
3182
|
+
queue._clearPreload();
|
|
3183
|
+
this.debug(`[${queue.id}] Getting stream from: ${song}`);
|
|
3184
|
+
await this.handler.attachStreamInfo(song);
|
|
3185
|
+
const willPlaySong = song.stream.playFromSource ? song : song.stream.song;
|
|
3186
|
+
const stream = willPlaySong?.stream;
|
|
3187
|
+
if (!willPlaySong || !stream?.playFromSource || !stream.url) throw new DisTubeError("NO_STREAM_URL", `${song}`);
|
|
3188
|
+
this.debug(`[${queue.id}] Creating DisTubeStream for: ${willPlaySong}`);
|
|
3189
|
+
const streamOptions = {
|
|
3190
|
+
ffmpeg: {
|
|
3191
|
+
path: this.options.ffmpeg.path,
|
|
3192
|
+
args: {
|
|
3193
|
+
global: { ...queue.ffmpegArgs.global },
|
|
3194
|
+
input: { ...queue.ffmpegArgs.input },
|
|
3195
|
+
output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
|
|
3196
|
+
}
|
|
3197
|
+
},
|
|
3198
|
+
seek: willPlaySong.duration ? queue._beginTime : void 0
|
|
3199
|
+
};
|
|
3200
|
+
queue._beginTime = 0;
|
|
3201
|
+
dtStream = new DisTubeStream(stream.url, streamOptions);
|
|
3202
|
+
dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
|
|
3203
|
+
}
|
|
3204
|
+
queue._beginTime = 0;
|
|
3205
|
+
this.debug(`[${queue.id}] Started playing: ${song}`);
|
|
3206
|
+
await queue.voice.play(dtStream);
|
|
3207
|
+
if (emitPlaySong) this.emit("playSong" /* PLAY_SONG */, queue, song);
|
|
3208
|
+
this.schedulePreload(queue);
|
|
3209
|
+
this.scheduleCrossfade(queue);
|
|
3210
|
+
} catch (e) {
|
|
3211
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
3212
|
+
if (options?.keepSongOnError) {
|
|
3213
|
+
this.debug(`[${queue.id}] Error while restarting the stream: ${error.stack || error.message}`);
|
|
3214
|
+
this.emitError(error, queue, queue.songs[0]);
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
await this.#handlePlayingError(queue, error);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
/**
|
|
3221
|
+
* Schedule preloading the next song in the queue before the current song finishes.
|
|
3222
|
+
* @param queue - The guild queue
|
|
3223
|
+
*/
|
|
3224
|
+
schedulePreload(queue) {
|
|
3225
|
+
if (!this.options.preload.enabled || queue.stopped || queue.paused) return;
|
|
3226
|
+
if (queue._preloadTimeout) {
|
|
3227
|
+
clearTimeout(queue._preloadTimeout);
|
|
3228
|
+
queue._preloadTimeout = void 0;
|
|
3229
|
+
}
|
|
3230
|
+
const currentSong = queue.songs[0];
|
|
3231
|
+
const nextSong = queue.songs[1];
|
|
3232
|
+
if (!currentSong || !nextSong) return;
|
|
3233
|
+
if (queue._preloadedSong === nextSong && queue._preloadedStream) return;
|
|
3234
|
+
const threshold = this.options.preload.threshold;
|
|
3235
|
+
const duration = currentSong.duration ?? 0;
|
|
3236
|
+
const currentTime = queue.currentTime;
|
|
3237
|
+
const remaining = duration > 0 && Number.isFinite(currentTime) ? duration - currentTime : 0;
|
|
3238
|
+
if (duration === 0 || remaining <= threshold) {
|
|
3239
|
+
void this.preloadNextSong(queue);
|
|
3240
|
+
} else {
|
|
3241
|
+
const delayMs = Math.max(0, (remaining - threshold) * 1e3);
|
|
3242
|
+
queue._preloadTimeout = setTimeout(() => {
|
|
3243
|
+
queue._preloadTimeout = void 0;
|
|
3244
|
+
void this.preloadNextSong(queue);
|
|
3245
|
+
}, delayMs);
|
|
3246
|
+
queue._preloadTimeout.unref?.();
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
/**
|
|
3250
|
+
* Preload the next song in the queue.
|
|
3251
|
+
* @param queue - The guild queue
|
|
3252
|
+
*/
|
|
3253
|
+
async preloadNextSong(queue) {
|
|
3254
|
+
if (queue._preloading) return queue._preloading;
|
|
3255
|
+
const task = this.#preloadTask(queue);
|
|
3256
|
+
queue._preloading = task;
|
|
3257
|
+
try {
|
|
3258
|
+
await task;
|
|
3259
|
+
} finally {
|
|
3260
|
+
if (queue._preloading === task) queue._preloading = void 0;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
async #preloadTask(queue) {
|
|
3264
|
+
if (!this.options.preload.enabled || queue.stopped || queue.paused) return;
|
|
3265
|
+
const nextSong = queue.songs[1];
|
|
3266
|
+
if (!nextSong) return;
|
|
3267
|
+
if (queue._preloadedSong === nextSong && queue._preloadedStream) return;
|
|
3268
|
+
const generation = queue._preloadGeneration;
|
|
3269
|
+
this.debug(`[${queue.id}] Preloading next song: ${nextSong}`);
|
|
3270
|
+
this.emit("preloadSong" /* PRELOAD_SONG */, queue, nextSong);
|
|
3271
|
+
try {
|
|
3272
|
+
await this.handler.attachStreamInfo(nextSong);
|
|
3273
|
+
if (queue.stopped || queue.songs[1] !== nextSong || generation !== queue._preloadGeneration) return;
|
|
3274
|
+
const willPlaySong = nextSong.stream.playFromSource ? nextSong : nextSong.stream.song;
|
|
2839
3275
|
const stream = willPlaySong?.stream;
|
|
2840
|
-
if (!willPlaySong || !stream?.playFromSource || !stream.url)
|
|
2841
|
-
|
|
3276
|
+
if (!willPlaySong || !stream?.playFromSource || !stream.url) {
|
|
3277
|
+
throw new DisTubeError("NO_STREAM_URL", `${nextSong}`);
|
|
3278
|
+
}
|
|
2842
3279
|
const streamOptions = {
|
|
2843
3280
|
ffmpeg: {
|
|
2844
3281
|
path: this.options.ffmpeg.path,
|
|
@@ -2848,22 +3285,185 @@ var QueueManager = class extends GuildIdManager {
|
|
|
2848
3285
|
output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
|
|
2849
3286
|
}
|
|
2850
3287
|
},
|
|
2851
|
-
seek:
|
|
3288
|
+
seek: void 0
|
|
2852
3289
|
};
|
|
2853
|
-
queue._beginTime = 0;
|
|
2854
3290
|
const dtStream = new DisTubeStream(stream.url, streamOptions);
|
|
2855
|
-
dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] ${data}`));
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
3291
|
+
dtStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] [preload] ${data}`));
|
|
3292
|
+
if (queue.stopped || queue.songs[1] !== nextSong || generation !== queue._preloadGeneration) {
|
|
3293
|
+
dtStream.kill();
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
queue._preloadedSong = nextSong;
|
|
3297
|
+
queue._preloadedStream = dtStream;
|
|
3298
|
+
this.debug(`[${queue.id}] Successfully preloaded next song: ${nextSong}`);
|
|
3299
|
+
this.emit("preloadedSong" /* PRELOADED_SONG */, queue, nextSong);
|
|
2859
3300
|
} catch (e) {
|
|
3301
|
+
if (queue._preloadedSong === nextSong) {
|
|
3302
|
+
queue._preloadedSong = void 0;
|
|
3303
|
+
queue._preloadedStream?.kill();
|
|
3304
|
+
queue._preloadedStream = void 0;
|
|
3305
|
+
}
|
|
2860
3306
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
3307
|
+
this.debug(`[${queue.id}] Preload failed for ${nextSong}: ${error.message}`);
|
|
3308
|
+
this.emit("preloadError" /* PRELOAD_ERROR */, queue, nextSong, error);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Schedule the crossfade into the next song: a single FFmpeg process mixes
|
|
3313
|
+
* the current song (seeked to its final fade window) with the next song, and
|
|
3314
|
+
* replaces the playback stream exactly when the fade window begins.
|
|
3315
|
+
*
|
|
3316
|
+
* Canceled automatically whenever the preloaded state is invalidated
|
|
3317
|
+
* (pause, seek, stop...), as both schedulers share the same generation.
|
|
3318
|
+
* @param queue - The guild queue
|
|
3319
|
+
*/
|
|
3320
|
+
scheduleCrossfade(queue) {
|
|
3321
|
+
queue._crossfadeCancel();
|
|
3322
|
+
if (queue.stopped || queue.paused) return;
|
|
3323
|
+
const currentSong = queue.songs[0];
|
|
3324
|
+
if (!currentSong) return;
|
|
3325
|
+
const songDuration = currentSong.duration ?? 0;
|
|
3326
|
+
if (!songDuration || !Number.isFinite(songDuration)) return;
|
|
3327
|
+
const fade = Math.min(Number(queue.crossfade) || 0, songDuration);
|
|
3328
|
+
if (fade <= 0) return;
|
|
3329
|
+
const nextSong = queue.repeatMode === 1 /* SONG */ ? currentSong : queue.songs[1];
|
|
3330
|
+
if (!nextSong) return;
|
|
3331
|
+
const currentTime = queue.currentTime;
|
|
3332
|
+
if (!Number.isFinite(currentTime)) return;
|
|
3333
|
+
const remaining = songDuration - currentTime;
|
|
3334
|
+
if (remaining <= fade) return;
|
|
3335
|
+
const delayMs = Math.max(0, (remaining - fade - this.options.crossfade.warmup) * 1e3);
|
|
3336
|
+
this.debug(
|
|
3337
|
+
`[${queue.id}] Scheduling crossfade (${fade}s fade) in ${(delayMs / 1e3).toFixed(1)}s: ${currentSong} -> ${nextSong}`
|
|
3338
|
+
);
|
|
3339
|
+
queue._crossfadeTimeout = setTimeout(() => {
|
|
3340
|
+
queue._crossfadeTimeout = void 0;
|
|
3341
|
+
void this.#crossfadeWarmupTask(queue);
|
|
3342
|
+
}, delayMs);
|
|
3343
|
+
queue._crossfadeTimeout.unref?.();
|
|
3344
|
+
}
|
|
3345
|
+
/**
|
|
3346
|
+
* Spawn the merged transition stream ahead of the fade window so both of
|
|
3347
|
+
* its inputs are buffered when the fade must start, then arm the boundary
|
|
3348
|
+
* timer that swaps the playback stream.
|
|
3349
|
+
*/
|
|
3350
|
+
async #crossfadeWarmupTask(queue) {
|
|
3351
|
+
let transitionStream;
|
|
3352
|
+
try {
|
|
3353
|
+
if (queue.stopped || queue.paused) return;
|
|
3354
|
+
const song = queue.songs[0];
|
|
3355
|
+
const generation = queue._preloadGeneration;
|
|
3356
|
+
const repeatSong = queue.repeatMode === 1 /* SONG */;
|
|
3357
|
+
const nextSong = repeatSong ? song : queue.songs[1];
|
|
3358
|
+
if (!song || !nextSong) return;
|
|
3359
|
+
const songDuration = song.duration ?? 0;
|
|
3360
|
+
const fade = Math.min(Number(queue.crossfade) || 0, songDuration);
|
|
3361
|
+
if (fade <= 0) return;
|
|
3362
|
+
const remaining = songDuration - queue.currentTime;
|
|
3363
|
+
if (remaining <= fade) return;
|
|
3364
|
+
if (!repeatSong) {
|
|
3365
|
+
if (this.options.preload.enabled) await this.preloadNextSong(queue);
|
|
3366
|
+
else await this.handler.attachStreamInfo(nextSong);
|
|
3367
|
+
}
|
|
3368
|
+
if (queue.stopped || queue.paused || queue.songs[0] !== song || generation !== queue._preloadGeneration) return;
|
|
3369
|
+
if (repeatSong ? queue.songs[0] !== song : queue.songs[1] !== nextSong) return;
|
|
3370
|
+
const primary = song.stream.playFromSource ? song : song.stream.song;
|
|
3371
|
+
const secondary = repeatSong ? primary : nextSong.stream.playFromSource ? nextSong : nextSong.stream.song;
|
|
3372
|
+
if (!primary?.stream.playFromSource || !primary.stream.url) return;
|
|
3373
|
+
if (!secondary?.stream.playFromSource || !secondary.stream.url) return;
|
|
3374
|
+
this.debug(`[${queue.id}] Warming up crossfade stream: ${song} -> ${nextSong}`);
|
|
3375
|
+
transitionStream = new DisTubeStream(primary.stream.url, {
|
|
3376
|
+
ffmpeg: {
|
|
3377
|
+
path: this.options.ffmpeg.path,
|
|
3378
|
+
args: {
|
|
3379
|
+
global: { ...queue.ffmpegArgs.global },
|
|
3380
|
+
input: { ...queue.ffmpegArgs.input },
|
|
3381
|
+
output: { ...queue.ffmpegArgs.output, ...queue.filters.ffmpegArgs }
|
|
3382
|
+
}
|
|
3383
|
+
},
|
|
3384
|
+
crossfade: {
|
|
3385
|
+
duration: fade,
|
|
3386
|
+
primarySeek: Math.max(0, songDuration - fade),
|
|
3387
|
+
secondaryUrl: secondary.stream.url,
|
|
3388
|
+
inCurve: this.options.crossfade.curve,
|
|
3389
|
+
outCurve: this.options.crossfade.curve
|
|
3390
|
+
}
|
|
3391
|
+
});
|
|
3392
|
+
transitionStream.on("debug", (data) => this.emit("ffmpegDebug" /* FFMPEG_DEBUG */, `[${queue.id}] [crossfade] ${data}`));
|
|
3393
|
+
if (queue.stopped || queue.paused || queue.songs[0] !== song || generation !== queue._preloadGeneration) {
|
|
3394
|
+
transitionStream.kill();
|
|
2864
3395
|
return;
|
|
2865
3396
|
}
|
|
2866
|
-
|
|
3397
|
+
queue._crossfadeWarmup = transitionStream;
|
|
3398
|
+
transitionStream.spawn();
|
|
3399
|
+
const boundaryMs = Math.max(0, (remaining - fade) * 1e3);
|
|
3400
|
+
queue._crossfadeTimeout = setTimeout(() => {
|
|
3401
|
+
queue._crossfadeTimeout = void 0;
|
|
3402
|
+
void this.#crossfadeSwapTask(queue, transitionStream);
|
|
3403
|
+
}, boundaryMs);
|
|
3404
|
+
queue._crossfadeTimeout.unref?.();
|
|
3405
|
+
} catch (e) {
|
|
3406
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
3407
|
+
this.debug(`[${queue.id}] Crossfade warmup failed, using the regular transition: ${errorMessage}`);
|
|
3408
|
+
if (transitionStream) transitionStream.kill();
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Swap the playing stream for the merged transition stream and update the
|
|
3413
|
+
* queue as if the current song had just finished.
|
|
3414
|
+
*/
|
|
3415
|
+
async #crossfadeSwapTask(queue, transitionStream) {
|
|
3416
|
+
const abort = /* @__PURE__ */ __name(() => {
|
|
3417
|
+
if (queue._crossfadeWarmup === transitionStream) queue._crossfadeWarmup = void 0;
|
|
3418
|
+
transitionStream.kill();
|
|
3419
|
+
}, "abort");
|
|
3420
|
+
try {
|
|
3421
|
+
if (queue.stopped || queue.paused || queue._crossfadeWarmup !== transitionStream) return abort();
|
|
3422
|
+
const song = queue.songs[0];
|
|
3423
|
+
if (!song) return abort();
|
|
3424
|
+
const songDuration = song.duration ?? 0;
|
|
3425
|
+
const fade = Math.min(Number(queue.crossfade) || 0, songDuration);
|
|
3426
|
+
if (fade <= 0) return abort();
|
|
3427
|
+
const drift = queue.currentTime - (songDuration - fade);
|
|
3428
|
+
if (Math.abs(drift) > CROSSFADE_DRIFT_TOLERANCE_SECS) {
|
|
3429
|
+
this.debug(`[${queue.id}] Crossfade aborted: playback drifted ${drift.toFixed(2)}s from the fade boundary`);
|
|
3430
|
+
return abort();
|
|
3431
|
+
}
|
|
3432
|
+
await queue._taskQueue.queuing();
|
|
3433
|
+
try {
|
|
3434
|
+
if (queue.stopped || queue.paused || queue.songs[0] !== song || queue._crossfadeWarmup !== transitionStream) {
|
|
3435
|
+
return abort();
|
|
3436
|
+
}
|
|
3437
|
+
this.debug(`[${queue.id}] Starting crossfade: ${song} -> ${queue.songs.length > 1 ? queue.songs[1] : song}`);
|
|
3438
|
+
await queue.voice.play(transitionStream);
|
|
3439
|
+
queue._crossfadeWarmup = void 0;
|
|
3440
|
+
this.emit("finishSong" /* FINISH_SONG */, queue, song);
|
|
3441
|
+
if (queue.repeatMode === 2 /* QUEUE */) queue.songs.push(song);
|
|
3442
|
+
if (queue.repeatMode !== 1 /* SONG */) {
|
|
3443
|
+
const prev = queue.songs.shift();
|
|
3444
|
+
if (this.options.savePreviousSongs) queue.previousSongs.push(prev);
|
|
3445
|
+
else queue.previousSongs.push({ id: prev.id });
|
|
3446
|
+
} else {
|
|
3447
|
+
}
|
|
3448
|
+
if (song !== queue.songs[0]) {
|
|
3449
|
+
const playedSong = song.stream.playFromSource ? song : song.stream.song;
|
|
3450
|
+
if (playedSong?.stream.playFromSource) delete playedSong.stream.url;
|
|
3451
|
+
}
|
|
3452
|
+
if (queue._preloadedStream) {
|
|
3453
|
+
queue._preloadedStream.kill();
|
|
3454
|
+
queue._preloadedStream = void 0;
|
|
3455
|
+
queue._preloadedSong = void 0;
|
|
3456
|
+
}
|
|
3457
|
+
this.emit("playSong" /* PLAY_SONG */, queue, queue.songs[0]);
|
|
3458
|
+
this.schedulePreload(queue);
|
|
3459
|
+
this.scheduleCrossfade(queue);
|
|
3460
|
+
} finally {
|
|
3461
|
+
queue._taskQueue.resolve();
|
|
3462
|
+
}
|
|
3463
|
+
} catch (e) {
|
|
3464
|
+
abort();
|
|
3465
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
3466
|
+
this.debug(`[${queue.id}] Crossfade swap failed, using the regular transition: ${errorMessage}`);
|
|
2867
3467
|
}
|
|
2868
3468
|
}
|
|
2869
3469
|
};
|
|
@@ -3879,6 +4479,11 @@ export {
|
|
|
3879
4479
|
AUDIO_CHANNELS,
|
|
3880
4480
|
AUDIO_SAMPLE_RATE,
|
|
3881
4481
|
BaseManager,
|
|
4482
|
+
CROSSFADE_DRIFT_TOLERANCE_SECS,
|
|
4483
|
+
DEFAULT_CROSSFADE_CURVE,
|
|
4484
|
+
DEFAULT_CROSSFADE_DURATION_SECS,
|
|
4485
|
+
DEFAULT_CROSSFADE_WARMUP_SECS,
|
|
4486
|
+
DEFAULT_PRELOAD_THRESHOLD_SECS,
|
|
3882
4487
|
DEFAULT_VOLUME,
|
|
3883
4488
|
DisTube,
|
|
3884
4489
|
DisTubeBase,
|
|
@@ -3894,6 +4499,7 @@ export {
|
|
|
3894
4499
|
HTTP_REDIRECT_CODES,
|
|
3895
4500
|
InfoExtractorPlugin,
|
|
3896
4501
|
JOIN_TIMEOUT_MS,
|
|
4502
|
+
MAX_CROSSFADE_DURATION_SECS,
|
|
3897
4503
|
MAX_REDIRECT_DEPTH,
|
|
3898
4504
|
Options,
|
|
3899
4505
|
PlayableExtractorPlugin,
|