ziplayer 0.3.2 → 0.3.4
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/{AI-Guide.md → AGENTS.md} +717 -624
- package/README.md +658 -526
- package/dist/extensions/BaseExtension.d.ts +10 -1
- package/dist/extensions/BaseExtension.d.ts.map +1 -1
- package/dist/extensions/BaseExtension.js +27 -1
- package/dist/extensions/BaseExtension.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +24 -6
- package/dist/extensions/index.js.map +1 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +105 -51
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/Player.d.ts +90 -15
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +487 -81
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +70 -6
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +184 -19
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +19 -0
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +21 -0
- package/dist/structures/Queue.js.map +1 -1
- package/dist/types/extension.d.ts +3 -0
- package/dist/types/extension.d.ts.map +1 -1
- package/dist/types/index.d.ts +69 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/extensions/BaseExtension.ts +31 -1
- package/src/extensions/index.ts +30 -7
- package/src/plugins/index.ts +137 -54
- package/src/structures/Player.ts +2937 -2457
- package/src/structures/PlayerManager.ts +916 -725
- package/src/structures/Queue.ts +621 -599
- package/src/types/extension.ts +3 -0
- package/src/types/index.ts +80 -2
|
@@ -4,6 +4,7 @@ exports.Player = void 0;
|
|
|
4
4
|
const events_1 = require("events");
|
|
5
5
|
const voice_1 = require("@discordjs/voice");
|
|
6
6
|
const lru_cache_1 = require("lru-cache");
|
|
7
|
+
const types_1 = require("../types");
|
|
7
8
|
const Queue_1 = require("./Queue");
|
|
8
9
|
const plugins_1 = require("../plugins");
|
|
9
10
|
const extensions_1 = require("../extensions");
|
|
@@ -51,11 +52,14 @@ class Player extends events_1.EventEmitter {
|
|
|
51
52
|
super();
|
|
52
53
|
this.connection = null;
|
|
53
54
|
this.volume = 100;
|
|
54
|
-
this.isPlaying = false;
|
|
55
|
-
this.isPaused = false;
|
|
56
55
|
this._lastActivity = Date.now();
|
|
57
|
-
this.
|
|
56
|
+
this._remotePaused = false;
|
|
58
57
|
this.currentResource = null;
|
|
58
|
+
this.destroyed = false;
|
|
59
|
+
this.playbackMode = types_1.PlaybackMode.NATIVE;
|
|
60
|
+
this.forwardFollowers = new Set();
|
|
61
|
+
this.forwardLeader = null;
|
|
62
|
+
this.leaveTimeout = null;
|
|
59
63
|
this.volumeInterval = null;
|
|
60
64
|
this.stuckTimer = null;
|
|
61
65
|
this.skipLoop = false;
|
|
@@ -103,7 +107,6 @@ class Player extends events_1.EventEmitter {
|
|
|
103
107
|
this.loudnessMaxBoostDb = 8;
|
|
104
108
|
this.loudnessMaxCutDb = 10;
|
|
105
109
|
this.loudnessLimiterCeiling = 0.95;
|
|
106
|
-
this.destroyed = false;
|
|
107
110
|
this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
108
111
|
this.ttsPlayer = null;
|
|
109
112
|
this.lastDuration = 0;
|
|
@@ -181,6 +184,7 @@ class Player extends events_1.EventEmitter {
|
|
|
181
184
|
this.loudnessMaxCutDb = Math.max(0, loudnessOptions.maxCutDb ?? 10);
|
|
182
185
|
this.loudnessLimiterCeiling = Math.min(1, Math.max(0.1, loudnessOptions.limiterCeiling ?? 0.95));
|
|
183
186
|
this.debug(`[Player] Runtime options resolved: lowPerformance=${this.lowPerformanceMode}, preload=${this.preloadEnabled}, crossfade=${this.crossfadeEnabled} (${this.crossfadeDurationMs}ms), smartTransition=${this.smartTransitionEnabled}, antiStuck=${this.antiStuckEnabled}, loudnessNormalization=${this.loudnessNormalizationEnabled}`);
|
|
187
|
+
this.trackMiddlewareChain = [...this.manager.getTrackMiddlewareChain(), ...(0, types_1.normalizeTrackMiddleware)(options.trackMiddleware)];
|
|
184
188
|
this.filter = new FilterManager_1.FilterManager(this, this.manager);
|
|
185
189
|
this.extensionManager = new extensions_1.ExtensionManager(this, this.manager);
|
|
186
190
|
this.pluginManager = new plugins_1.PluginManager(this, this.manager, {
|
|
@@ -375,6 +379,10 @@ class Player extends events_1.EventEmitter {
|
|
|
375
379
|
* await player.play(null); // play from queue
|
|
376
380
|
*/
|
|
377
381
|
async play(query, requestedBy) {
|
|
382
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
383
|
+
this.debug("[Player] Cannot play while subscribed to another player. Call unsubscribeForward() first.");
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
378
386
|
const debugInfo = query === null ? "null"
|
|
379
387
|
: typeof query === "string" ? query
|
|
380
388
|
: "tracks" in query ? `${query.tracks.length} tracks`
|
|
@@ -772,16 +780,44 @@ class Player extends events_1.EventEmitter {
|
|
|
772
780
|
}
|
|
773
781
|
}
|
|
774
782
|
}
|
|
783
|
+
mergeTrackPreserveRef(target, source) {
|
|
784
|
+
if (source === target)
|
|
785
|
+
return;
|
|
786
|
+
const mergedMeta = {
|
|
787
|
+
...(target.metadata || {}),
|
|
788
|
+
...(source.metadata || {}),
|
|
789
|
+
};
|
|
790
|
+
Object.assign(target, source);
|
|
791
|
+
target.metadata = mergedMeta;
|
|
792
|
+
}
|
|
793
|
+
async applyTrackMiddleware(track) {
|
|
794
|
+
if (this.trackMiddlewareChain.length === 0)
|
|
795
|
+
return;
|
|
796
|
+
const ctx = { player: this, manager: this.manager };
|
|
797
|
+
for (const mw of this.trackMiddlewareChain) {
|
|
798
|
+
try {
|
|
799
|
+
const out = await mw(track, ctx);
|
|
800
|
+
if (out != null && out !== track) {
|
|
801
|
+
this.mergeTrackPreserveRef(track, out);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
this.debug(`[TrackMiddleware] Error:`, err);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
775
809
|
async getStream(track) {
|
|
776
810
|
if (this.destroyed) {
|
|
777
811
|
throw new Error("PLAYER_DESTROYED");
|
|
778
812
|
}
|
|
813
|
+
await this.applyTrackMiddleware(track);
|
|
779
814
|
const trackId = track.id || track.url || track.title;
|
|
780
815
|
const existingStream = this.streamManager.getStreamByTrack(trackId);
|
|
781
816
|
if (existingStream && !existingStream.destroyed) {
|
|
782
817
|
this.debug(`[Stream] Using existing stream from manager for: ${track.title}`);
|
|
783
818
|
return { stream: existingStream, type: "arbitrary" };
|
|
784
819
|
}
|
|
820
|
+
// FIRST: Try to get stream from extensions
|
|
785
821
|
let stream = await this.extensionManager.provideStream(track);
|
|
786
822
|
if (this.destroyed) {
|
|
787
823
|
if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
|
|
@@ -789,10 +825,24 @@ class Player extends events_1.EventEmitter {
|
|
|
789
825
|
}
|
|
790
826
|
throw new Error("PLAYER_DESTROYED");
|
|
791
827
|
}
|
|
828
|
+
// Handle remote playback - THIS SHOULD BE FIRST PRIORITY
|
|
829
|
+
if (stream?.remote && stream.handle) {
|
|
830
|
+
this.debug(`[Stream] Remote handle provided by extension for: ${track.title}`);
|
|
831
|
+
this.playbackMode = types_1.PlaybackMode.REMOTE;
|
|
832
|
+
this.preloadEnabled = false;
|
|
833
|
+
this.crossfadeEnabled = false;
|
|
834
|
+
// Clear any existing preload for remote mode
|
|
835
|
+
this.cancelPreload();
|
|
836
|
+
return stream;
|
|
837
|
+
}
|
|
838
|
+
// If extension returned a regular stream
|
|
792
839
|
if (stream?.stream) {
|
|
793
840
|
this.debug(`[Stream] Extension provided stream for: ${track.title}`);
|
|
841
|
+
this.playbackMode = types_1.PlaybackMode.NATIVE;
|
|
794
842
|
return stream;
|
|
795
843
|
}
|
|
844
|
+
// SECOND: Try plugins only if extension didn't handle it
|
|
845
|
+
this.debug(`[Stream] Extension didn't provide stream, trying plugins for: ${track.title}`);
|
|
796
846
|
stream = await this.pluginManager.getStream(track);
|
|
797
847
|
if (this.destroyed) {
|
|
798
848
|
if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
|
|
@@ -807,10 +857,11 @@ class Player extends events_1.EventEmitter {
|
|
|
807
857
|
stream.stream.destroy();
|
|
808
858
|
return { stream: existingAgain, type: "arbitrary" };
|
|
809
859
|
}
|
|
810
|
-
// Register with StreamManager
|
|
811
860
|
this.debug(`[Stream] Plugin provided stream for: ${track.title}`);
|
|
861
|
+
this.playbackMode = types_1.PlaybackMode.NATIVE;
|
|
812
862
|
return stream;
|
|
813
863
|
}
|
|
864
|
+
// Check if any plugin claims to support this track but failed
|
|
814
865
|
if (!this.pluginManager.hasStreamCandidate(track)) {
|
|
815
866
|
throw new Error(`UNRECOVERABLE_NO_PLUGIN:${track.title}`);
|
|
816
867
|
}
|
|
@@ -827,13 +878,25 @@ class Player extends events_1.EventEmitter {
|
|
|
827
878
|
async startTrack(track) {
|
|
828
879
|
if (this.destroyed)
|
|
829
880
|
return false;
|
|
881
|
+
// First, get stream info (this will handle remote detection)
|
|
882
|
+
let streamInfo = null;
|
|
883
|
+
try {
|
|
884
|
+
streamInfo = await this.getStream(track);
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
this.debug(`[Player] Failed to get stream for track: ${track.title}`, error);
|
|
888
|
+
throw error;
|
|
889
|
+
}
|
|
890
|
+
// Handle remote playback
|
|
891
|
+
if (streamInfo?.remote && streamInfo.handle) {
|
|
892
|
+
return await this.playRemote(track, streamInfo);
|
|
893
|
+
}
|
|
894
|
+
// Handle native playback
|
|
830
895
|
try {
|
|
831
896
|
// Try to use preloaded resource
|
|
832
897
|
if (this.preloadManager.hasValidPreload(track)) {
|
|
833
898
|
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
834
|
-
// Stop current playback
|
|
835
899
|
this.audioPlayer.stop(true);
|
|
836
|
-
// Clean up old current stream (but delay to be safe)
|
|
837
900
|
const oldStreamId = this.currentSlot.streamId;
|
|
838
901
|
if (oldStreamId && this.streamManager) {
|
|
839
902
|
setTimeout(() => {
|
|
@@ -842,29 +905,24 @@ class Player extends events_1.EventEmitter {
|
|
|
842
905
|
}
|
|
843
906
|
}, 3000);
|
|
844
907
|
}
|
|
845
|
-
// Set current slot from preload
|
|
846
908
|
this.promotePreloadToCurrent(track);
|
|
847
909
|
const currentResource = this.currentSlot.resource;
|
|
848
910
|
if (!currentResource) {
|
|
849
911
|
return false;
|
|
850
912
|
}
|
|
851
913
|
const targetVolume = this.getTrackTargetVolume(track);
|
|
852
|
-
// Apply volume
|
|
853
914
|
if (currentResource.volume) {
|
|
854
915
|
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
855
916
|
}
|
|
856
|
-
// Play
|
|
857
917
|
await this.maybeAlignToBeatBoundary();
|
|
858
918
|
this.audioPlayer.play(currentResource);
|
|
859
919
|
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
860
920
|
await this.applyCrossfadeIn(currentResource, track);
|
|
861
|
-
// Start preloading next track (async, don't await)
|
|
862
921
|
this.preloadNextTrack().catch((err) => {
|
|
863
922
|
this.debug(`[Player] Preload error:`, err);
|
|
864
923
|
});
|
|
865
924
|
return true;
|
|
866
925
|
}
|
|
867
|
-
// No valid preload, load fresh
|
|
868
926
|
this.debug(`[Player] No preload available, loading fresh: ${track.title}`);
|
|
869
927
|
return await this.loadFreshStream(track);
|
|
870
928
|
}
|
|
@@ -874,53 +932,6 @@ class Player extends events_1.EventEmitter {
|
|
|
874
932
|
return false;
|
|
875
933
|
}
|
|
876
934
|
}
|
|
877
|
-
/**
|
|
878
|
-
* Swap preload slot to current slot
|
|
879
|
-
*/
|
|
880
|
-
async swapToCurrent(track) {
|
|
881
|
-
if (!this.preloadManager.hasValidPreload(track)) {
|
|
882
|
-
return false;
|
|
883
|
-
}
|
|
884
|
-
const oldStreamId = this.currentSlot.streamId;
|
|
885
|
-
// Stop current playback
|
|
886
|
-
this.audioPlayer.stop(true);
|
|
887
|
-
// Clean up old current stream (but keep it for a moment)
|
|
888
|
-
if (oldStreamId && this.streamManager) {
|
|
889
|
-
// Delay cleanup to avoid destroying if still needed
|
|
890
|
-
setTimeout(() => {
|
|
891
|
-
if (this.currentSlot.streamId === oldStreamId) {
|
|
892
|
-
this.streamManager.unregisterStream(oldStreamId, true);
|
|
893
|
-
}
|
|
894
|
-
}, 5000);
|
|
895
|
-
}
|
|
896
|
-
// Set new current
|
|
897
|
-
this.promotePreloadToCurrent(track);
|
|
898
|
-
const currentResource = this.currentSlot.resource;
|
|
899
|
-
if (!currentResource) {
|
|
900
|
-
return false;
|
|
901
|
-
}
|
|
902
|
-
const targetVolume = this.getTrackTargetVolume(track);
|
|
903
|
-
// Apply volume
|
|
904
|
-
if (currentResource.volume) {
|
|
905
|
-
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
906
|
-
}
|
|
907
|
-
// Play
|
|
908
|
-
await this.maybeAlignToBeatBoundary();
|
|
909
|
-
this.audioPlayer.play(currentResource);
|
|
910
|
-
try {
|
|
911
|
-
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
912
|
-
await this.applyCrossfadeIn(currentResource, track);
|
|
913
|
-
// Start preloading next track
|
|
914
|
-
this.preloadNextTrack().catch((err) => {
|
|
915
|
-
this.debug(`[Player] Preload error:`, err);
|
|
916
|
-
});
|
|
917
|
-
return true;
|
|
918
|
-
}
|
|
919
|
-
catch (err) {
|
|
920
|
-
this.debug(`[Player] Failed to play swapped track:`, err);
|
|
921
|
-
return false;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
935
|
/**
|
|
925
936
|
* Load fresh stream when no preload available
|
|
926
937
|
*/
|
|
@@ -931,6 +942,10 @@ class Player extends events_1.EventEmitter {
|
|
|
931
942
|
await this.safeCancelPreload();
|
|
932
943
|
try {
|
|
933
944
|
const streamInfo = await this.getStream(track);
|
|
945
|
+
// Handle remote playback
|
|
946
|
+
if (streamInfo?.remote && streamInfo.handle) {
|
|
947
|
+
return await this.playRemote(track, streamInfo);
|
|
948
|
+
}
|
|
934
949
|
if (!streamInfo?.stream) {
|
|
935
950
|
throw new Error(`No stream available`);
|
|
936
951
|
}
|
|
@@ -983,8 +998,6 @@ class Player extends events_1.EventEmitter {
|
|
|
983
998
|
if (this.destroyed)
|
|
984
999
|
return false;
|
|
985
1000
|
this.debug("[Player] playNext called");
|
|
986
|
-
// Don't cancel preload here unless absolutely necessary
|
|
987
|
-
// Let startTrack handle it
|
|
988
1001
|
while (true) {
|
|
989
1002
|
const track = this.queue.next(this.skipLoop);
|
|
990
1003
|
this.skipLoop = false;
|
|
@@ -1005,7 +1018,6 @@ class Player extends events_1.EventEmitter {
|
|
|
1005
1018
|
}
|
|
1006
1019
|
}
|
|
1007
1020
|
this.debug(`[Player] No next track in queue`);
|
|
1008
|
-
this.isPlaying = false;
|
|
1009
1021
|
this.emit("queueEnd");
|
|
1010
1022
|
// Clean up both slots when queue is empty
|
|
1011
1023
|
this.clearSlot(this.currentSlot);
|
|
@@ -1024,6 +1036,11 @@ class Player extends events_1.EventEmitter {
|
|
|
1024
1036
|
this.antiStuckConsecutiveFailures = 0;
|
|
1025
1037
|
return true;
|
|
1026
1038
|
}
|
|
1039
|
+
// For remote playback, if startTrack returns false, it's a failure
|
|
1040
|
+
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
1041
|
+
this.debug(`[Player] Remote track failed to start: ${track.title}`);
|
|
1042
|
+
continue; // Skip to next track
|
|
1043
|
+
}
|
|
1027
1044
|
const recovered = await this.attemptTrackRecovery(track, new Error("TRACK_START_RETURNED_FALSE"));
|
|
1028
1045
|
if (recovered) {
|
|
1029
1046
|
return true;
|
|
@@ -1038,6 +1055,11 @@ class Player extends events_1.EventEmitter {
|
|
|
1038
1055
|
catch (err) {
|
|
1039
1056
|
this.debug(`[Player] playNext error:`, err);
|
|
1040
1057
|
this.emit("playerError", err, track);
|
|
1058
|
+
// For remote playback, just skip to next track
|
|
1059
|
+
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
1060
|
+
this.debug(`[Player] Remote track error, skipping: ${track.title}`);
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1041
1063
|
if (this.isUnrecoverableStreamError(err)) {
|
|
1042
1064
|
this.debug(`[Player] Skipping unrecoverable track (no plugin): ${track.title}`);
|
|
1043
1065
|
continue;
|
|
@@ -1056,6 +1078,27 @@ class Player extends events_1.EventEmitter {
|
|
|
1056
1078
|
}
|
|
1057
1079
|
}
|
|
1058
1080
|
}
|
|
1081
|
+
async playRemote(track, stream) {
|
|
1082
|
+
if (!stream.handle)
|
|
1083
|
+
return false;
|
|
1084
|
+
try {
|
|
1085
|
+
// Store the remote handle for later use
|
|
1086
|
+
this.remoteHandle = stream.handle;
|
|
1087
|
+
// Set current track before playing
|
|
1088
|
+
this.queue.setCurrentTrack(track);
|
|
1089
|
+
// Emit track start event before playing (so UI updates)
|
|
1090
|
+
this.emit("trackStart", track);
|
|
1091
|
+
// Start playback via remote handle
|
|
1092
|
+
await stream.handle.play();
|
|
1093
|
+
this.debug(`[Player] Remote playback started for: ${track.title}`);
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
catch (error) {
|
|
1097
|
+
this.debug(`[Player] Remote playback error:`, error);
|
|
1098
|
+
this.emit("playerError", error, track);
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1059
1102
|
//#endregion
|
|
1060
1103
|
//#region TTS
|
|
1061
1104
|
ensureTTSPlayer() {
|
|
@@ -1088,6 +1131,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1088
1131
|
if (!this.connection)
|
|
1089
1132
|
throw new Error("No voice connection for TTS");
|
|
1090
1133
|
const ttsPlayer = this.ensureTTSPlayer();
|
|
1134
|
+
await this.applyTrackMiddleware(track);
|
|
1091
1135
|
// Build resource from plugin stream
|
|
1092
1136
|
const streamInfo = await this.pluginManager.getStream(track);
|
|
1093
1137
|
if (!streamInfo) {
|
|
@@ -1205,6 +1249,145 @@ class Player extends events_1.EventEmitter {
|
|
|
1205
1249
|
throw error;
|
|
1206
1250
|
}
|
|
1207
1251
|
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Subscribe this player to another player's playback stream.
|
|
1254
|
+
*
|
|
1255
|
+
* This enables "forward mode", where the follower player directly subscribes
|
|
1256
|
+
* to the leader player's {@link audioPlayer} instead of creating its own stream.
|
|
1257
|
+
*
|
|
1258
|
+
* Greatly reduces CPU, bandwidth, and extractor usage because only the leader
|
|
1259
|
+
* creates and decodes the audio resource.
|
|
1260
|
+
*
|
|
1261
|
+
* ## Features
|
|
1262
|
+
* - Real-time shared playback
|
|
1263
|
+
* - Followers may join at any time
|
|
1264
|
+
* - Automatic track synchronization
|
|
1265
|
+
* - Optional volume synchronization
|
|
1266
|
+
* - Automatic cleanup on destroy
|
|
1267
|
+
* - Supports unlimited followers
|
|
1268
|
+
*
|
|
1269
|
+
* ## Lifecycle
|
|
1270
|
+
* - When the leader starts a track, followers automatically receive the same track metadata.
|
|
1271
|
+
* - When the leader pauses/resumes/stops, followers are synchronized.
|
|
1272
|
+
* - Destroying the leader automatically unsubscribes all followers.
|
|
1273
|
+
* - Destroying a follower only removes that follower.
|
|
1274
|
+
*
|
|
1275
|
+
* ## Notes
|
|
1276
|
+
* - Both players must already be connected to voice.
|
|
1277
|
+
* - A player cannot subscribe to itself.
|
|
1278
|
+
* - Existing playback subscriptions are automatically replaced.
|
|
1279
|
+
*
|
|
1280
|
+
* @param {Player} leader The leader player to subscribe to.
|
|
1281
|
+
* @param options Additional playback mirror options.
|
|
1282
|
+
* @param options.syncVolume When true, follower volume automatically follows the leader. Default: true.
|
|
1283
|
+
*
|
|
1284
|
+
* @returns {boolean} True if subscription succeeded.
|
|
1285
|
+
*
|
|
1286
|
+
* @example
|
|
1287
|
+
* follower.subscribeTo(leader);
|
|
1288
|
+
*
|
|
1289
|
+
* @example
|
|
1290
|
+
* follower.subscribeTo(leader, {
|
|
1291
|
+
* syncVolume: true,
|
|
1292
|
+
* });
|
|
1293
|
+
*/
|
|
1294
|
+
subscribeTo(leader, options) {
|
|
1295
|
+
if (!leader)
|
|
1296
|
+
return false;
|
|
1297
|
+
if (leader === this) {
|
|
1298
|
+
this.debug(`[Player] Cannot subscribe to self`);
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
if (leader.destroyed) {
|
|
1302
|
+
this.debug("[Player] Cannot subscribe to destroyed leader");
|
|
1303
|
+
return false;
|
|
1304
|
+
}
|
|
1305
|
+
if (this.destroyed) {
|
|
1306
|
+
this.debug("[Player] Cannot subscribe destroyed player");
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
if (!!leader.forwardLeader) {
|
|
1310
|
+
this.debug("[Player] Cannot subscribe to follower player");
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
if (!this.connection || !leader.connection) {
|
|
1314
|
+
this.debug(`[Player] Missing connection for subscribeTo`);
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
// cleanup old leader
|
|
1318
|
+
if (this.forwardLeader) {
|
|
1319
|
+
this.unsubscribeForward("This Player new subscribeTo " + leader.guildId);
|
|
1320
|
+
}
|
|
1321
|
+
this.forwardLeader = leader;
|
|
1322
|
+
leader.forwardFollowers.add(this);
|
|
1323
|
+
try {
|
|
1324
|
+
// clear local playback
|
|
1325
|
+
this.stop();
|
|
1326
|
+
// detach current followers first
|
|
1327
|
+
for (const fp of [...this.forwardFollowers]) {
|
|
1328
|
+
try {
|
|
1329
|
+
fp.unsubscribeForward("Leader new subscribeTo " + leader.guildId);
|
|
1330
|
+
}
|
|
1331
|
+
catch { }
|
|
1332
|
+
}
|
|
1333
|
+
this.forwardFollowers.clear();
|
|
1334
|
+
this.queue.clear();
|
|
1335
|
+
if (leader.currentTrack) {
|
|
1336
|
+
this.queue.setCurrentTrack(leader.currentTrack);
|
|
1337
|
+
}
|
|
1338
|
+
if (options?.forwardMode ?? true)
|
|
1339
|
+
this.playbackMode = types_1.PlaybackMode.FORWARD;
|
|
1340
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD && this.connection) {
|
|
1341
|
+
this.connection.subscribe(leader.audioPlayer);
|
|
1342
|
+
}
|
|
1343
|
+
this.volume = leader.volume;
|
|
1344
|
+
this.emit("forwardModeStart", leader);
|
|
1345
|
+
this.debug(`[Player] Forward mode subscribed ${this.guildId} -> ${leader.guildId}`);
|
|
1346
|
+
return true;
|
|
1347
|
+
}
|
|
1348
|
+
catch (e) {
|
|
1349
|
+
this.debug(`[Player] subscribeTo error:`, e);
|
|
1350
|
+
this.forwardLeader = null;
|
|
1351
|
+
leader.forwardFollowers.delete(this);
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Unsubscribe this player from its current playback leader.
|
|
1357
|
+
*
|
|
1358
|
+
* This disables forward mode and restores the player's own audioPlayer
|
|
1359
|
+
* subscription back to its voice connection.
|
|
1360
|
+
*
|
|
1361
|
+
* Automatically emitted when:
|
|
1362
|
+
* - The leader player is destroyed
|
|
1363
|
+
* - This player is destroyed
|
|
1364
|
+
* - A new leader subscription replaces the old one
|
|
1365
|
+
*
|
|
1366
|
+
* Emits:
|
|
1367
|
+
* - `forwardModeEnd`
|
|
1368
|
+
*
|
|
1369
|
+
* @returns {boolean} True if a playback subscription existed and was removed.
|
|
1370
|
+
*
|
|
1371
|
+
* @example
|
|
1372
|
+
* follower.unsubscribeForward();
|
|
1373
|
+
*/
|
|
1374
|
+
unsubscribeForward(reason) {
|
|
1375
|
+
if (!this.forwardLeader) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
const leader = this.forwardLeader;
|
|
1379
|
+
leader.forwardFollowers.delete(this);
|
|
1380
|
+
this.forwardLeader = null;
|
|
1381
|
+
this.playbackMode = types_1.PlaybackMode.NATIVE;
|
|
1382
|
+
try {
|
|
1383
|
+
this.connection?.subscribe(this.audioPlayer);
|
|
1384
|
+
}
|
|
1385
|
+
catch { }
|
|
1386
|
+
this.queue.clear();
|
|
1387
|
+
this.emit("forwardModeEnd", leader, reason);
|
|
1388
|
+
this.debug(`[Player] Forward mode unsubscribed ${this.guildId} <- ${leader.guildId}: ${reason ?? null}`);
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1208
1391
|
/**
|
|
1209
1392
|
* Pause the current track
|
|
1210
1393
|
*
|
|
@@ -1214,10 +1397,22 @@ class Player extends events_1.EventEmitter {
|
|
|
1214
1397
|
* console.log(`Paused: ${paused}`);
|
|
1215
1398
|
*/
|
|
1216
1399
|
pause() {
|
|
1400
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
1401
|
+
this.debug("[Player] Cannot pause while subscribed to another player");
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1217
1404
|
this.debug(`[Player] pause called`);
|
|
1218
|
-
if (this.
|
|
1219
|
-
|
|
1405
|
+
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
1406
|
+
if (!this.remoteHandle)
|
|
1407
|
+
return false;
|
|
1408
|
+
void this.remoteHandle.pause().catch((e) => this.debug("[Player] Remote pause:", e));
|
|
1409
|
+
const track = this.queue.currentTrack;
|
|
1410
|
+
if (track)
|
|
1411
|
+
this.emit("playerPause", track);
|
|
1412
|
+
return true;
|
|
1220
1413
|
}
|
|
1414
|
+
if (this.isPlaying && !this.isPaused)
|
|
1415
|
+
return this.audioPlayer.pause();
|
|
1221
1416
|
return false;
|
|
1222
1417
|
}
|
|
1223
1418
|
/**
|
|
@@ -1229,7 +1424,20 @@ class Player extends events_1.EventEmitter {
|
|
|
1229
1424
|
* console.log(`Resumed: ${resumed}`);
|
|
1230
1425
|
*/
|
|
1231
1426
|
resume() {
|
|
1427
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
1428
|
+
this.debug("[Player] Cannot resume while subscribed to another player");
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1232
1431
|
this.debug(`[Player] resume called`);
|
|
1432
|
+
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
1433
|
+
if (!this.remoteHandle)
|
|
1434
|
+
return false;
|
|
1435
|
+
void this.remoteHandle.resume().catch((e) => this.debug("[Player] Remote resume:", e));
|
|
1436
|
+
const track = this.queue.currentTrack;
|
|
1437
|
+
if (track)
|
|
1438
|
+
this.emit("playerResume", track);
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1233
1441
|
if (this.isPaused) {
|
|
1234
1442
|
const result = this.audioPlayer.unpause();
|
|
1235
1443
|
if (result) {
|
|
@@ -1252,16 +1460,33 @@ class Player extends events_1.EventEmitter {
|
|
|
1252
1460
|
* console.log(`Stopped: ${stopped}`);
|
|
1253
1461
|
*/
|
|
1254
1462
|
stop() {
|
|
1463
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
1464
|
+
this.debug("[Player] Cannot stop while subscribed to another player");
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1255
1467
|
this.debug(`[Player] stop called`);
|
|
1468
|
+
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
1469
|
+
this.cancelPreload();
|
|
1470
|
+
this.queue.clear();
|
|
1471
|
+
void this.remoteHandle?.stop().catch((e) => this.debug("[Player] Remote stop:", e));
|
|
1472
|
+
this.emit("playerStop");
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1256
1475
|
// Cancel preload when stopping
|
|
1257
1476
|
this.cancelPreload();
|
|
1258
1477
|
this.queue.clear();
|
|
1259
1478
|
const result = this.audioPlayer.stop();
|
|
1260
1479
|
this.destroyCurrentStream();
|
|
1261
1480
|
this.currentResource = null;
|
|
1262
|
-
this.isPlaying = false;
|
|
1263
|
-
this.isPaused = false;
|
|
1264
1481
|
this.emit("playerStop");
|
|
1482
|
+
for (const fp of this.forwardFollowers) {
|
|
1483
|
+
try {
|
|
1484
|
+
fp.connection?.subscribe(fp.audioPlayer);
|
|
1485
|
+
fp.audioPlayer.stop(true);
|
|
1486
|
+
fp.emit("playerStop");
|
|
1487
|
+
}
|
|
1488
|
+
catch { }
|
|
1489
|
+
}
|
|
1265
1490
|
return result;
|
|
1266
1491
|
}
|
|
1267
1492
|
/**
|
|
@@ -1278,7 +1503,17 @@ class Player extends events_1.EventEmitter {
|
|
|
1278
1503
|
* await player.seek(90000);
|
|
1279
1504
|
*/
|
|
1280
1505
|
async seek(position) {
|
|
1506
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
1507
|
+
this.debug("[Player] Cannot seek while subscribed to another player");
|
|
1508
|
+
return false;
|
|
1509
|
+
}
|
|
1281
1510
|
this.debug(`[Player] seek called with position: ${position}ms`);
|
|
1511
|
+
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
1512
|
+
if (!this.remoteHandle)
|
|
1513
|
+
return false;
|
|
1514
|
+
await this.remoteHandle.seek(position);
|
|
1515
|
+
return true;
|
|
1516
|
+
}
|
|
1282
1517
|
const track = this.queue.currentTrack;
|
|
1283
1518
|
if (!track) {
|
|
1284
1519
|
this.debug(`[Player] No current track to seek`);
|
|
@@ -1303,7 +1538,20 @@ class Player extends events_1.EventEmitter {
|
|
|
1303
1538
|
* console.log(`Skipped: ${skipped}`);
|
|
1304
1539
|
*/
|
|
1305
1540
|
skip(index) {
|
|
1541
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
1542
|
+
this.debug("[Player] Cannot skip while subscribed to another player");
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1306
1545
|
this.debug(`[Player] skip called with index: ${index}`);
|
|
1546
|
+
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
1547
|
+
if (typeof index === "number" && index >= 0) {
|
|
1548
|
+
for (let i = 0; i < index; i++)
|
|
1549
|
+
this.queue.remove(0);
|
|
1550
|
+
}
|
|
1551
|
+
// signal the remote backend to stop; TrackEndEvent triggers playNext()
|
|
1552
|
+
void this.remoteHandle?.stop().catch((e) => this.debug("[Player] Remote skip:", e));
|
|
1553
|
+
return true;
|
|
1554
|
+
}
|
|
1307
1555
|
try {
|
|
1308
1556
|
if (typeof index === "number" && index >= 0) {
|
|
1309
1557
|
const targetTrack = this.queue.getTrack(index);
|
|
@@ -1340,6 +1588,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1340
1588
|
* console.log(`Previous: ${previous}`);
|
|
1341
1589
|
*/
|
|
1342
1590
|
async previous() {
|
|
1591
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
1592
|
+
this.debug("[Player] Cannot previous while subscribed to another player");
|
|
1593
|
+
return false;
|
|
1594
|
+
}
|
|
1343
1595
|
this.debug(`[Player] previous called`);
|
|
1344
1596
|
const track = this.queue.previous();
|
|
1345
1597
|
if (!track)
|
|
@@ -1392,6 +1644,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1392
1644
|
saveOptions = options;
|
|
1393
1645
|
}
|
|
1394
1646
|
try {
|
|
1647
|
+
await this.applyTrackMiddleware(track);
|
|
1395
1648
|
// Skip extension manager for saving - we want the raw stream without filters/seek applied, and extensions may not support this
|
|
1396
1649
|
let streamInfo = await this.pluginManager.getStream(track);
|
|
1397
1650
|
if (!streamInfo || !streamInfo.stream) {
|
|
@@ -1468,6 +1721,12 @@ class Player extends events_1.EventEmitter {
|
|
|
1468
1721
|
* console.log(`Auto-play mode: ${autoPlayMode}`);
|
|
1469
1722
|
*/
|
|
1470
1723
|
autoPlay(mode) {
|
|
1724
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
1725
|
+
if (!mode)
|
|
1726
|
+
return this.forwardLeader?.autoPlay() ?? false;
|
|
1727
|
+
this.debug("[Player] Cannot autoPlay while subscribed to another player");
|
|
1728
|
+
return false;
|
|
1729
|
+
}
|
|
1471
1730
|
return this.queue.autoPlay(mode);
|
|
1472
1731
|
}
|
|
1473
1732
|
/**
|
|
@@ -1480,11 +1739,20 @@ class Player extends events_1.EventEmitter {
|
|
|
1480
1739
|
* console.log(`Volume set: ${volumeSet}`);
|
|
1481
1740
|
*/
|
|
1482
1741
|
setVolume(volume) {
|
|
1742
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
1743
|
+
this.debug("[Player] Cannot setVolume while subscribed to another player");
|
|
1744
|
+
return false;
|
|
1745
|
+
}
|
|
1483
1746
|
this.debug(`[Player] setVolume called: ${volume}`);
|
|
1484
1747
|
if (volume < 0 || volume > 200)
|
|
1485
1748
|
return false;
|
|
1486
1749
|
const oldVolume = this.volume;
|
|
1487
1750
|
this.volume = volume;
|
|
1751
|
+
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
1752
|
+
void this.remoteHandle?.setVolume(volume).catch((e) => this.debug("[Player] Remote volume:", e));
|
|
1753
|
+
this.emit("volumeChange", oldVolume, volume);
|
|
1754
|
+
return true;
|
|
1755
|
+
}
|
|
1488
1756
|
const resourceVolume = this.currentResource?.volume;
|
|
1489
1757
|
if (resourceVolume) {
|
|
1490
1758
|
if (this.volumeInterval)
|
|
@@ -1504,6 +1772,12 @@ class Player extends events_1.EventEmitter {
|
|
|
1504
1772
|
}, 300);
|
|
1505
1773
|
}
|
|
1506
1774
|
this.emit("volumeChange", oldVolume, volume);
|
|
1775
|
+
for (const fp of this.forwardFollowers) {
|
|
1776
|
+
try {
|
|
1777
|
+
fp.volume = volume;
|
|
1778
|
+
}
|
|
1779
|
+
catch { }
|
|
1780
|
+
}
|
|
1507
1781
|
return true;
|
|
1508
1782
|
}
|
|
1509
1783
|
/**
|
|
@@ -1544,6 +1818,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1544
1818
|
*/
|
|
1545
1819
|
async insert(query, index, requestedBy) {
|
|
1546
1820
|
try {
|
|
1821
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
1822
|
+
this.debug("[Player] Cannot insert while subscribed to another player");
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1547
1825
|
this.debug(`[Player] insert called at index ${index} with type: ${typeof query}`);
|
|
1548
1826
|
let tracksToAdd = [];
|
|
1549
1827
|
let isPlaylist = false;
|
|
@@ -1754,6 +2032,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1754
2032
|
if (this.destroyed)
|
|
1755
2033
|
return;
|
|
1756
2034
|
this.destroyed = true;
|
|
2035
|
+
if (this.remoteHandle?.destroy) {
|
|
2036
|
+
this.remoteHandle.destroy().catch(() => { });
|
|
2037
|
+
this.remoteHandle = undefined;
|
|
2038
|
+
}
|
|
1757
2039
|
if (this.leaveTimeout) {
|
|
1758
2040
|
clearTimeout(this.leaveTimeout);
|
|
1759
2041
|
this.leaveTimeout = null;
|
|
@@ -1781,14 +2063,21 @@ class Player extends events_1.EventEmitter {
|
|
|
1781
2063
|
this.pluginManager.clear();
|
|
1782
2064
|
this.filter.destroy();
|
|
1783
2065
|
this.extensionManager.destroy();
|
|
1784
|
-
this.isPlaying = false;
|
|
1785
|
-
this.isPaused = false;
|
|
1786
2066
|
// Clear any remaining intervals
|
|
1787
2067
|
if (this.volumeInterval) {
|
|
1788
2068
|
clearInterval(this.volumeInterval);
|
|
1789
2069
|
this.volumeInterval = null;
|
|
1790
2070
|
}
|
|
1791
2071
|
this.emit("playerDestroy");
|
|
2072
|
+
this.unsubscribeForward("Player destroy");
|
|
2073
|
+
// release followers
|
|
2074
|
+
for (const fp of [...this.forwardFollowers]) {
|
|
2075
|
+
try {
|
|
2076
|
+
fp.unsubscribeForward("Leader destroy");
|
|
2077
|
+
}
|
|
2078
|
+
catch { }
|
|
2079
|
+
}
|
|
2080
|
+
this.forwardFollowers.clear();
|
|
1792
2081
|
this.removeAllListeners();
|
|
1793
2082
|
}
|
|
1794
2083
|
//#endregion
|
|
@@ -1860,13 +2149,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1860
2149
|
this.audioPlayer.play(resource);
|
|
1861
2150
|
}
|
|
1862
2151
|
// Restore playing state
|
|
1863
|
-
if (
|
|
1864
|
-
this.isPlaying = true;
|
|
1865
|
-
this.isPaused = false;
|
|
1866
|
-
}
|
|
1867
|
-
else if (wasPaused) {
|
|
1868
|
-
this.isPlaying = false;
|
|
1869
|
-
this.isPaused = true;
|
|
2152
|
+
if (wasPaused) {
|
|
1870
2153
|
this.audioPlayer.pause();
|
|
1871
2154
|
}
|
|
1872
2155
|
this.debug(`[Player] Successfully applied filter to current track at position ${currentPosition}ms`);
|
|
@@ -1932,6 +2215,9 @@ class Player extends events_1.EventEmitter {
|
|
|
1932
2215
|
if (track) {
|
|
1933
2216
|
this.debug(`[Player] Track ended: ${track.title}`);
|
|
1934
2217
|
this.emit("trackEnd", track);
|
|
2218
|
+
for (const fp of this.forwardFollowers) {
|
|
2219
|
+
fp.emit("trackEnd", track);
|
|
2220
|
+
}
|
|
1935
2221
|
}
|
|
1936
2222
|
void this.playNext();
|
|
1937
2223
|
}
|
|
@@ -1939,30 +2225,41 @@ class Player extends events_1.EventEmitter {
|
|
|
1939
2225
|
(oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) {
|
|
1940
2226
|
// Track started
|
|
1941
2227
|
this.clearLeaveTimeout();
|
|
1942
|
-
this.isPlaying = true;
|
|
1943
|
-
this.isPaused = false;
|
|
1944
2228
|
const track = this.queue.currentTrack;
|
|
1945
2229
|
if (track) {
|
|
1946
2230
|
this.debug(`[Player] Track started: ${track.title}`);
|
|
1947
2231
|
this.emit("trackStart", track);
|
|
2232
|
+
for (const fp of this.forwardFollowers) {
|
|
2233
|
+
try {
|
|
2234
|
+
fp.queue.clear();
|
|
2235
|
+
fp.connection?.subscribe(this.audioPlayer);
|
|
2236
|
+
fp.queue.setCurrentTrack(track);
|
|
2237
|
+
fp.emit("trackStart", track);
|
|
2238
|
+
}
|
|
2239
|
+
catch (e) {
|
|
2240
|
+
this.debug(`[Player] Failed to sync follower ${fp.guildId}:`, e);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
1948
2243
|
}
|
|
1949
2244
|
}
|
|
1950
2245
|
else if (newState.status === voice_1.AudioPlayerStatus.Paused && oldState.status !== voice_1.AudioPlayerStatus.Paused) {
|
|
1951
|
-
// Track paused
|
|
1952
|
-
this.isPaused = true;
|
|
1953
2246
|
const track = this.queue.currentTrack;
|
|
1954
2247
|
if (track) {
|
|
1955
2248
|
this.debug(`[Player] Player paused on track: ${track.title}`);
|
|
1956
2249
|
this.emit("playerPause", track);
|
|
2250
|
+
for (const fp of this.forwardFollowers) {
|
|
2251
|
+
fp.emit("playerPause", track);
|
|
2252
|
+
}
|
|
1957
2253
|
}
|
|
1958
2254
|
}
|
|
1959
2255
|
else if (newState.status !== voice_1.AudioPlayerStatus.Paused && oldState.status === voice_1.AudioPlayerStatus.Paused) {
|
|
1960
|
-
// Track resumed
|
|
1961
|
-
this.isPaused = false;
|
|
1962
2256
|
const track = this.queue.currentTrack;
|
|
1963
2257
|
if (track) {
|
|
1964
2258
|
this.debug(`[Player] Player resumed on track: ${track.title}`);
|
|
1965
2259
|
this.emit("playerResume", track);
|
|
2260
|
+
for (const fp of this.forwardFollowers) {
|
|
2261
|
+
fp.emit("playerResume", track);
|
|
2262
|
+
}
|
|
1966
2263
|
}
|
|
1967
2264
|
}
|
|
1968
2265
|
else if (newState.status === voice_1.AudioPlayerStatus.AutoPaused) {
|
|
@@ -2056,6 +2353,25 @@ class Player extends events_1.EventEmitter {
|
|
|
2056
2353
|
plugins: this.pluginManager.getAll().map((plugin) => plugin.name),
|
|
2057
2354
|
};
|
|
2058
2355
|
}
|
|
2356
|
+
exitRemoteMode() {
|
|
2357
|
+
if (this.playbackMode !== types_1.PlaybackMode.REMOTE)
|
|
2358
|
+
return;
|
|
2359
|
+
this.debug("[Player] Exiting REMOTE mode, restoring native playback");
|
|
2360
|
+
void this.remoteHandle?.destroy().catch(() => { });
|
|
2361
|
+
this.remoteHandle = undefined;
|
|
2362
|
+
this.playbackMode = types_1.PlaybackMode.NATIVE;
|
|
2363
|
+
this._remotePaused = false;
|
|
2364
|
+
// Restore preload/crossfade from original options
|
|
2365
|
+
const preloadOptions = this.options.preload ?? {};
|
|
2366
|
+
const autoDisable = preloadOptions.autoDisableInLowPerformance ?? true;
|
|
2367
|
+
this.preloadEnabled = (preloadOptions.enabled ?? true) && !(this.lowPerformanceMode && autoDisable);
|
|
2368
|
+
const crossfadeOptions = this.options.crossfade ?? {};
|
|
2369
|
+
const cfAutoDisable = crossfadeOptions.autoDisableInLowPerformance ?? true;
|
|
2370
|
+
this.crossfadeEnabled =
|
|
2371
|
+
typeof crossfadeOptions.enabled === "boolean" ? crossfadeOptions.enabled : (crossfadeOptions.autoEnable ?? true);
|
|
2372
|
+
if (this.lowPerformanceMode && cfAutoDisable)
|
|
2373
|
+
this.crossfadeEnabled = false;
|
|
2374
|
+
}
|
|
2059
2375
|
/**
|
|
2060
2376
|
* Get serializable state (for manual persistence)
|
|
2061
2377
|
*/
|
|
@@ -2109,6 +2425,59 @@ class Player extends events_1.EventEmitter {
|
|
|
2109
2425
|
totalStreams: this.streamManager.getStreamCount(),
|
|
2110
2426
|
};
|
|
2111
2427
|
}
|
|
2428
|
+
getForwardHealthStatus() {
|
|
2429
|
+
const issues = [];
|
|
2430
|
+
const details = {};
|
|
2431
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD && this.forwardLeader) {
|
|
2432
|
+
// This player is a follower
|
|
2433
|
+
details.leaderId = this.forwardLeader.guildId;
|
|
2434
|
+
details.connectionState = this.connection?.state.status;
|
|
2435
|
+
details.audioPlayerState = this.audioPlayer.state.status;
|
|
2436
|
+
if (this.forwardLeader.destroyed) {
|
|
2437
|
+
issues.push("Leader is destroyed");
|
|
2438
|
+
}
|
|
2439
|
+
if (!this.forwardLeader.connection) {
|
|
2440
|
+
issues.push("Leader has no connection");
|
|
2441
|
+
}
|
|
2442
|
+
if (this.forwardLeader.destroyed || !this.forwardLeader.connection) {
|
|
2443
|
+
issues.push("Leader is unavailable");
|
|
2444
|
+
}
|
|
2445
|
+
return {
|
|
2446
|
+
guildId: this.guildId,
|
|
2447
|
+
healthy: issues.length === 0,
|
|
2448
|
+
role: "follower",
|
|
2449
|
+
issues,
|
|
2450
|
+
details,
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
else if (this.forwardFollowers.size > 0) {
|
|
2454
|
+
// This player is a leader
|
|
2455
|
+
details.followerCount = this.forwardFollowers.size;
|
|
2456
|
+
details.connectionState = this.connection?.state.status;
|
|
2457
|
+
const deadFollowers = [];
|
|
2458
|
+
for (const follower of this.forwardFollowers) {
|
|
2459
|
+
if (follower.destroyed)
|
|
2460
|
+
deadFollowers.push(follower.guildId);
|
|
2461
|
+
}
|
|
2462
|
+
if (deadFollowers.length > 0) {
|
|
2463
|
+
issues.push(`Has ${deadFollowers.length} dead followers: ${deadFollowers.join(", ")}`);
|
|
2464
|
+
}
|
|
2465
|
+
return {
|
|
2466
|
+
guildId: this.guildId,
|
|
2467
|
+
healthy: true, // Leader being healthy doesn't depend on followers
|
|
2468
|
+
role: "leader",
|
|
2469
|
+
issues,
|
|
2470
|
+
details,
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
return {
|
|
2474
|
+
guildId: this.guildId,
|
|
2475
|
+
healthy: true,
|
|
2476
|
+
role: "none",
|
|
2477
|
+
issues: [],
|
|
2478
|
+
details: {},
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2112
2481
|
//#endregion
|
|
2113
2482
|
//#region Getters
|
|
2114
2483
|
/**
|
|
@@ -2189,8 +2558,45 @@ class Player extends events_1.EventEmitter {
|
|
|
2189
2558
|
return this.queue.relatedTracks();
|
|
2190
2559
|
}
|
|
2191
2560
|
get isLive() {
|
|
2561
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD)
|
|
2562
|
+
return true; //forward Mode -> live from Leader
|
|
2192
2563
|
return this.currentTrack?.isLive === true;
|
|
2193
2564
|
}
|
|
2565
|
+
get isPlaying() {
|
|
2566
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
2567
|
+
if (!this.forwardLeader || this.forwardLeader.destroyed) {
|
|
2568
|
+
this.unsubscribeForward("Leader destroyed");
|
|
2569
|
+
return false;
|
|
2570
|
+
}
|
|
2571
|
+
return this.forwardLeader.isPlaying;
|
|
2572
|
+
}
|
|
2573
|
+
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
2574
|
+
return !!this.queue.currentTrack; // driven by queue state, not audioPlayer
|
|
2575
|
+
}
|
|
2576
|
+
return (this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Playing || this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Buffering);
|
|
2577
|
+
}
|
|
2578
|
+
get isPaused() {
|
|
2579
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
2580
|
+
return this.forwardLeader?.isPaused ?? false;
|
|
2581
|
+
}
|
|
2582
|
+
if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
|
|
2583
|
+
// Extension tracks pause state via handle; Player exposes a flag
|
|
2584
|
+
return this._remotePaused;
|
|
2585
|
+
}
|
|
2586
|
+
return this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Paused;
|
|
2587
|
+
}
|
|
2588
|
+
get isIdle() {
|
|
2589
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
2590
|
+
return this.forwardLeader?.isIdle ?? false;
|
|
2591
|
+
}
|
|
2592
|
+
return this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Idle;
|
|
2593
|
+
}
|
|
2594
|
+
get isBuffering() {
|
|
2595
|
+
if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
|
|
2596
|
+
return this.forwardLeader?.isBuffering ?? false;
|
|
2597
|
+
}
|
|
2598
|
+
return this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Buffering;
|
|
2599
|
+
}
|
|
2194
2600
|
}
|
|
2195
2601
|
exports.Player = Player;
|
|
2196
2602
|
//# sourceMappingURL=Player.js.map
|