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
|
@@ -64,6 +64,7 @@ class Player extends events_1.EventEmitter {
|
|
|
64
64
|
this.stuckTimer = null;
|
|
65
65
|
this.skipLoop = false;
|
|
66
66
|
this.refreshLock = false;
|
|
67
|
+
this.seekInProgress = false;
|
|
67
68
|
this.currentSlot = {
|
|
68
69
|
resource: null,
|
|
69
70
|
track: null,
|
|
@@ -110,6 +111,7 @@ class Player extends events_1.EventEmitter {
|
|
|
110
111
|
this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
111
112
|
this.ttsPlayer = null;
|
|
112
113
|
this.lastDuration = 0;
|
|
114
|
+
this.seekOffset = 0;
|
|
113
115
|
this.debug(`[Player] Constructor called for guildId: ${guildId}`);
|
|
114
116
|
this.guildId = guildId;
|
|
115
117
|
this.queue = new Queue_1.Queue();
|
|
@@ -200,7 +202,12 @@ class Player extends events_1.EventEmitter {
|
|
|
200
202
|
this.preloadManager = new PreloadManager_1.PreloadManager({
|
|
201
203
|
streamManager: this.streamManager,
|
|
202
204
|
debug: this.debug.bind(this),
|
|
203
|
-
getNextTrack: () =>
|
|
205
|
+
getNextTrack: () => {
|
|
206
|
+
if (this.queue.loop() === "track") {
|
|
207
|
+
return this.queue.currentTrack;
|
|
208
|
+
}
|
|
209
|
+
return this.queue.nextTrack;
|
|
210
|
+
},
|
|
204
211
|
getStream: (track) => this.getStream(track),
|
|
205
212
|
isDestroyed: () => this.destroyed,
|
|
206
213
|
isEnabled: () => this.preloadEnabled,
|
|
@@ -743,42 +750,26 @@ class Player extends events_1.EventEmitter {
|
|
|
743
750
|
*/
|
|
744
751
|
async createResource(streamInfo, track, position = 0) {
|
|
745
752
|
const filterString = this.filter.getFilterString();
|
|
746
|
-
this.debug(`[Player] Creating AudioResource
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
// Create AudioResource with better error handling
|
|
755
|
-
const resource = (0, voice_1.createAudioResource)(stream, {
|
|
753
|
+
this.debug(`[Player] Creating AudioResource — filters: ${filterString || "none"}, seek: ${position}ms`);
|
|
754
|
+
// -1 = sentinel "no seek requested"
|
|
755
|
+
const seekArg = position > 0 ? position : -1;
|
|
756
|
+
if (filterString || position > 0) {
|
|
757
|
+
// throws on failure — do NOT fall back to the already-piped stream
|
|
758
|
+
const processedStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, seekArg);
|
|
759
|
+
streamInfo.type = voice_1.StreamType.Arbitrary;
|
|
760
|
+
return (0, voice_1.createAudioResource)(processedStream, {
|
|
756
761
|
metadata: track,
|
|
757
|
-
inputType:
|
|
758
|
-
: streamInfo.type === "ogg/opus" ? voice_1.StreamType.OggOpus
|
|
759
|
-
: voice_1.StreamType.Arbitrary,
|
|
762
|
+
inputType: voice_1.StreamType.Arbitrary,
|
|
760
763
|
inlineVolume: true,
|
|
761
764
|
});
|
|
762
|
-
return resource;
|
|
763
|
-
}
|
|
764
|
-
catch (error) {
|
|
765
|
-
this.debug(`[Player] Error creating AudioResource with filters+seek:`, error);
|
|
766
|
-
// Fallback to basic AudioResource
|
|
767
|
-
try {
|
|
768
|
-
const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
769
|
-
metadata: track,
|
|
770
|
-
inputType: streamInfo.type === "webm/opus" ? voice_1.StreamType.WebmOpus
|
|
771
|
-
: streamInfo.type === "ogg/opus" ? voice_1.StreamType.OggOpus
|
|
772
|
-
: voice_1.StreamType.Arbitrary,
|
|
773
|
-
inlineVolume: true,
|
|
774
|
-
});
|
|
775
|
-
return resource;
|
|
776
|
-
}
|
|
777
|
-
catch (fallbackError) {
|
|
778
|
-
this.debug(`[Player] Fallback AudioResource creation failed:`, fallbackError);
|
|
779
|
-
throw fallbackError;
|
|
780
|
-
}
|
|
781
765
|
}
|
|
766
|
+
return (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
767
|
+
metadata: track,
|
|
768
|
+
inputType: streamInfo.type === "webm/opus" ? voice_1.StreamType.WebmOpus
|
|
769
|
+
: streamInfo.type === "ogg/opus" ? voice_1.StreamType.OggOpus
|
|
770
|
+
: voice_1.StreamType.Arbitrary,
|
|
771
|
+
inlineVolume: true,
|
|
772
|
+
});
|
|
782
773
|
}
|
|
783
774
|
mergeTrackPreserveRef(target, source) {
|
|
784
775
|
if (source === target)
|
|
@@ -897,7 +888,6 @@ class Player extends events_1.EventEmitter {
|
|
|
897
888
|
// Try to use preloaded resource
|
|
898
889
|
if (this.preloadManager.hasValidPreload(track)) {
|
|
899
890
|
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
900
|
-
this.audioPlayer.stop(true);
|
|
901
891
|
const oldStreamId = this.currentSlot.streamId;
|
|
902
892
|
if (oldStreamId && this.streamManager) {
|
|
903
893
|
setTimeout(() => {
|
|
@@ -911,13 +901,21 @@ class Player extends events_1.EventEmitter {
|
|
|
911
901
|
if (!currentResource) {
|
|
912
902
|
return false;
|
|
913
903
|
}
|
|
904
|
+
this.seekOffset = 0;
|
|
914
905
|
const targetVolume = this.getTrackTargetVolume(track);
|
|
915
906
|
if (currentResource.volume) {
|
|
916
907
|
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
917
908
|
}
|
|
918
909
|
await this.maybeAlignToBeatBoundary();
|
|
919
|
-
this.
|
|
920
|
-
|
|
910
|
+
this.refreshLock = true;
|
|
911
|
+
try {
|
|
912
|
+
this.audioPlayer.stop(true);
|
|
913
|
+
this.audioPlayer.play(currentResource);
|
|
914
|
+
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
915
|
+
}
|
|
916
|
+
finally {
|
|
917
|
+
this.refreshLock = false;
|
|
918
|
+
}
|
|
921
919
|
await this.applyCrossfadeIn(currentResource, track);
|
|
922
920
|
this.preloadNextTrack().catch((err) => {
|
|
923
921
|
this.debug(`[Player] Preload error:`, err);
|
|
@@ -969,16 +967,23 @@ class Player extends events_1.EventEmitter {
|
|
|
969
967
|
this.currentSlot.streamId = streamId;
|
|
970
968
|
this.currentSlot.isValid = true;
|
|
971
969
|
this.currentResource = resource;
|
|
970
|
+
this.seekOffset = 0; // new track — reset seek baseline
|
|
972
971
|
// Apply volume
|
|
973
972
|
const targetVolume = this.getTrackTargetVolume(track);
|
|
974
973
|
if (resource.volume) {
|
|
975
974
|
resource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
976
975
|
}
|
|
977
|
-
// Play
|
|
976
|
+
// Play — lock refresh so Idle event doesn't spawn duplicate playNext
|
|
978
977
|
await this.maybeAlignToBeatBoundary();
|
|
979
|
-
this.
|
|
980
|
-
|
|
981
|
-
|
|
978
|
+
this.refreshLock = true;
|
|
979
|
+
try {
|
|
980
|
+
this.audioPlayer.stop(true);
|
|
981
|
+
this.audioPlayer.play(resource);
|
|
982
|
+
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
983
|
+
}
|
|
984
|
+
finally {
|
|
985
|
+
this.refreshLock = false;
|
|
986
|
+
}
|
|
982
987
|
await this.applyCrossfadeIn(resource, track);
|
|
983
988
|
// Preload next (async)
|
|
984
989
|
if (!this.destroyed) {
|
|
@@ -1053,6 +1058,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1053
1058
|
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1054
1059
|
}
|
|
1055
1060
|
}
|
|
1061
|
+
else {
|
|
1062
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
1063
|
+
this.skipLoop = true;
|
|
1064
|
+
}
|
|
1056
1065
|
}
|
|
1057
1066
|
catch (err) {
|
|
1058
1067
|
this.debug(`[Player] playNext error:`, err);
|
|
@@ -1076,6 +1085,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1076
1085
|
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1077
1086
|
}
|
|
1078
1087
|
}
|
|
1088
|
+
else {
|
|
1089
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
1090
|
+
this.skipLoop = true;
|
|
1091
|
+
}
|
|
1079
1092
|
continue;
|
|
1080
1093
|
}
|
|
1081
1094
|
}
|
|
@@ -1208,15 +1221,17 @@ class Player extends events_1.EventEmitter {
|
|
|
1208
1221
|
* @example
|
|
1209
1222
|
* await player.connect(voiceChannel);
|
|
1210
1223
|
*/
|
|
1211
|
-
async connect(channel) {
|
|
1224
|
+
async connect(channel, options) {
|
|
1212
1225
|
try {
|
|
1213
1226
|
this.debug(`[Player] Connecting to voice channel: ${channel.id}`);
|
|
1214
1227
|
const connection = (0, voice_1.joinVoiceChannel)({
|
|
1228
|
+
...options,
|
|
1215
1229
|
channelId: channel.id,
|
|
1216
1230
|
guildId: channel.guildId,
|
|
1217
1231
|
adapterCreator: channel.guild.voiceAdapterCreator,
|
|
1218
|
-
selfDeaf: this.options
|
|
1219
|
-
selfMute: this.options
|
|
1232
|
+
selfDeaf: options?.selfDeaf ?? this.options?.selfDeaf ?? true,
|
|
1233
|
+
selfMute: options?.selfMute ?? this.options?.selfMute ?? false,
|
|
1234
|
+
group: options?.group ?? this.options?.group ?? "Ziplayer",
|
|
1220
1235
|
});
|
|
1221
1236
|
await (0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Ready, 50_000);
|
|
1222
1237
|
this.connection = connection;
|
|
@@ -1526,7 +1541,11 @@ class Player extends events_1.EventEmitter {
|
|
|
1526
1541
|
this.debug(`[Player] Invalid seek position: ${position}ms (track duration: ${totalDuration}ms)`);
|
|
1527
1542
|
return false;
|
|
1528
1543
|
}
|
|
1529
|
-
await this.refreshPlayerResource(true, position);
|
|
1544
|
+
const ok = await this.refreshPlayerResource(true, position);
|
|
1545
|
+
if (!ok) {
|
|
1546
|
+
this.debug(`[Player] Seek failed at position: ${position}ms`);
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1530
1549
|
return true;
|
|
1531
1550
|
}
|
|
1532
1551
|
/**
|
|
@@ -1697,9 +1716,9 @@ class Player extends events_1.EventEmitter {
|
|
|
1697
1716
|
* console.log(`Loop mode: ${loopMode}`);
|
|
1698
1717
|
*/
|
|
1699
1718
|
loop(mode) {
|
|
1700
|
-
this.debug(`[Player] loop called with mode: ${mode}`);
|
|
1701
1719
|
if (typeof mode === "number") {
|
|
1702
1720
|
// Number mode: convert to text mode
|
|
1721
|
+
this.debug(`[Player] loop called with mode: ${mode}`);
|
|
1703
1722
|
switch (mode) {
|
|
1704
1723
|
case 0:
|
|
1705
1724
|
return this.queue.loop("off");
|
|
@@ -1909,8 +1928,8 @@ class Player extends events_1.EventEmitter {
|
|
|
1909
1928
|
}
|
|
1910
1929
|
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
1911
1930
|
if (!total)
|
|
1912
|
-
return this.formatTimeCompact(resource.playbackDuration);
|
|
1913
|
-
const current = resource.playbackDuration;
|
|
1931
|
+
return this.formatTimeCompact(resource.playbackDuration + this.seekOffset);
|
|
1932
|
+
const current = resource.playbackDuration + this.seekOffset;
|
|
1914
1933
|
const ratio = Math.min(Math.max(current / total, 0), 1);
|
|
1915
1934
|
const progress = Math.round(ratio * size);
|
|
1916
1935
|
// Build progress bar
|
|
@@ -2012,7 +2031,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2012
2031
|
};
|
|
2013
2032
|
}
|
|
2014
2033
|
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
2015
|
-
const current = resource.playbackDuration;
|
|
2034
|
+
const current = resource.playbackDuration + this.seekOffset;
|
|
2016
2035
|
return {
|
|
2017
2036
|
current: current,
|
|
2018
2037
|
total: total,
|
|
@@ -2111,57 +2130,113 @@ class Player extends events_1.EventEmitter {
|
|
|
2111
2130
|
if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
|
|
2112
2131
|
return false;
|
|
2113
2132
|
}
|
|
2114
|
-
if (this.refreshLock)
|
|
2133
|
+
if (this.refreshLock) {
|
|
2134
|
+
this.debug(`[Player] refreshPlayerResource skipped — lock held`);
|
|
2115
2135
|
return false;
|
|
2136
|
+
}
|
|
2137
|
+
// Lock before anything so stateChange idle sees it when stop() fires.
|
|
2116
2138
|
this.refreshLock = true;
|
|
2139
|
+
// Clear any existing stuckTimer from the previous playback cycle so it
|
|
2140
|
+
// cannot fire while we are mid-refresh.
|
|
2141
|
+
if (this.stuckTimer) {
|
|
2142
|
+
clearTimeout(this.stuckTimer);
|
|
2143
|
+
this.stuckTimer = null;
|
|
2144
|
+
}
|
|
2117
2145
|
try {
|
|
2118
2146
|
const track = this.queue.currentTrack;
|
|
2119
2147
|
this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
const streaminfo = await this.getStream(track);
|
|
2123
|
-
if (!streaminfo?.stream) {
|
|
2124
|
-
this.debug(`[Player] No stream to refresh`);
|
|
2125
|
-
return false;
|
|
2126
|
-
}
|
|
2127
|
-
// Create AudioResource with filters and seek to current position
|
|
2128
|
-
const resource = await this.createResource(streaminfo, track, currentPosition);
|
|
2129
|
-
// Stop current playback and destroy old resource/stream
|
|
2130
|
-
const wasPlaying = this.isPlaying;
|
|
2148
|
+
const currentPosition = position >= 0 ? position : (this.currentResource?.playbackDuration ?? 0);
|
|
2149
|
+
this.seekOffset = currentPosition;
|
|
2131
2150
|
const wasPaused = this.isPaused;
|
|
2132
|
-
this.
|
|
2133
|
-
//
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2151
|
+
const playbackDuration = this.currentResource?.playbackDuration ?? 0;
|
|
2152
|
+
// Reuse is only viable for forward seeks (stream is sequential).
|
|
2153
|
+
const isForwardSeek = position < 0 || position >= playbackDuration;
|
|
2154
|
+
const currentStreamId = this.currentSlot.streamId;
|
|
2155
|
+
// Try to grab the live source stream for reuse.
|
|
2156
|
+
// getRawStream accepts "paused" streams (discordjs/voice pauses source streams on NoSubscriberBehavior); getStream would reject them.
|
|
2157
|
+
let reuseStream = null;
|
|
2158
|
+
if (isForwardSeek && currentStreamId) {
|
|
2159
|
+
reuseStream = this.streamManager.getRawStream(currentStreamId);
|
|
2160
|
+
if (reuseStream) {
|
|
2161
|
+
this.debug(`[Player] Will reuse source stream for seek (pos: ${currentPosition}ms)`);
|
|
2140
2162
|
}
|
|
2141
2163
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2164
|
+
// ── CRITICAL: unpipe BEFORE stop ──────────────────────────────────────
|
|
2165
|
+
// stop() kills discordjs/voice internal FFmpeg → EPIPE on source stream.
|
|
2166
|
+
// unpipe() first disconnects our stream cleanly before that happens.
|
|
2167
|
+
if (reuseStream) {
|
|
2168
|
+
reuseStream.unpipe();
|
|
2144
2169
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2170
|
+
// Remove StreamManager listeners.
|
|
2171
|
+
// forceDestroy=false when reusing so the Readable object stays alive.
|
|
2172
|
+
if (currentStreamId) {
|
|
2173
|
+
this.streamManager.unregisterStream(currentStreamId, !reuseStream);
|
|
2174
|
+
this.currentSlot.streamId = null;
|
|
2175
|
+
}
|
|
2176
|
+
// Stop the AudioPlayer.
|
|
2177
|
+
// stateChange (playing→idle) fires; refreshLock=true guards it (v4 fix).
|
|
2178
|
+
this.audioPlayer.stop(true);
|
|
2179
|
+
this.currentResource = null;
|
|
2180
|
+
this.currentSlot.resource = null;
|
|
2181
|
+
this.currentSlot.isValid = false;
|
|
2182
|
+
// One event-loop tick: lets deferred stream events settle.
|
|
2183
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2184
|
+
// Verify the reuse stream survived stop().
|
|
2185
|
+
if (reuseStream) {
|
|
2186
|
+
if (reuseStream.destroyed || reuseStream.readable === false) {
|
|
2187
|
+
this.debug(`[Player] Source stream did not survive stop — falling back to fresh stream`);
|
|
2188
|
+
reuseStream = null;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
let streaminfo = null;
|
|
2192
|
+
if (reuseStream) {
|
|
2193
|
+
streaminfo = { stream: reuseStream, type: "arbitrary" };
|
|
2194
|
+
}
|
|
2195
|
+
else {
|
|
2196
|
+
// Clear caches so we don't get the dead Readable back.
|
|
2197
|
+
this.pluginManager.clearStreamCache();
|
|
2198
|
+
this.extensionManager.clearCache("stream");
|
|
2199
|
+
this.debug(`[Player] Fetching fresh stream${!isForwardSeek ? " (backward seek)" : " (reuse failed)"}`);
|
|
2200
|
+
streaminfo = await this.getStream(track);
|
|
2201
|
+
}
|
|
2202
|
+
if (!streaminfo?.stream) {
|
|
2203
|
+
this.debug(`[Player] No stream available for refresh`);
|
|
2204
|
+
return false;
|
|
2147
2205
|
}
|
|
2206
|
+
// Build AudioResource (input-side FFmpeg seek via FilterManager).
|
|
2207
|
+
const resource = await this.createResource(streaminfo, track, currentPosition);
|
|
2208
|
+
// Register the source stream.
|
|
2209
|
+
const newStreamId = this.streamManager.registerStream(streaminfo.stream, track, {
|
|
2210
|
+
source: track.source || "stream",
|
|
2211
|
+
isPreload: false,
|
|
2212
|
+
priority: 10,
|
|
2213
|
+
});
|
|
2214
|
+
this.currentSlot.resource = resource;
|
|
2215
|
+
this.currentSlot.track = track;
|
|
2216
|
+
this.currentSlot.streamId = newStreamId;
|
|
2217
|
+
this.currentSlot.isValid = true;
|
|
2148
2218
|
this.currentResource = resource;
|
|
2149
|
-
//
|
|
2219
|
+
// ── Set seek flag BEFORE play so the Buffering handler sees it ────────
|
|
2220
|
+
if (position >= 0) {
|
|
2221
|
+
this.seekInProgress = true;
|
|
2222
|
+
}
|
|
2150
2223
|
if (this.connection) {
|
|
2151
2224
|
this.connection.subscribe(this.audioPlayer);
|
|
2152
2225
|
this.audioPlayer.play(resource);
|
|
2153
2226
|
}
|
|
2154
|
-
|
|
2155
|
-
if (wasPaused) {
|
|
2227
|
+
if (wasPaused)
|
|
2156
2228
|
this.audioPlayer.pause();
|
|
2157
|
-
}
|
|
2158
|
-
this.debug(`[Player] Successfully applied filter to current track at position ${currentPosition}ms`);
|
|
2229
|
+
this.debug(`[Player] Resource refreshed at position ${currentPosition}ms`);
|
|
2159
2230
|
return true;
|
|
2160
2231
|
}
|
|
2161
2232
|
catch (error) {
|
|
2162
|
-
this.debug(`[Player]
|
|
2163
|
-
|
|
2164
|
-
|
|
2233
|
+
this.debug(`[Player] refreshPlayerResource error:`, error);
|
|
2234
|
+
this.seekInProgress = false; // ensure flag is cleared on failure
|
|
2235
|
+
this.emit("playerError", error, this.queue.currentTrack ?? undefined);
|
|
2236
|
+
return false;
|
|
2237
|
+
}
|
|
2238
|
+
finally {
|
|
2239
|
+
this.refreshLock = false; // always released
|
|
2165
2240
|
}
|
|
2166
2241
|
}
|
|
2167
2242
|
/**
|
|
@@ -2212,7 +2287,12 @@ class Player extends events_1.EventEmitter {
|
|
|
2212
2287
|
if (this.destroyed)
|
|
2213
2288
|
return;
|
|
2214
2289
|
this.debug(`[Player] AudioPlayer stateChange from ${oldState.status} to ${newState.status}`);
|
|
2290
|
+
// ── Idle: track ended naturally ───────────────────────────────────────
|
|
2215
2291
|
if (newState.status === voice_1.AudioPlayerStatus.Idle && oldState.status !== voice_1.AudioPlayerStatus.Idle) {
|
|
2292
|
+
if (this.refreshLock) {
|
|
2293
|
+
this.debug(`[Player] AudioPlayer went idle during resource refresh — skipping trackEnd/playNext`);
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2216
2296
|
// Track ended
|
|
2217
2297
|
const track = this.queue.currentTrack;
|
|
2218
2298
|
if (track) {
|
|
@@ -2223,11 +2303,16 @@ class Player extends events_1.EventEmitter {
|
|
|
2223
2303
|
}
|
|
2224
2304
|
}
|
|
2225
2305
|
void this.playNext();
|
|
2306
|
+
// ── Playing: started or resumed ───────────────────────────────────────
|
|
2226
2307
|
}
|
|
2227
2308
|
else if (newState.status === voice_1.AudioPlayerStatus.Playing &&
|
|
2228
2309
|
(oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) {
|
|
2229
2310
|
// Track started
|
|
2230
2311
|
this.clearLeaveTimeout();
|
|
2312
|
+
if (this.seekInProgress) {
|
|
2313
|
+
this.debug(`[Player] Seek complete — audio output started`);
|
|
2314
|
+
this.seekInProgress = false;
|
|
2315
|
+
}
|
|
2231
2316
|
const track = this.queue.currentTrack;
|
|
2232
2317
|
if (track) {
|
|
2233
2318
|
this.debug(`[Player] Track started: ${track.title}`);
|
|
@@ -2244,6 +2329,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2244
2329
|
}
|
|
2245
2330
|
}
|
|
2246
2331
|
}
|
|
2332
|
+
// ── Paused ────────────────────────────────────────────────────────────
|
|
2247
2333
|
}
|
|
2248
2334
|
else if (newState.status === voice_1.AudioPlayerStatus.Paused && oldState.status !== voice_1.AudioPlayerStatus.Paused) {
|
|
2249
2335
|
const track = this.queue.currentTrack;
|
|
@@ -2254,6 +2340,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2254
2340
|
fp.emit("playerPause", track);
|
|
2255
2341
|
}
|
|
2256
2342
|
}
|
|
2343
|
+
// ── Resumed from pause ────────────────────────────────────────────────
|
|
2257
2344
|
}
|
|
2258
2345
|
else if (newState.status !== voice_1.AudioPlayerStatus.Paused && oldState.status === voice_1.AudioPlayerStatus.Paused) {
|
|
2259
2346
|
const track = this.queue.currentTrack;
|
|
@@ -2267,9 +2354,14 @@ class Player extends events_1.EventEmitter {
|
|
|
2267
2354
|
}
|
|
2268
2355
|
else if (newState.status === voice_1.AudioPlayerStatus.AutoPaused) {
|
|
2269
2356
|
this.debug(`[Player] AudioPlayerStatus.AutoPaused`);
|
|
2357
|
+
// ── Buffering: start stuck detector ───────────────────────────────────
|
|
2270
2358
|
}
|
|
2271
2359
|
else if (newState.status === voice_1.AudioPlayerStatus.Buffering) {
|
|
2272
2360
|
this.debug(`[Player] AudioPlayerStatus.Buffering`);
|
|
2361
|
+
if (this.seekInProgress) {
|
|
2362
|
+
this.debug(`[Player] Buffering during seek — stuckTimer suppressed`);
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2273
2365
|
this.lastDuration = this.currentResource?.playbackDuration || 0;
|
|
2274
2366
|
this.stuckTimer = setTimeout(() => {
|
|
2275
2367
|
if (this.currentResource?.playbackDuration === this.lastDuration) {
|