ziplayer 0.3.5 → 0.3.7
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/plugins/index.d.ts +6 -15
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +214 -219
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.d.ts +2 -0
- package/dist/structures/FilterManager.d.ts.map +1 -1
- package/dist/structures/FilterManager.js +123 -58
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/Player.d.ts +7 -1
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +172 -80
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +4 -0
- package/dist/structures/Queue.js.map +1 -1
- package/dist/structures/StreamManager.d.ts +7 -0
- package/dist/structures/StreamManager.d.ts.map +1 -1
- package/dist/structures/StreamManager.js +23 -0
- package/dist/structures/StreamManager.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/index.ts +252 -262
- package/src/structures/FilterManager.ts +139 -62
- package/src/structures/Player.ts +192 -87
- package/src/structures/Queue.ts +5 -0
- package/src/structures/StreamManager.ts +583 -563
- package/src/types/index.ts +2 -0
package/src/structures/Player.ts
CHANGED
|
@@ -118,6 +118,7 @@ export class Player extends EventEmitter {
|
|
|
118
118
|
|
|
119
119
|
private skipLoop = false;
|
|
120
120
|
private refreshLock = false;
|
|
121
|
+
private seekInProgress = false;
|
|
121
122
|
private remoteHandle: StreamInfo["handle"];
|
|
122
123
|
|
|
123
124
|
private currentSlot: StreamSlot = {
|
|
@@ -171,6 +172,7 @@ export class Player extends EventEmitter {
|
|
|
171
172
|
private readonly SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
172
173
|
private ttsPlayer: DiscordAudioPlayer | null = null;
|
|
173
174
|
private lastDuration: number = 0;
|
|
175
|
+
private seekOffset: number = 0;
|
|
174
176
|
|
|
175
177
|
constructor(guildId: string, options: PlayerOptions = {}, manager: PlayerManager) {
|
|
176
178
|
super();
|
|
@@ -276,7 +278,12 @@ export class Player extends EventEmitter {
|
|
|
276
278
|
this.preloadManager = new PreloadManager({
|
|
277
279
|
streamManager: this.streamManager,
|
|
278
280
|
debug: this.debug.bind(this),
|
|
279
|
-
getNextTrack: () =>
|
|
281
|
+
getNextTrack: () => {
|
|
282
|
+
if (this.queue.loop() === "track") {
|
|
283
|
+
return this.queue.currentTrack;
|
|
284
|
+
}
|
|
285
|
+
return this.queue.nextTrack;
|
|
286
|
+
},
|
|
280
287
|
getStream: (track) => this.getStream(track),
|
|
281
288
|
isDestroyed: () => this.destroyed,
|
|
282
289
|
isEnabled: () => this.preloadEnabled,
|
|
@@ -882,46 +889,31 @@ export class Player extends EventEmitter {
|
|
|
882
889
|
*/
|
|
883
890
|
private async createResource(streamInfo: StreamInfo, track: Track, position: number = 0): Promise<AudioResource> {
|
|
884
891
|
const filterString = this.filter.getFilterString();
|
|
892
|
+
this.debug(`[Player] Creating AudioResource — filters: ${filterString || "none"}, seek: ${position}ms`);
|
|
885
893
|
|
|
886
|
-
|
|
894
|
+
// -1 = sentinel "no seek requested"
|
|
895
|
+
const seekArg = position > 0 ? position : -1;
|
|
887
896
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
stream = await this.filter.applyFiltersAndSeek(streamInfo.stream, position);
|
|
893
|
-
streamInfo.type = StreamType.Arbitrary;
|
|
894
|
-
}
|
|
897
|
+
if (filterString || position > 0) {
|
|
898
|
+
// throws on failure — do NOT fall back to the already-piped stream
|
|
899
|
+
const processedStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, seekArg);
|
|
900
|
+
streamInfo.type = StreamType.Arbitrary as any;
|
|
895
901
|
|
|
896
|
-
|
|
897
|
-
const resource = createAudioResource(stream, {
|
|
902
|
+
return createAudioResource(processedStream, {
|
|
898
903
|
metadata: track,
|
|
899
|
-
inputType:
|
|
900
|
-
streamInfo.type === "webm/opus" ? StreamType.WebmOpus
|
|
901
|
-
: streamInfo.type === "ogg/opus" ? StreamType.OggOpus
|
|
902
|
-
: StreamType.Arbitrary,
|
|
904
|
+
inputType: StreamType.Arbitrary,
|
|
903
905
|
inlineVolume: true,
|
|
904
906
|
});
|
|
905
|
-
|
|
906
|
-
return resource;
|
|
907
|
-
} catch (error) {
|
|
908
|
-
this.debug(`[Player] Error creating AudioResource with filters+seek:`, error);
|
|
909
|
-
// Fallback to basic AudioResource
|
|
910
|
-
try {
|
|
911
|
-
const resource = createAudioResource(streamInfo.stream, {
|
|
912
|
-
metadata: track,
|
|
913
|
-
inputType:
|
|
914
|
-
streamInfo.type === "webm/opus" ? StreamType.WebmOpus
|
|
915
|
-
: streamInfo.type === "ogg/opus" ? StreamType.OggOpus
|
|
916
|
-
: StreamType.Arbitrary,
|
|
917
|
-
inlineVolume: true,
|
|
918
|
-
});
|
|
919
|
-
return resource;
|
|
920
|
-
} catch (fallbackError) {
|
|
921
|
-
this.debug(`[Player] Fallback AudioResource creation failed:`, fallbackError);
|
|
922
|
-
throw fallbackError;
|
|
923
|
-
}
|
|
924
907
|
}
|
|
908
|
+
|
|
909
|
+
return createAudioResource(streamInfo.stream, {
|
|
910
|
+
metadata: track,
|
|
911
|
+
inputType:
|
|
912
|
+
streamInfo.type === "webm/opus" ? StreamType.WebmOpus
|
|
913
|
+
: streamInfo.type === "ogg/opus" ? StreamType.OggOpus
|
|
914
|
+
: StreamType.Arbitrary,
|
|
915
|
+
inlineVolume: true,
|
|
916
|
+
});
|
|
925
917
|
}
|
|
926
918
|
|
|
927
919
|
private mergeTrackPreserveRef(target: Track, source: Track): void {
|
|
@@ -1056,8 +1048,6 @@ export class Player extends EventEmitter {
|
|
|
1056
1048
|
if (this.preloadManager.hasValidPreload(track)) {
|
|
1057
1049
|
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
1058
1050
|
|
|
1059
|
-
this.audioPlayer.stop(true);
|
|
1060
|
-
|
|
1061
1051
|
const oldStreamId = this.currentSlot.streamId;
|
|
1062
1052
|
if (oldStreamId && this.streamManager) {
|
|
1063
1053
|
setTimeout(() => {
|
|
@@ -1072,6 +1062,7 @@ export class Player extends EventEmitter {
|
|
|
1072
1062
|
if (!currentResource) {
|
|
1073
1063
|
return false;
|
|
1074
1064
|
}
|
|
1065
|
+
this.seekOffset = 0;
|
|
1075
1066
|
const targetVolume = this.getTrackTargetVolume(track);
|
|
1076
1067
|
|
|
1077
1068
|
if (currentResource.volume) {
|
|
@@ -1079,8 +1070,14 @@ export class Player extends EventEmitter {
|
|
|
1079
1070
|
}
|
|
1080
1071
|
|
|
1081
1072
|
await this.maybeAlignToBeatBoundary();
|
|
1082
|
-
this.
|
|
1083
|
-
|
|
1073
|
+
this.refreshLock = true;
|
|
1074
|
+
try {
|
|
1075
|
+
this.audioPlayer.stop(true);
|
|
1076
|
+
this.audioPlayer.play(currentResource);
|
|
1077
|
+
await entersState(this.audioPlayer, AudioPlayerStatus.Playing, 10_000);
|
|
1078
|
+
} finally {
|
|
1079
|
+
this.refreshLock = false;
|
|
1080
|
+
}
|
|
1084
1081
|
await this.applyCrossfadeIn(currentResource, track);
|
|
1085
1082
|
|
|
1086
1083
|
this.preloadNextTrack().catch((err) => {
|
|
@@ -1142,6 +1139,7 @@ export class Player extends EventEmitter {
|
|
|
1142
1139
|
this.currentSlot.streamId = streamId;
|
|
1143
1140
|
this.currentSlot.isValid = true;
|
|
1144
1141
|
this.currentResource = resource;
|
|
1142
|
+
this.seekOffset = 0; // new track — reset seek baseline
|
|
1145
1143
|
|
|
1146
1144
|
// Apply volume
|
|
1147
1145
|
const targetVolume = this.getTrackTargetVolume(track);
|
|
@@ -1149,11 +1147,16 @@ export class Player extends EventEmitter {
|
|
|
1149
1147
|
resource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
1150
1148
|
}
|
|
1151
1149
|
|
|
1152
|
-
// Play
|
|
1150
|
+
// Play — lock refresh so Idle event doesn't spawn duplicate playNext
|
|
1153
1151
|
await this.maybeAlignToBeatBoundary();
|
|
1154
|
-
this.
|
|
1155
|
-
|
|
1156
|
-
|
|
1152
|
+
this.refreshLock = true;
|
|
1153
|
+
try {
|
|
1154
|
+
this.audioPlayer.stop(true);
|
|
1155
|
+
this.audioPlayer.play(resource);
|
|
1156
|
+
await entersState(this.audioPlayer, AudioPlayerStatus.Playing, 10_000);
|
|
1157
|
+
} finally {
|
|
1158
|
+
this.refreshLock = false;
|
|
1159
|
+
}
|
|
1157
1160
|
await this.applyCrossfadeIn(resource, track);
|
|
1158
1161
|
|
|
1159
1162
|
// Preload next (async)
|
|
@@ -1239,6 +1242,9 @@ export class Player extends EventEmitter {
|
|
|
1239
1242
|
if (this.antiStuckRetryDelayMs > 0) {
|
|
1240
1243
|
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1241
1244
|
}
|
|
1245
|
+
} else {
|
|
1246
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
1247
|
+
this.skipLoop = true;
|
|
1242
1248
|
}
|
|
1243
1249
|
} catch (err) {
|
|
1244
1250
|
this.debug(`[Player] playNext error:`, err);
|
|
@@ -1263,6 +1269,9 @@ export class Player extends EventEmitter {
|
|
|
1263
1269
|
if (this.antiStuckRetryDelayMs > 0) {
|
|
1264
1270
|
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1265
1271
|
}
|
|
1272
|
+
} else {
|
|
1273
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
1274
|
+
this.skipLoop = true;
|
|
1266
1275
|
}
|
|
1267
1276
|
continue;
|
|
1268
1277
|
}
|
|
@@ -1410,15 +1419,21 @@ export class Player extends EventEmitter {
|
|
|
1410
1419
|
* @example
|
|
1411
1420
|
* await player.connect(voiceChannel);
|
|
1412
1421
|
*/
|
|
1413
|
-
async connect(
|
|
1422
|
+
async connect(
|
|
1423
|
+
channel: VoiceChannel,
|
|
1424
|
+
options: { group: string; selfDeaf: boolean; selfMute: boolean },
|
|
1425
|
+
): Promise<VoiceConnection> {
|
|
1414
1426
|
try {
|
|
1415
1427
|
this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
|
|
1428
|
+
|
|
1416
1429
|
const connection = joinVoiceChannel({
|
|
1430
|
+
...options,
|
|
1417
1431
|
channelId: channel.id,
|
|
1418
1432
|
guildId: channel.guildId,
|
|
1419
1433
|
adapterCreator: channel.guild.voiceAdapterCreator as any,
|
|
1420
|
-
selfDeaf: this.options
|
|
1421
|
-
selfMute: this.options
|
|
1434
|
+
selfDeaf: options?.selfDeaf ?? this.options?.selfDeaf ?? true,
|
|
1435
|
+
selfMute: options?.selfMute ?? this.options?.selfMute ?? false,
|
|
1436
|
+
group: options?.group ?? this.options?.group ?? "Ziplayer",
|
|
1422
1437
|
});
|
|
1423
1438
|
|
|
1424
1439
|
await entersState(connection, VoiceConnectionStatus.Ready, 50_000);
|
|
@@ -1772,8 +1787,11 @@ export class Player extends EventEmitter {
|
|
|
1772
1787
|
return false;
|
|
1773
1788
|
}
|
|
1774
1789
|
|
|
1775
|
-
await this.refreshPlayerResource(true, position);
|
|
1776
|
-
|
|
1790
|
+
const ok = await this.refreshPlayerResource(true, position);
|
|
1791
|
+
if (!ok) {
|
|
1792
|
+
this.debug(`[Player] Seek failed at position: ${position}ms`);
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1777
1795
|
return true;
|
|
1778
1796
|
}
|
|
1779
1797
|
|
|
@@ -1956,10 +1974,10 @@ export class Player extends EventEmitter {
|
|
|
1956
1974
|
* console.log(`Loop mode: ${loopMode}`);
|
|
1957
1975
|
*/
|
|
1958
1976
|
loop(mode?: LoopMode | number): LoopMode {
|
|
1959
|
-
this.debug(`[Player] loop called with mode: ${mode}`);
|
|
1960
|
-
|
|
1961
1977
|
if (typeof mode === "number") {
|
|
1962
1978
|
// Number mode: convert to text mode
|
|
1979
|
+
this.debug(`[Player] loop called with mode: ${mode}`);
|
|
1980
|
+
|
|
1963
1981
|
switch (mode) {
|
|
1964
1982
|
case 0:
|
|
1965
1983
|
return this.queue.loop("off");
|
|
@@ -2186,9 +2204,9 @@ export class Player extends EventEmitter {
|
|
|
2186
2204
|
}
|
|
2187
2205
|
|
|
2188
2206
|
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
2189
|
-
if (!total) return this.formatTimeCompact(resource.playbackDuration);
|
|
2207
|
+
if (!total) return this.formatTimeCompact(resource.playbackDuration + this.seekOffset);
|
|
2190
2208
|
|
|
2191
|
-
const current = resource.playbackDuration;
|
|
2209
|
+
const current = resource.playbackDuration + this.seekOffset;
|
|
2192
2210
|
const ratio = Math.min(Math.max(current / total, 0), 1);
|
|
2193
2211
|
const progress = Math.round(ratio * size);
|
|
2194
2212
|
|
|
@@ -2298,7 +2316,7 @@ export class Player extends EventEmitter {
|
|
|
2298
2316
|
}
|
|
2299
2317
|
|
|
2300
2318
|
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
2301
|
-
const current = resource.playbackDuration;
|
|
2319
|
+
const current = resource.playbackDuration + this.seekOffset;
|
|
2302
2320
|
|
|
2303
2321
|
return {
|
|
2304
2322
|
current: current,
|
|
@@ -2411,63 +2429,128 @@ export class Player extends EventEmitter {
|
|
|
2411
2429
|
if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
|
|
2412
2430
|
return false;
|
|
2413
2431
|
}
|
|
2414
|
-
if (this.refreshLock)
|
|
2432
|
+
if (this.refreshLock) {
|
|
2433
|
+
this.debug(`[Player] refreshPlayerResource skipped — lock held`);
|
|
2434
|
+
return false;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
// Lock before anything so stateChange idle sees it when stop() fires.
|
|
2415
2438
|
this.refreshLock = true;
|
|
2439
|
+
|
|
2440
|
+
// Clear any existing stuckTimer from the previous playback cycle so it
|
|
2441
|
+
// cannot fire while we are mid-refresh.
|
|
2442
|
+
if (this.stuckTimer) {
|
|
2443
|
+
clearTimeout(this.stuckTimer);
|
|
2444
|
+
this.stuckTimer = null;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2416
2447
|
try {
|
|
2417
2448
|
const track = this.queue.currentTrack;
|
|
2418
2449
|
this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
|
|
2419
2450
|
|
|
2420
|
-
|
|
2421
|
-
|
|
2451
|
+
const currentPosition = position >= 0 ? position : (this.currentResource?.playbackDuration ?? 0);
|
|
2452
|
+
this.seekOffset = currentPosition;
|
|
2453
|
+
const wasPaused = this.isPaused;
|
|
2454
|
+
const playbackDuration = this.currentResource?.playbackDuration ?? 0;
|
|
2455
|
+
|
|
2456
|
+
// Reuse is only viable for forward seeks (stream is sequential).
|
|
2457
|
+
const isForwardSeek = position < 0 || position >= playbackDuration;
|
|
2458
|
+
const currentStreamId = this.currentSlot.streamId;
|
|
2459
|
+
|
|
2460
|
+
// Try to grab the live source stream for reuse.
|
|
2461
|
+
// getRawStream accepts "paused" streams (discordjs/voice pauses source streams on NoSubscriberBehavior); getStream would reject them.
|
|
2462
|
+
let reuseStream: Readable | null = null;
|
|
2463
|
+
if (isForwardSeek && currentStreamId) {
|
|
2464
|
+
reuseStream = this.streamManager.getRawStream(currentStreamId);
|
|
2465
|
+
if (reuseStream) {
|
|
2466
|
+
this.debug(`[Player] Will reuse source stream for seek (pos: ${currentPosition}ms)`);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2422
2469
|
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2470
|
+
// ── CRITICAL: unpipe BEFORE stop ──────────────────────────────────────
|
|
2471
|
+
// stop() kills discordjs/voice internal FFmpeg → EPIPE on source stream.
|
|
2472
|
+
// unpipe() first disconnects our stream cleanly before that happens.
|
|
2473
|
+
if (reuseStream) {
|
|
2474
|
+
reuseStream.unpipe();
|
|
2427
2475
|
}
|
|
2428
2476
|
|
|
2429
|
-
//
|
|
2430
|
-
|
|
2477
|
+
// Remove StreamManager listeners.
|
|
2478
|
+
// forceDestroy=false when reusing so the Readable object stays alive.
|
|
2479
|
+
if (currentStreamId) {
|
|
2480
|
+
this.streamManager.unregisterStream(currentStreamId, !reuseStream);
|
|
2481
|
+
this.currentSlot.streamId = null;
|
|
2482
|
+
}
|
|
2431
2483
|
|
|
2432
|
-
// Stop
|
|
2433
|
-
|
|
2434
|
-
|
|
2484
|
+
// Stop the AudioPlayer.
|
|
2485
|
+
// stateChange (playing→idle) fires; refreshLock=true guards it (v4 fix).
|
|
2486
|
+
this.audioPlayer.stop(true);
|
|
2487
|
+
this.currentResource = null;
|
|
2488
|
+
this.currentSlot.resource = null;
|
|
2489
|
+
this.currentSlot.isValid = false;
|
|
2490
|
+
|
|
2491
|
+
// One event-loop tick: lets deferred stream events settle.
|
|
2492
|
+
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
2493
|
+
|
|
2494
|
+
// Verify the reuse stream survived stop().
|
|
2495
|
+
if (reuseStream) {
|
|
2496
|
+
if (reuseStream.destroyed || (reuseStream as any).readable === false) {
|
|
2497
|
+
this.debug(`[Player] Source stream did not survive stop — falling back to fresh stream`);
|
|
2498
|
+
reuseStream = null;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2435
2501
|
|
|
2436
|
-
|
|
2502
|
+
let streaminfo: StreamInfo | null = null;
|
|
2437
2503
|
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
this.
|
|
2504
|
+
if (reuseStream) {
|
|
2505
|
+
streaminfo = { stream: reuseStream, type: "arbitrary" };
|
|
2506
|
+
} else {
|
|
2507
|
+
// Clear caches so we don't get the dead Readable back.
|
|
2508
|
+
this.pluginManager.clearStreamCache();
|
|
2509
|
+
this.extensionManager.clearCache("stream");
|
|
2510
|
+
this.debug(`[Player] Fetching fresh stream${!isForwardSeek ? " (backward seek)" : " (reuse failed)"}`);
|
|
2511
|
+
streaminfo = await this.getStream(track);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
if (!streaminfo?.stream) {
|
|
2515
|
+
this.debug(`[Player] No stream available for refresh`);
|
|
2516
|
+
return false;
|
|
2450
2517
|
}
|
|
2451
2518
|
|
|
2519
|
+
// Build AudioResource (input-side FFmpeg seek via FilterManager).
|
|
2520
|
+
const resource = await this.createResource(streaminfo, track, currentPosition);
|
|
2521
|
+
|
|
2522
|
+
// Register the source stream.
|
|
2523
|
+
const newStreamId = this.streamManager.registerStream(streaminfo.stream, track, {
|
|
2524
|
+
source: track.source || "stream",
|
|
2525
|
+
isPreload: false,
|
|
2526
|
+
priority: 10,
|
|
2527
|
+
});
|
|
2528
|
+
this.currentSlot.resource = resource;
|
|
2529
|
+
this.currentSlot.track = track;
|
|
2530
|
+
this.currentSlot.streamId = newStreamId;
|
|
2531
|
+
this.currentSlot.isValid = true;
|
|
2452
2532
|
this.currentResource = resource;
|
|
2453
2533
|
|
|
2454
|
-
//
|
|
2534
|
+
// ── Set seek flag BEFORE play so the Buffering handler sees it ────────
|
|
2535
|
+
if (position >= 0) {
|
|
2536
|
+
this.seekInProgress = true;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2455
2539
|
if (this.connection) {
|
|
2456
2540
|
this.connection.subscribe(this.audioPlayer);
|
|
2457
2541
|
this.audioPlayer.play(resource);
|
|
2458
2542
|
}
|
|
2543
|
+
if (wasPaused) this.audioPlayer.pause();
|
|
2459
2544
|
|
|
2460
|
-
|
|
2461
|
-
if (wasPaused) {
|
|
2462
|
-
this.audioPlayer.pause();
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
this.debug(`[Player] Successfully applied filter to current track at position ${currentPosition}ms`);
|
|
2545
|
+
this.debug(`[Player] Resource refreshed at position ${currentPosition}ms`);
|
|
2466
2546
|
return true;
|
|
2467
2547
|
} catch (error) {
|
|
2468
|
-
this.debug(`[Player]
|
|
2469
|
-
|
|
2470
|
-
|
|
2548
|
+
this.debug(`[Player] refreshPlayerResource error:`, error);
|
|
2549
|
+
this.seekInProgress = false; // ensure flag is cleared on failure
|
|
2550
|
+
this.emit("playerError", error as Error, this.queue.currentTrack ?? undefined);
|
|
2551
|
+
return false;
|
|
2552
|
+
} finally {
|
|
2553
|
+
this.refreshLock = false; // always released
|
|
2471
2554
|
}
|
|
2472
2555
|
}
|
|
2473
2556
|
|
|
@@ -2523,7 +2606,13 @@ export class Player extends EventEmitter {
|
|
|
2523
2606
|
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
|
2524
2607
|
if (this.destroyed) return;
|
|
2525
2608
|
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
2609
|
+
|
|
2610
|
+
// ── Idle: track ended naturally ───────────────────────────────────────
|
|
2526
2611
|
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
|
|
2612
|
+
if (this.refreshLock) {
|
|
2613
|
+
this.debug(`[Player] AudioPlayer went idle during resource refresh — skipping trackEnd/playNext`);
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2527
2616
|
// Track ended
|
|
2528
2617
|
const track = this.queue.currentTrack;
|
|
2529
2618
|
if (track) {
|
|
@@ -2534,12 +2623,19 @@ export class Player extends EventEmitter {
|
|
|
2534
2623
|
}
|
|
2535
2624
|
}
|
|
2536
2625
|
void this.playNext();
|
|
2626
|
+
// ── Playing: started or resumed ───────────────────────────────────────
|
|
2537
2627
|
} else if (
|
|
2538
2628
|
newState.status === AudioPlayerStatus.Playing &&
|
|
2539
2629
|
(oldState.status === AudioPlayerStatus.Idle || oldState.status === AudioPlayerStatus.Buffering)
|
|
2540
2630
|
) {
|
|
2541
2631
|
// Track started
|
|
2542
2632
|
this.clearLeaveTimeout();
|
|
2633
|
+
|
|
2634
|
+
if (this.seekInProgress) {
|
|
2635
|
+
this.debug(`[Player] Seek complete — audio output started`);
|
|
2636
|
+
this.seekInProgress = false;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2543
2639
|
const track = this.queue.currentTrack;
|
|
2544
2640
|
if (track) {
|
|
2545
2641
|
this.debug(`[Player] Track started: ${track.title}`);
|
|
@@ -2556,6 +2652,7 @@ export class Player extends EventEmitter {
|
|
|
2556
2652
|
}
|
|
2557
2653
|
}
|
|
2558
2654
|
}
|
|
2655
|
+
// ── Paused ────────────────────────────────────────────────────────────
|
|
2559
2656
|
} else if (newState.status === AudioPlayerStatus.Paused && oldState.status !== AudioPlayerStatus.Paused) {
|
|
2560
2657
|
const track = this.queue.currentTrack;
|
|
2561
2658
|
if (track) {
|
|
@@ -2565,6 +2662,7 @@ export class Player extends EventEmitter {
|
|
|
2565
2662
|
fp.emit("playerPause", track);
|
|
2566
2663
|
}
|
|
2567
2664
|
}
|
|
2665
|
+
// ── Resumed from pause ────────────────────────────────────────────────
|
|
2568
2666
|
} else if (newState.status !== AudioPlayerStatus.Paused && oldState.status === AudioPlayerStatus.Paused) {
|
|
2569
2667
|
const track = this.queue.currentTrack;
|
|
2570
2668
|
if (track) {
|
|
@@ -2576,8 +2674,15 @@ export class Player extends EventEmitter {
|
|
|
2576
2674
|
}
|
|
2577
2675
|
} else if (newState.status === AudioPlayerStatus.AutoPaused) {
|
|
2578
2676
|
this.debug(`[Player] AudioPlayerStatus.AutoPaused`);
|
|
2677
|
+
// ── Buffering: start stuck detector ───────────────────────────────────
|
|
2579
2678
|
} else if (newState.status === AudioPlayerStatus.Buffering) {
|
|
2580
2679
|
this.debug(`[Player] AudioPlayerStatus.Buffering`);
|
|
2680
|
+
|
|
2681
|
+
if (this.seekInProgress) {
|
|
2682
|
+
this.debug(`[Player] Buffering during seek — stuckTimer suppressed`);
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2581
2686
|
this.lastDuration = this.currentResource?.playbackDuration || 0;
|
|
2582
2687
|
this.stuckTimer = setTimeout(() => {
|
|
2583
2688
|
if (this.currentResource?.playbackDuration === this.lastDuration) {
|
package/src/structures/Queue.ts
CHANGED
|
@@ -231,6 +231,11 @@ export class Queue {
|
|
|
231
231
|
this.current = this.tracks.shift() || null;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// Skip bypassed track loop but no other track exists → restore current from history
|
|
235
|
+
if (!this.current && this._loop === "track" && ignoreLoop && this.history.length > 0) {
|
|
236
|
+
this.current = this.history.pop() || null;
|
|
237
|
+
}
|
|
238
|
+
|
|
234
239
|
return this.current;
|
|
235
240
|
}
|
|
236
241
|
|