ziplayer 0.3.7 → 0.3.8
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 +1 -0
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +99 -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/package.json +1 -1
- package/src/plugins/index.ts +63 -119
- package/src/structures/FilterManager.ts +439 -380
- package/src/structures/Player.ts +120 -97
- package/src/structures/PreloadManager.ts +293 -274
- package/src/structures/StreamManager.ts +2 -0
|
@@ -193,7 +193,7 @@ class Player extends events_1.EventEmitter {
|
|
|
193
193
|
extractorTimeout: this.options.extractorTimeout,
|
|
194
194
|
});
|
|
195
195
|
this.streamManager = new StreamManager_1.StreamManager({
|
|
196
|
-
maxConcurrentStreams:
|
|
196
|
+
maxConcurrentStreams: 4,
|
|
197
197
|
streamTimeout: 5 * 60 * 1000,
|
|
198
198
|
maxListenersPerStream: 15,
|
|
199
199
|
enableMetrics: true,
|
|
@@ -751,25 +751,26 @@ class Player extends events_1.EventEmitter {
|
|
|
751
751
|
async createResource(streamInfo, track, position = 0) {
|
|
752
752
|
const filterString = this.filter.getFilterString();
|
|
753
753
|
this.debug(`[Player] Creating AudioResource — filters: ${filterString || "none"}, seek: ${position}ms`);
|
|
754
|
-
|
|
754
|
+
this.filter.setSourceStreamType(streamInfo.type);
|
|
755
755
|
const seekArg = position > 0 ? position : -1;
|
|
756
756
|
if (filterString || position > 0) {
|
|
757
|
-
// throws on failure — do NOT fall back to the already-piped stream
|
|
758
757
|
const processedStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, seekArg);
|
|
759
|
-
|
|
760
|
-
|
|
758
|
+
// rawStream. Always use Arbitrary so discordjs/voice doesn't re-encode.
|
|
759
|
+
const resource = (0, voice_1.createAudioResource)(processedStream, {
|
|
761
760
|
metadata: track,
|
|
762
761
|
inputType: voice_1.StreamType.Arbitrary,
|
|
763
762
|
inlineVolume: true,
|
|
764
763
|
});
|
|
764
|
+
return { resource, processedStream };
|
|
765
765
|
}
|
|
766
|
-
|
|
766
|
+
const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
767
767
|
metadata: track,
|
|
768
768
|
inputType: streamInfo.type === "webm/opus" ? voice_1.StreamType.WebmOpus
|
|
769
769
|
: streamInfo.type === "ogg/opus" ? voice_1.StreamType.OggOpus
|
|
770
770
|
: voice_1.StreamType.Arbitrary,
|
|
771
771
|
inlineVolume: true,
|
|
772
772
|
});
|
|
773
|
+
return { resource, processedStream: null };
|
|
773
774
|
}
|
|
774
775
|
mergeTrackPreserveRef(target, source) {
|
|
775
776
|
if (source === target)
|
|
@@ -870,7 +871,14 @@ class Player extends events_1.EventEmitter {
|
|
|
870
871
|
async startTrack(track) {
|
|
871
872
|
if (this.destroyed)
|
|
872
873
|
return false;
|
|
873
|
-
//
|
|
874
|
+
// Check preload BEFORE calling getStream so we never fetch a
|
|
875
|
+
// stream we're about to throw away. The original code called getStream()
|
|
876
|
+
// unconditionally at the top, then used the preload if available — leaking
|
|
877
|
+
// the just-fetched stream and running middleware twice.
|
|
878
|
+
if (this.preloadManager.hasValidPreload(track)) {
|
|
879
|
+
return await this.startFromPreload(track);
|
|
880
|
+
}
|
|
881
|
+
// Only fetch a stream when there is no usable preload.
|
|
874
882
|
let streamInfo = null;
|
|
875
883
|
try {
|
|
876
884
|
streamInfo = await this.getStream(track);
|
|
@@ -879,101 +887,103 @@ class Player extends events_1.EventEmitter {
|
|
|
879
887
|
this.debug(`[Player] Failed to get stream for track: ${track.title}`, error);
|
|
880
888
|
throw error;
|
|
881
889
|
}
|
|
882
|
-
//
|
|
890
|
+
// Remote playback
|
|
883
891
|
if (streamInfo?.remote && streamInfo.handle) {
|
|
884
892
|
return await this.playRemote(track, streamInfo);
|
|
885
893
|
}
|
|
886
|
-
//
|
|
894
|
+
// Native playback — pass the already-fetched streamInfo to avoid a second fetch
|
|
895
|
+
return await this.loadFreshStream(track, streamInfo);
|
|
896
|
+
}
|
|
897
|
+
async startFromPreload(track) {
|
|
898
|
+
if (this.destroyed)
|
|
899
|
+
return false;
|
|
900
|
+
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
901
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
902
|
+
this.promotePreloadToCurrent(track);
|
|
903
|
+
if (oldStreamId && oldStreamId !== this.currentSlot.streamId) {
|
|
904
|
+
this.streamManager.unregisterStream(oldStreamId, true);
|
|
905
|
+
this.debug(`[Player] Released old stream ${oldStreamId} after preload promotion`);
|
|
906
|
+
}
|
|
907
|
+
const currentResource = this.currentSlot.resource;
|
|
908
|
+
if (!currentResource)
|
|
909
|
+
return false;
|
|
910
|
+
this.seekOffset = 0;
|
|
911
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
912
|
+
if (currentResource.volume) {
|
|
913
|
+
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
914
|
+
}
|
|
915
|
+
await this.maybeAlignToBeatBoundary();
|
|
916
|
+
this.refreshLock = true;
|
|
887
917
|
try {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
892
|
-
if (oldStreamId && this.streamManager) {
|
|
893
|
-
setTimeout(() => {
|
|
894
|
-
if (this.currentSlot.streamId === oldStreamId) {
|
|
895
|
-
this.streamManager.unregisterStream(oldStreamId, true);
|
|
896
|
-
}
|
|
897
|
-
}, 3000);
|
|
898
|
-
}
|
|
899
|
-
this.promotePreloadToCurrent(track);
|
|
900
|
-
const currentResource = this.currentSlot.resource;
|
|
901
|
-
if (!currentResource) {
|
|
902
|
-
return false;
|
|
903
|
-
}
|
|
904
|
-
this.seekOffset = 0;
|
|
905
|
-
const targetVolume = this.getTrackTargetVolume(track);
|
|
906
|
-
if (currentResource.volume) {
|
|
907
|
-
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
908
|
-
}
|
|
909
|
-
await this.maybeAlignToBeatBoundary();
|
|
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
|
-
}
|
|
919
|
-
await this.applyCrossfadeIn(currentResource, track);
|
|
920
|
-
this.preloadNextTrack().catch((err) => {
|
|
921
|
-
this.debug(`[Player] Preload error:`, err);
|
|
922
|
-
});
|
|
923
|
-
return true;
|
|
924
|
-
}
|
|
925
|
-
this.debug(`[Player] No preload available, loading fresh: ${track.title}`);
|
|
926
|
-
return await this.loadFreshStream(track);
|
|
918
|
+
this.audioPlayer.stop(true);
|
|
919
|
+
this.audioPlayer.play(currentResource);
|
|
920
|
+
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
927
921
|
}
|
|
928
|
-
|
|
929
|
-
this.
|
|
930
|
-
this.emit("playerError", error, track);
|
|
931
|
-
return false;
|
|
922
|
+
finally {
|
|
923
|
+
this.refreshLock = false;
|
|
932
924
|
}
|
|
925
|
+
await this.applyCrossfadeIn(currentResource, track);
|
|
926
|
+
this.preloadNextTrack().catch((err) => {
|
|
927
|
+
this.debug(`[Player] Preload error:`, err);
|
|
928
|
+
});
|
|
929
|
+
return true;
|
|
933
930
|
}
|
|
934
931
|
/**
|
|
935
932
|
* Load fresh stream when no preload available
|
|
936
933
|
*/
|
|
937
|
-
async loadFreshStream(track) {
|
|
934
|
+
async loadFreshStream(track, preloadedStreamInfo) {
|
|
938
935
|
if (this.destroyed)
|
|
939
936
|
return false;
|
|
940
|
-
// Cancel preload to free resources
|
|
941
937
|
await this.safeCancelPreload();
|
|
942
938
|
try {
|
|
943
|
-
|
|
944
|
-
//
|
|
939
|
+
// use caller-supplied streamInfo when available so we don't
|
|
940
|
+
// call getStream() a second time and run middleware twice.
|
|
941
|
+
const streamInfo = preloadedStreamInfo ?? (await this.getStream(track));
|
|
945
942
|
if (streamInfo?.remote && streamInfo.handle) {
|
|
946
943
|
return await this.playRemote(track, streamInfo);
|
|
947
944
|
}
|
|
948
945
|
if (!streamInfo?.stream) {
|
|
949
946
|
throw new Error(`No stream available`);
|
|
950
947
|
}
|
|
951
|
-
// Register
|
|
952
|
-
const
|
|
948
|
+
// Register the RAW source stream — this is what we can reuse on seek
|
|
949
|
+
const rawStreamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
953
950
|
source: track.source || "stream",
|
|
954
951
|
isPreload: false,
|
|
955
952
|
isRemote: !!streamInfo?.remote,
|
|
956
953
|
priority: 10,
|
|
957
954
|
});
|
|
958
|
-
//
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
955
|
+
// createResource now returns both the AudioResource
|
|
956
|
+
// AND the processedStream (ffmpeg stdout) when filters/seek are involved.
|
|
957
|
+
const { resource, processedStream } = await this.createResource(streamInfo, track, 0);
|
|
958
|
+
// when a processedStream exists, register it too so its
|
|
959
|
+
// lifecycle is tracked. Store its id separately in currentSlot so
|
|
960
|
+
// destroyCurrentStream() and refreshPlayerResource() clean the right object.
|
|
961
|
+
let playStreamId = rawStreamId;
|
|
962
|
+
if (processedStream && processedStream !== streamInfo.stream) {
|
|
963
|
+
playStreamId = this.streamManager.registerStream(processedStream, track, {
|
|
964
|
+
source: track.source || "stream-processed",
|
|
965
|
+
isPreload: false,
|
|
966
|
+
priority: 10,
|
|
967
|
+
});
|
|
968
|
+
this.debug(`[Player] Registered processedStream ${playStreamId} (rawStream: ${rawStreamId})`);
|
|
969
|
+
}
|
|
970
|
+
if (this.currentSlot.streamId && this.currentSlot.streamId !== rawStreamId) {
|
|
962
971
|
this.streamManager.unregisterStream(this.currentSlot.streamId, true);
|
|
963
972
|
}
|
|
964
|
-
|
|
973
|
+
if (this.currentSlot.processedStreamId && this.currentSlot.processedStreamId !== playStreamId) {
|
|
974
|
+
this.streamManager.unregisterStream(this.currentSlot.processedStreamId, true);
|
|
975
|
+
}
|
|
965
976
|
this.currentSlot.resource = resource;
|
|
966
977
|
this.currentSlot.track = track;
|
|
967
|
-
this.currentSlot.streamId =
|
|
978
|
+
this.currentSlot.streamId = rawStreamId;
|
|
979
|
+
this.currentSlot.processedStreamId = processedStream ? playStreamId : null;
|
|
968
980
|
this.currentSlot.isValid = true;
|
|
969
981
|
this.currentResource = resource;
|
|
970
|
-
this.seekOffset = 0;
|
|
971
|
-
// Apply volume
|
|
982
|
+
this.seekOffset = 0;
|
|
972
983
|
const targetVolume = this.getTrackTargetVolume(track);
|
|
973
984
|
if (resource.volume) {
|
|
974
985
|
resource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
975
986
|
}
|
|
976
|
-
// Play — lock refresh so Idle event doesn't spawn duplicate playNext
|
|
977
987
|
await this.maybeAlignToBeatBoundary();
|
|
978
988
|
this.refreshLock = true;
|
|
979
989
|
try {
|
|
@@ -985,7 +995,6 @@ class Player extends events_1.EventEmitter {
|
|
|
985
995
|
this.refreshLock = false;
|
|
986
996
|
}
|
|
987
997
|
await this.applyCrossfadeIn(resource, track);
|
|
988
|
-
// Preload next (async)
|
|
989
998
|
if (!this.destroyed) {
|
|
990
999
|
this.preloadNextTrack().catch((err) => {
|
|
991
1000
|
this.debug(`[Player] Preload error:`, err);
|
|
@@ -1153,7 +1162,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1153
1162
|
throw new Error(`No stream available for track: ${track.title}`);
|
|
1154
1163
|
}
|
|
1155
1164
|
ttsStream = streamInfo.stream;
|
|
1156
|
-
const resource = await this.createResource(streamInfo, track);
|
|
1165
|
+
const { resource, processedStream } = await this.createResource(streamInfo, track);
|
|
1157
1166
|
if (!resource) {
|
|
1158
1167
|
throw new Error(`No resource available for track: ${track.title}`);
|
|
1159
1168
|
}
|
|
@@ -2134,10 +2143,7 @@ class Player extends events_1.EventEmitter {
|
|
|
2134
2143
|
this.debug(`[Player] refreshPlayerResource skipped — lock held`);
|
|
2135
2144
|
return false;
|
|
2136
2145
|
}
|
|
2137
|
-
// Lock before anything so stateChange idle sees it when stop() fires.
|
|
2138
2146
|
this.refreshLock = true;
|
|
2139
|
-
// Clear any existing stuckTimer from the previous playback cycle so it
|
|
2140
|
-
// cannot fire while we are mid-refresh.
|
|
2141
2147
|
if (this.stuckTimer) {
|
|
2142
2148
|
clearTimeout(this.stuckTimer);
|
|
2143
2149
|
this.stuckTimer = null;
|
|
@@ -2149,11 +2155,9 @@ class Player extends events_1.EventEmitter {
|
|
|
2149
2155
|
this.seekOffset = currentPosition;
|
|
2150
2156
|
const wasPaused = this.isPaused;
|
|
2151
2157
|
const playbackDuration = this.currentResource?.playbackDuration ?? 0;
|
|
2152
|
-
// Reuse is only viable for forward seeks (stream is sequential).
|
|
2153
2158
|
const isForwardSeek = position < 0 || position >= playbackDuration;
|
|
2154
2159
|
const currentStreamId = this.currentSlot.streamId;
|
|
2155
|
-
// Try to grab the
|
|
2156
|
-
// getRawStream accepts "paused" streams (discordjs/voice pauses source streams on NoSubscriberBehavior); getStream would reject them.
|
|
2160
|
+
// Try to grab the raw source stream for reuse (forward seeks only)
|
|
2157
2161
|
let reuseStream = null;
|
|
2158
2162
|
if (isForwardSeek && currentStreamId) {
|
|
2159
2163
|
reuseStream = this.streamManager.getRawStream(currentStreamId);
|
|
@@ -2161,27 +2165,24 @@ class Player extends events_1.EventEmitter {
|
|
|
2161
2165
|
this.debug(`[Player] Will reuse source stream for seek (pos: ${currentPosition}ms)`);
|
|
2162
2166
|
}
|
|
2163
2167
|
}
|
|
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
2168
|
if (reuseStream) {
|
|
2168
2169
|
reuseStream.unpipe();
|
|
2169
2170
|
}
|
|
2170
|
-
//
|
|
2171
|
-
|
|
2171
|
+
// Clean up processedStream first (it's what AudioResource reads)
|
|
2172
|
+
const processedStreamId = this.currentSlot.processedStreamId;
|
|
2173
|
+
if (processedStreamId && processedStreamId !== currentStreamId) {
|
|
2174
|
+
this.streamManager.unregisterStream(processedStreamId, true);
|
|
2175
|
+
this.currentSlot.processedStreamId = null;
|
|
2176
|
+
}
|
|
2172
2177
|
if (currentStreamId) {
|
|
2173
2178
|
this.streamManager.unregisterStream(currentStreamId, !reuseStream);
|
|
2174
2179
|
this.currentSlot.streamId = null;
|
|
2175
2180
|
}
|
|
2176
|
-
// Stop the AudioPlayer.
|
|
2177
|
-
// stateChange (playing→idle) fires; refreshLock=true guards it (v4 fix).
|
|
2178
2181
|
this.audioPlayer.stop(true);
|
|
2179
2182
|
this.currentResource = null;
|
|
2180
2183
|
this.currentSlot.resource = null;
|
|
2181
2184
|
this.currentSlot.isValid = false;
|
|
2182
|
-
// One event-loop tick: lets deferred stream events settle.
|
|
2183
2185
|
await new Promise((resolve) => setImmediate(resolve));
|
|
2184
|
-
// Verify the reuse stream survived stop().
|
|
2185
2186
|
if (reuseStream) {
|
|
2186
2187
|
if (reuseStream.destroyed || reuseStream.readable === false) {
|
|
2187
2188
|
this.debug(`[Player] Source stream did not survive stop — falling back to fresh stream`);
|
|
@@ -2193,7 +2194,6 @@ class Player extends events_1.EventEmitter {
|
|
|
2193
2194
|
streaminfo = { stream: reuseStream, type: "arbitrary" };
|
|
2194
2195
|
}
|
|
2195
2196
|
else {
|
|
2196
|
-
// Clear caches so we don't get the dead Readable back.
|
|
2197
2197
|
this.pluginManager.clearStreamCache();
|
|
2198
2198
|
this.extensionManager.clearCache("stream");
|
|
2199
2199
|
this.debug(`[Player] Fetching fresh stream${!isForwardSeek ? " (backward seek)" : " (reuse failed)"}`);
|
|
@@ -2203,20 +2203,28 @@ class Player extends events_1.EventEmitter {
|
|
|
2203
2203
|
this.debug(`[Player] No stream available for refresh`);
|
|
2204
2204
|
return false;
|
|
2205
2205
|
}
|
|
2206
|
-
|
|
2207
|
-
const resource = await this.createResource(streaminfo, track,
|
|
2208
|
-
// Register
|
|
2206
|
+
const createPosition = reuseStream ? -1 : currentPosition;
|
|
2207
|
+
const { resource, processedStream } = await this.createResource(streaminfo, track, createPosition);
|
|
2208
|
+
// Register raw source stream
|
|
2209
2209
|
const newStreamId = this.streamManager.registerStream(streaminfo.stream, track, {
|
|
2210
2210
|
source: track.source || "stream",
|
|
2211
2211
|
isPreload: false,
|
|
2212
2212
|
priority: 10,
|
|
2213
2213
|
});
|
|
2214
|
+
let newProcessedStreamId = null;
|
|
2215
|
+
if (processedStream && processedStream !== streaminfo.stream) {
|
|
2216
|
+
newProcessedStreamId = this.streamManager.registerStream(processedStream, track, {
|
|
2217
|
+
source: track.source || "stream-processed",
|
|
2218
|
+
isPreload: false,
|
|
2219
|
+
priority: 10,
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2214
2222
|
this.currentSlot.resource = resource;
|
|
2215
2223
|
this.currentSlot.track = track;
|
|
2216
2224
|
this.currentSlot.streamId = newStreamId;
|
|
2225
|
+
this.currentSlot.processedStreamId = newProcessedStreamId;
|
|
2217
2226
|
this.currentSlot.isValid = true;
|
|
2218
2227
|
this.currentResource = resource;
|
|
2219
|
-
// ── Set seek flag BEFORE play so the Buffering handler sees it ────────
|
|
2220
2228
|
if (position >= 0) {
|
|
2221
2229
|
this.seekInProgress = true;
|
|
2222
2230
|
}
|
|
@@ -2231,12 +2239,12 @@ class Player extends events_1.EventEmitter {
|
|
|
2231
2239
|
}
|
|
2232
2240
|
catch (error) {
|
|
2233
2241
|
this.debug(`[Player] refreshPlayerResource error:`, error);
|
|
2234
|
-
this.seekInProgress = false;
|
|
2242
|
+
this.seekInProgress = false;
|
|
2235
2243
|
this.emit("playerError", error, this.queue.currentTrack ?? undefined);
|
|
2236
2244
|
return false;
|
|
2237
2245
|
}
|
|
2238
2246
|
finally {
|
|
2239
|
-
this.refreshLock = false;
|
|
2247
|
+
this.refreshLock = false;
|
|
2240
2248
|
}
|
|
2241
2249
|
}
|
|
2242
2250
|
/**
|