ziplayer 0.3.7 → 0.3.9
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 +1 -8
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +52 -106
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.d.ts +7 -24
- package/dist/structures/FilterManager.d.ts.map +1 -1
- package/dist/structures/FilterManager.js +123 -99
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/Player.d.ts +2 -0
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +113 -91
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PreloadManager.d.ts +1 -0
- package/dist/structures/PreloadManager.d.ts.map +1 -1
- package/dist/structures/PreloadManager.js +26 -6
- package/dist/structures/PreloadManager.js.map +1 -1
- package/dist/structures/StreamManager.d.ts +1 -0
- package/dist/structures/StreamManager.d.ts.map +1 -1
- package/dist/structures/StreamManager.js.map +1 -1
- package/dist/types/index.d.ts +1 -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 +63 -119
- package/src/structures/FilterManager.ts +439 -380
- package/src/structures/Player.ts +133 -99
- package/src/structures/PreloadManager.ts +293 -274
- package/src/structures/StreamManager.ts +2 -0
- package/src/types/index.ts +2 -0
package/src/structures/Player.ts
CHANGED
|
@@ -173,6 +173,7 @@ export class Player extends EventEmitter {
|
|
|
173
173
|
private ttsPlayer: DiscordAudioPlayer | null = null;
|
|
174
174
|
private lastDuration: number = 0;
|
|
175
175
|
private seekOffset: number = 0;
|
|
176
|
+
private recoveryInProgress = false;
|
|
176
177
|
|
|
177
178
|
constructor(guildId: string, options: PlayerOptions = {}, manager: PlayerManager) {
|
|
178
179
|
super();
|
|
@@ -269,7 +270,7 @@ export class Player extends EventEmitter {
|
|
|
269
270
|
extractorTimeout: this.options.extractorTimeout,
|
|
270
271
|
});
|
|
271
272
|
this.streamManager = new StreamManager({
|
|
272
|
-
maxConcurrentStreams:
|
|
273
|
+
maxConcurrentStreams: this.options?.maxStreamStore ?? 4,
|
|
273
274
|
streamTimeout: 5 * 60 * 1000,
|
|
274
275
|
maxListenersPerStream: 15,
|
|
275
276
|
enableMetrics: true,
|
|
@@ -787,6 +788,7 @@ export class Player extends EventEmitter {
|
|
|
787
788
|
|
|
788
789
|
private async attemptTrackRecovery(track: Track, reason: unknown): Promise<boolean> {
|
|
789
790
|
if (!this.antiStuckEnabled) return false;
|
|
791
|
+
this.recoveryInProgress = true;
|
|
790
792
|
this.debug(`[AntiStuck] Recovery started for: ${track.title}`, reason);
|
|
791
793
|
|
|
792
794
|
const originalQuality = this.options.quality;
|
|
@@ -808,6 +810,7 @@ export class Player extends EventEmitter {
|
|
|
808
810
|
if (startedFromPreload) {
|
|
809
811
|
this.antiStuckConsecutiveFailures = 0;
|
|
810
812
|
this.options.quality = originalQuality;
|
|
813
|
+
this.recoveryInProgress = false;
|
|
811
814
|
return true;
|
|
812
815
|
}
|
|
813
816
|
}
|
|
@@ -816,6 +819,7 @@ export class Player extends EventEmitter {
|
|
|
816
819
|
if (started) {
|
|
817
820
|
this.antiStuckConsecutiveFailures = 0;
|
|
818
821
|
this.options.quality = originalQuality;
|
|
822
|
+
this.recoveryInProgress = false;
|
|
819
823
|
return true;
|
|
820
824
|
}
|
|
821
825
|
} catch (error) {
|
|
@@ -827,9 +831,10 @@ export class Player extends EventEmitter {
|
|
|
827
831
|
this.antiStuckConsecutiveFailures++;
|
|
828
832
|
if (this.antiStuckConsecutiveFailures >= this.antiStuckControlledSkipThreshold) {
|
|
829
833
|
this.debug(`[AntiStuck] Controlled skip threshold reached for ${track.title}`);
|
|
834
|
+
this.recoveryInProgress = false;
|
|
830
835
|
return false;
|
|
831
836
|
}
|
|
832
|
-
|
|
837
|
+
this.recoveryInProgress = false;
|
|
833
838
|
// Avoid hard skip storm by leaving track for next natural retry window.
|
|
834
839
|
this.debug(`[AntiStuck] Keeping track for controlled retry window: ${track.title}`);
|
|
835
840
|
return false;
|
|
@@ -887,26 +892,32 @@ export class Player extends EventEmitter {
|
|
|
887
892
|
* @param {number} position - Position in milliseconds to seek to (0 = no seek)
|
|
888
893
|
* @returns {Promise<AudioResource>} The AudioResource with filters and seek applied
|
|
889
894
|
*/
|
|
890
|
-
private async createResource(
|
|
895
|
+
private async createResource(
|
|
896
|
+
streamInfo: StreamInfo,
|
|
897
|
+
track: Track,
|
|
898
|
+
position: number = 0,
|
|
899
|
+
): Promise<{ resource: AudioResource; processedStream: import("stream").Readable | null }> {
|
|
891
900
|
const filterString = this.filter.getFilterString();
|
|
892
901
|
this.debug(`[Player] Creating AudioResource — filters: ${filterString || "none"}, seek: ${position}ms`);
|
|
893
902
|
|
|
894
|
-
|
|
903
|
+
this.filter.setSourceStreamType(streamInfo.type);
|
|
904
|
+
|
|
895
905
|
const seekArg = position > 0 ? position : -1;
|
|
896
906
|
|
|
897
907
|
if (filterString || position > 0) {
|
|
898
|
-
// throws on failure — do NOT fall back to the already-piped stream
|
|
899
908
|
const processedStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, seekArg);
|
|
900
|
-
streamInfo.type = StreamType.Arbitrary as any;
|
|
901
909
|
|
|
902
|
-
|
|
910
|
+
// rawStream. Always use Arbitrary so discordjs/voice doesn't re-encode.
|
|
911
|
+
const resource = createAudioResource(processedStream, {
|
|
903
912
|
metadata: track,
|
|
904
913
|
inputType: StreamType.Arbitrary,
|
|
905
914
|
inlineVolume: true,
|
|
906
915
|
});
|
|
916
|
+
|
|
917
|
+
return { resource, processedStream };
|
|
907
918
|
}
|
|
908
919
|
|
|
909
|
-
|
|
920
|
+
const resource = createAudioResource(streamInfo.stream, {
|
|
910
921
|
metadata: track,
|
|
911
922
|
inputType:
|
|
912
923
|
streamInfo.type === "webm/opus" ? StreamType.WebmOpus
|
|
@@ -914,6 +925,8 @@ export class Player extends EventEmitter {
|
|
|
914
925
|
: StreamType.Arbitrary,
|
|
915
926
|
inlineVolume: true,
|
|
916
927
|
});
|
|
928
|
+
|
|
929
|
+
return { resource, processedStream: null };
|
|
917
930
|
}
|
|
918
931
|
|
|
919
932
|
private mergeTrackPreserveRef(target: Track, source: Track): void {
|
|
@@ -1027,9 +1040,16 @@ export class Player extends EventEmitter {
|
|
|
1027
1040
|
private async startTrack(track: Track): Promise<boolean> {
|
|
1028
1041
|
if (this.destroyed) return false;
|
|
1029
1042
|
|
|
1030
|
-
//
|
|
1031
|
-
|
|
1043
|
+
// Check preload BEFORE calling getStream so we never fetch a
|
|
1044
|
+
// stream we're about to throw away. The original code called getStream()
|
|
1045
|
+
// unconditionally at the top, then used the preload if available — leaking
|
|
1046
|
+
// the just-fetched stream and running middleware twice.
|
|
1047
|
+
if (this.preloadManager.hasValidPreload(track)) {
|
|
1048
|
+
return await this.startFromPreload(track);
|
|
1049
|
+
}
|
|
1032
1050
|
|
|
1051
|
+
// Only fetch a stream when there is no usable preload.
|
|
1052
|
+
let streamInfo: StreamInfo | null = null;
|
|
1033
1053
|
try {
|
|
1034
1054
|
streamInfo = await this.getStream(track);
|
|
1035
1055
|
} catch (error) {
|
|
@@ -1037,78 +1057,70 @@ export class Player extends EventEmitter {
|
|
|
1037
1057
|
throw error;
|
|
1038
1058
|
}
|
|
1039
1059
|
|
|
1040
|
-
//
|
|
1060
|
+
// Remote playback
|
|
1041
1061
|
if (streamInfo?.remote && streamInfo.handle) {
|
|
1042
1062
|
return await this.playRemote(track, streamInfo);
|
|
1043
1063
|
}
|
|
1044
1064
|
|
|
1045
|
-
//
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
1052
|
-
if (oldStreamId && this.streamManager) {
|
|
1053
|
-
setTimeout(() => {
|
|
1054
|
-
if (this.currentSlot.streamId === oldStreamId) {
|
|
1055
|
-
this.streamManager.unregisterStream(oldStreamId, true);
|
|
1056
|
-
}
|
|
1057
|
-
}, 3000);
|
|
1058
|
-
}
|
|
1065
|
+
// Native playback — pass the already-fetched streamInfo to avoid a second fetch
|
|
1066
|
+
return await this.loadFreshStream(track, streamInfo);
|
|
1067
|
+
}
|
|
1068
|
+
private async startFromPreload(track: Track): Promise<boolean> {
|
|
1069
|
+
if (this.destroyed) return false;
|
|
1059
1070
|
|
|
1060
|
-
|
|
1061
|
-
const currentResource = this.currentSlot.resource;
|
|
1062
|
-
if (!currentResource) {
|
|
1063
|
-
return false;
|
|
1064
|
-
}
|
|
1065
|
-
this.seekOffset = 0;
|
|
1066
|
-
const targetVolume = this.getTrackTargetVolume(track);
|
|
1071
|
+
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
1067
1072
|
|
|
1068
|
-
|
|
1069
|
-
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
1070
|
-
}
|
|
1073
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1071
1074
|
|
|
1072
|
-
|
|
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
|
-
}
|
|
1081
|
-
await this.applyCrossfadeIn(currentResource, track);
|
|
1075
|
+
this.promotePreloadToCurrent(track);
|
|
1082
1076
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1077
|
+
if (oldStreamId && oldStreamId !== this.currentSlot.streamId) {
|
|
1078
|
+
this.streamManager.unregisterStream(oldStreamId, true);
|
|
1079
|
+
this.debug(`[Player] Released old stream ${oldStreamId} after preload promotion`);
|
|
1080
|
+
}
|
|
1086
1081
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1082
|
+
const currentResource = this.currentSlot.resource;
|
|
1083
|
+
if (!currentResource) return false;
|
|
1089
1084
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
this.
|
|
1095
|
-
return false;
|
|
1085
|
+
this.seekOffset = 0;
|
|
1086
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
1087
|
+
|
|
1088
|
+
if (currentResource.volume) {
|
|
1089
|
+
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
1096
1090
|
}
|
|
1091
|
+
|
|
1092
|
+
await this.maybeAlignToBeatBoundary();
|
|
1093
|
+
this.refreshLock = true;
|
|
1094
|
+
try {
|
|
1095
|
+
this.audioPlayer.stop(true);
|
|
1096
|
+
this.audioPlayer.play(currentResource);
|
|
1097
|
+
await entersState(this.audioPlayer, AudioPlayerStatus.Playing, 10_000);
|
|
1098
|
+
} finally {
|
|
1099
|
+
this.refreshLock = false;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
await this.applyCrossfadeIn(currentResource, track);
|
|
1103
|
+
|
|
1104
|
+
this.preloadNextTrack().catch((err: any) => {
|
|
1105
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
return true;
|
|
1097
1109
|
}
|
|
1098
1110
|
|
|
1099
1111
|
/**
|
|
1100
1112
|
* Load fresh stream when no preload available
|
|
1101
1113
|
*/
|
|
1102
|
-
private async loadFreshStream(track: Track): Promise<boolean> {
|
|
1114
|
+
private async loadFreshStream(track: Track, preloadedStreamInfo?: StreamInfo | null): Promise<boolean> {
|
|
1103
1115
|
if (this.destroyed) return false;
|
|
1104
1116
|
|
|
1105
|
-
// Cancel preload to free resources
|
|
1106
1117
|
await this.safeCancelPreload();
|
|
1107
1118
|
|
|
1108
1119
|
try {
|
|
1109
|
-
|
|
1120
|
+
// use caller-supplied streamInfo when available so we don't
|
|
1121
|
+
// call getStream() a second time and run middleware twice.
|
|
1122
|
+
const streamInfo = preloadedStreamInfo ?? (await this.getStream(track));
|
|
1110
1123
|
|
|
1111
|
-
// Handle remote playback
|
|
1112
1124
|
if (streamInfo?.remote && streamInfo.handle) {
|
|
1113
1125
|
return await this.playRemote(track, streamInfo);
|
|
1114
1126
|
}
|
|
@@ -1117,37 +1129,50 @@ export class Player extends EventEmitter {
|
|
|
1117
1129
|
throw new Error(`No stream available`);
|
|
1118
1130
|
}
|
|
1119
1131
|
|
|
1120
|
-
// Register
|
|
1121
|
-
const
|
|
1132
|
+
// Register the RAW source stream — this is what we can reuse on seek
|
|
1133
|
+
const rawStreamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
1122
1134
|
source: track.source || "stream",
|
|
1123
1135
|
isPreload: false,
|
|
1124
1136
|
isRemote: !!streamInfo?.remote,
|
|
1125
1137
|
priority: 10,
|
|
1126
1138
|
});
|
|
1127
1139
|
|
|
1128
|
-
//
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1140
|
+
// createResource now returns both the AudioResource
|
|
1141
|
+
// AND the processedStream (ffmpeg stdout) when filters/seek are involved.
|
|
1142
|
+
const { resource, processedStream } = await this.createResource(streamInfo, track, 0);
|
|
1143
|
+
|
|
1144
|
+
// when a processedStream exists, register it too so its
|
|
1145
|
+
// lifecycle is tracked. Store its id separately in currentSlot so
|
|
1146
|
+
// destroyCurrentStream() and refreshPlayerResource() clean the right object.
|
|
1147
|
+
let playStreamId = rawStreamId;
|
|
1148
|
+
if (processedStream && processedStream !== streamInfo.stream) {
|
|
1149
|
+
playStreamId = this.streamManager.registerStream(processedStream, track, {
|
|
1150
|
+
source: track.source || "stream-processed",
|
|
1151
|
+
isPreload: false,
|
|
1152
|
+
priority: 10,
|
|
1153
|
+
});
|
|
1154
|
+
this.debug(`[Player] Registered processedStream ${playStreamId} (rawStream: ${rawStreamId})`);
|
|
1155
|
+
}
|
|
1156
|
+
if (this.currentSlot.streamId && this.currentSlot.streamId !== rawStreamId) {
|
|
1133
1157
|
this.streamManager.unregisterStream(this.currentSlot.streamId, true);
|
|
1134
1158
|
}
|
|
1159
|
+
if ((this.currentSlot as any).processedStreamId && (this.currentSlot as any).processedStreamId !== playStreamId) {
|
|
1160
|
+
this.streamManager.unregisterStream((this.currentSlot as any).processedStreamId, true);
|
|
1161
|
+
}
|
|
1135
1162
|
|
|
1136
|
-
// Set current slot
|
|
1137
1163
|
this.currentSlot.resource = resource;
|
|
1138
1164
|
this.currentSlot.track = track;
|
|
1139
|
-
this.currentSlot.streamId =
|
|
1165
|
+
this.currentSlot.streamId = rawStreamId;
|
|
1166
|
+
(this.currentSlot as any).processedStreamId = processedStream ? playStreamId : null;
|
|
1140
1167
|
this.currentSlot.isValid = true;
|
|
1141
1168
|
this.currentResource = resource;
|
|
1142
|
-
this.seekOffset = 0;
|
|
1169
|
+
this.seekOffset = 0;
|
|
1143
1170
|
|
|
1144
|
-
// Apply volume
|
|
1145
1171
|
const targetVolume = this.getTrackTargetVolume(track);
|
|
1146
1172
|
if (resource.volume) {
|
|
1147
1173
|
resource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
1148
1174
|
}
|
|
1149
1175
|
|
|
1150
|
-
// Play — lock refresh so Idle event doesn't spawn duplicate playNext
|
|
1151
1176
|
await this.maybeAlignToBeatBoundary();
|
|
1152
1177
|
this.refreshLock = true;
|
|
1153
1178
|
try {
|
|
@@ -1157,11 +1182,11 @@ export class Player extends EventEmitter {
|
|
|
1157
1182
|
} finally {
|
|
1158
1183
|
this.refreshLock = false;
|
|
1159
1184
|
}
|
|
1185
|
+
|
|
1160
1186
|
await this.applyCrossfadeIn(resource, track);
|
|
1161
1187
|
|
|
1162
|
-
// Preload next (async)
|
|
1163
1188
|
if (!this.destroyed) {
|
|
1164
|
-
this.preloadNextTrack().catch((err) => {
|
|
1189
|
+
this.preloadNextTrack().catch((err: any) => {
|
|
1165
1190
|
this.debug(`[Player] Preload error:`, err);
|
|
1166
1191
|
});
|
|
1167
1192
|
}
|
|
@@ -1347,7 +1372,7 @@ export class Player extends EventEmitter {
|
|
|
1347
1372
|
throw new Error(`No stream available for track: ${track.title}`);
|
|
1348
1373
|
}
|
|
1349
1374
|
ttsStream = streamInfo.stream;
|
|
1350
|
-
const resource = await this.createResource(streamInfo as StreamInfo, track);
|
|
1375
|
+
const { resource, processedStream } = await this.createResource(streamInfo as StreamInfo, track);
|
|
1351
1376
|
if (!resource) {
|
|
1352
1377
|
throw new Error(`No resource available for track: ${track.title}`);
|
|
1353
1378
|
}
|
|
@@ -1720,6 +1745,7 @@ export class Player extends EventEmitter {
|
|
|
1720
1745
|
this.debug("[Player] Cannot stop while subscribed to another player");
|
|
1721
1746
|
return false;
|
|
1722
1747
|
}
|
|
1748
|
+
this.recoveryInProgress = false;
|
|
1723
1749
|
this.debug(`[Player] stop called`);
|
|
1724
1750
|
if (this.playbackMode === PlaybackMode.REMOTE) {
|
|
1725
1751
|
this.cancelPreload();
|
|
@@ -1811,7 +1837,7 @@ export class Player extends EventEmitter {
|
|
|
1811
1837
|
return false;
|
|
1812
1838
|
}
|
|
1813
1839
|
this.debug(`[Player] skip called with index: ${index}`);
|
|
1814
|
-
|
|
1840
|
+
this.recoveryInProgress = false;
|
|
1815
1841
|
if (this.playbackMode === PlaybackMode.REMOTE) {
|
|
1816
1842
|
if (typeof index === "number" && index >= 0) {
|
|
1817
1843
|
for (let i = 0; i < index; i++) this.queue.remove(0);
|
|
@@ -2434,11 +2460,8 @@ export class Player extends EventEmitter {
|
|
|
2434
2460
|
return false;
|
|
2435
2461
|
}
|
|
2436
2462
|
|
|
2437
|
-
// Lock before anything so stateChange idle sees it when stop() fires.
|
|
2438
2463
|
this.refreshLock = true;
|
|
2439
2464
|
|
|
2440
|
-
// Clear any existing stuckTimer from the previous playback cycle so it
|
|
2441
|
-
// cannot fire while we are mid-refresh.
|
|
2442
2465
|
if (this.stuckTimer) {
|
|
2443
2466
|
clearTimeout(this.stuckTimer);
|
|
2444
2467
|
this.stuckTimer = null;
|
|
@@ -2453,13 +2476,11 @@ export class Player extends EventEmitter {
|
|
|
2453
2476
|
const wasPaused = this.isPaused;
|
|
2454
2477
|
const playbackDuration = this.currentResource?.playbackDuration ?? 0;
|
|
2455
2478
|
|
|
2456
|
-
// Reuse is only viable for forward seeks (stream is sequential).
|
|
2457
2479
|
const isForwardSeek = position < 0 || position >= playbackDuration;
|
|
2458
2480
|
const currentStreamId = this.currentSlot.streamId;
|
|
2459
2481
|
|
|
2460
|
-
// Try to grab the
|
|
2461
|
-
|
|
2462
|
-
let reuseStream: Readable | null = null;
|
|
2482
|
+
// Try to grab the raw source stream for reuse (forward seeks only)
|
|
2483
|
+
let reuseStream: import("stream").Readable | null = null;
|
|
2463
2484
|
if (isForwardSeek && currentStreamId) {
|
|
2464
2485
|
reuseStream = this.streamManager.getRawStream(currentStreamId);
|
|
2465
2486
|
if (reuseStream) {
|
|
@@ -2467,31 +2488,29 @@ export class Player extends EventEmitter {
|
|
|
2467
2488
|
}
|
|
2468
2489
|
}
|
|
2469
2490
|
|
|
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
2491
|
if (reuseStream) {
|
|
2474
2492
|
reuseStream.unpipe();
|
|
2475
2493
|
}
|
|
2476
2494
|
|
|
2477
|
-
//
|
|
2478
|
-
|
|
2495
|
+
// Clean up processedStream first (it's what AudioResource reads)
|
|
2496
|
+
const processedStreamId = (this.currentSlot as any).processedStreamId;
|
|
2497
|
+
if (processedStreamId && processedStreamId !== currentStreamId) {
|
|
2498
|
+
this.streamManager.unregisterStream(processedStreamId, true);
|
|
2499
|
+
(this.currentSlot as any).processedStreamId = null;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2479
2502
|
if (currentStreamId) {
|
|
2480
2503
|
this.streamManager.unregisterStream(currentStreamId, !reuseStream);
|
|
2481
2504
|
this.currentSlot.streamId = null;
|
|
2482
2505
|
}
|
|
2483
2506
|
|
|
2484
|
-
// Stop the AudioPlayer.
|
|
2485
|
-
// stateChange (playing→idle) fires; refreshLock=true guards it (v4 fix).
|
|
2486
2507
|
this.audioPlayer.stop(true);
|
|
2487
2508
|
this.currentResource = null;
|
|
2488
2509
|
this.currentSlot.resource = null;
|
|
2489
2510
|
this.currentSlot.isValid = false;
|
|
2490
2511
|
|
|
2491
|
-
// One event-loop tick: lets deferred stream events settle.
|
|
2492
2512
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
2493
2513
|
|
|
2494
|
-
// Verify the reuse stream survived stop().
|
|
2495
2514
|
if (reuseStream) {
|
|
2496
2515
|
if (reuseStream.destroyed || (reuseStream as any).readable === false) {
|
|
2497
2516
|
this.debug(`[Player] Source stream did not survive stop — falling back to fresh stream`);
|
|
@@ -2504,7 +2523,6 @@ export class Player extends EventEmitter {
|
|
|
2504
2523
|
if (reuseStream) {
|
|
2505
2524
|
streaminfo = { stream: reuseStream, type: "arbitrary" };
|
|
2506
2525
|
} else {
|
|
2507
|
-
// Clear caches so we don't get the dead Readable back.
|
|
2508
2526
|
this.pluginManager.clearStreamCache();
|
|
2509
2527
|
this.extensionManager.clearCache("stream");
|
|
2510
2528
|
this.debug(`[Player] Fetching fresh stream${!isForwardSeek ? " (backward seek)" : " (reuse failed)"}`);
|
|
@@ -2516,22 +2534,33 @@ export class Player extends EventEmitter {
|
|
|
2516
2534
|
return false;
|
|
2517
2535
|
}
|
|
2518
2536
|
|
|
2519
|
-
|
|
2520
|
-
|
|
2537
|
+
const createPosition = reuseStream ? -1 : currentPosition;
|
|
2538
|
+
|
|
2539
|
+
const { resource, processedStream } = await this.createResource(streaminfo, track, createPosition);
|
|
2521
2540
|
|
|
2522
|
-
// Register
|
|
2541
|
+
// Register raw source stream
|
|
2523
2542
|
const newStreamId = this.streamManager.registerStream(streaminfo.stream, track, {
|
|
2524
2543
|
source: track.source || "stream",
|
|
2525
2544
|
isPreload: false,
|
|
2526
2545
|
priority: 10,
|
|
2527
2546
|
});
|
|
2547
|
+
|
|
2548
|
+
let newProcessedStreamId: string | null = null;
|
|
2549
|
+
if (processedStream && processedStream !== streaminfo.stream) {
|
|
2550
|
+
newProcessedStreamId = this.streamManager.registerStream(processedStream, track, {
|
|
2551
|
+
source: track.source || "stream-processed",
|
|
2552
|
+
isPreload: false,
|
|
2553
|
+
priority: 10,
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2528
2557
|
this.currentSlot.resource = resource;
|
|
2529
2558
|
this.currentSlot.track = track;
|
|
2530
2559
|
this.currentSlot.streamId = newStreamId;
|
|
2560
|
+
(this.currentSlot as any).processedStreamId = newProcessedStreamId;
|
|
2531
2561
|
this.currentSlot.isValid = true;
|
|
2532
2562
|
this.currentResource = resource;
|
|
2533
2563
|
|
|
2534
|
-
// ── Set seek flag BEFORE play so the Buffering handler sees it ────────
|
|
2535
2564
|
if (position >= 0) {
|
|
2536
2565
|
this.seekInProgress = true;
|
|
2537
2566
|
}
|
|
@@ -2546,11 +2575,11 @@ export class Player extends EventEmitter {
|
|
|
2546
2575
|
return true;
|
|
2547
2576
|
} catch (error) {
|
|
2548
2577
|
this.debug(`[Player] refreshPlayerResource error:`, error);
|
|
2549
|
-
this.seekInProgress = false;
|
|
2578
|
+
this.seekInProgress = false;
|
|
2550
2579
|
this.emit("playerError", error as Error, this.queue.currentTrack ?? undefined);
|
|
2551
2580
|
return false;
|
|
2552
2581
|
} finally {
|
|
2553
|
-
this.refreshLock = false;
|
|
2582
|
+
this.refreshLock = false;
|
|
2554
2583
|
}
|
|
2555
2584
|
}
|
|
2556
2585
|
|
|
@@ -2613,6 +2642,10 @@ export class Player extends EventEmitter {
|
|
|
2613
2642
|
this.debug(`[Player] AudioPlayer went idle during resource refresh — skipping trackEnd/playNext`);
|
|
2614
2643
|
return;
|
|
2615
2644
|
}
|
|
2645
|
+
if (this.recoveryInProgress) {
|
|
2646
|
+
this.debug(`[Player] AudioPlayer went idle during recovery — skipping playNext`);
|
|
2647
|
+
return;
|
|
2648
|
+
}
|
|
2616
2649
|
// Track ended
|
|
2617
2650
|
const track = this.queue.currentTrack;
|
|
2618
2651
|
if (track) {
|
|
@@ -2708,6 +2741,7 @@ export class Player extends EventEmitter {
|
|
|
2708
2741
|
});
|
|
2709
2742
|
this.audioPlayer.on("error", (error) => {
|
|
2710
2743
|
if (this.destroyed) return;
|
|
2744
|
+
if (this.recoveryInProgress) return;
|
|
2711
2745
|
this.debug(`[Player] AudioPlayer error:`, error);
|
|
2712
2746
|
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
2713
2747
|
const track = this.queue.currentTrack;
|