ziplayer 0.3.3 → 0.3.5

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.
Files changed (40) hide show
  1. package/AGENTS.md +717 -653
  2. package/README.md +658 -639
  3. package/dist/extensions/BaseExtension.d.ts +10 -1
  4. package/dist/extensions/BaseExtension.d.ts.map +1 -1
  5. package/dist/extensions/BaseExtension.js +27 -1
  6. package/dist/extensions/BaseExtension.js.map +1 -1
  7. package/dist/extensions/index.d.ts.map +1 -1
  8. package/dist/extensions/index.js +24 -6
  9. package/dist/extensions/index.js.map +1 -1
  10. package/dist/plugins/index.d.ts.map +1 -1
  11. package/dist/plugins/index.js +104 -50
  12. package/dist/plugins/index.js.map +1 -1
  13. package/dist/structures/Player.d.ts +74 -43
  14. package/dist/structures/Player.d.ts.map +1 -1
  15. package/dist/structures/Player.js +443 -114
  16. package/dist/structures/Player.js.map +1 -1
  17. package/dist/structures/PlayerManager.d.ts +41 -6
  18. package/dist/structures/PlayerManager.d.ts.map +1 -1
  19. package/dist/structures/PlayerManager.js +94 -125
  20. package/dist/structures/PlayerManager.js.map +1 -1
  21. package/dist/structures/StreamManager.d.ts +1 -0
  22. package/dist/structures/StreamManager.d.ts.map +1 -1
  23. package/dist/structures/StreamManager.js +1 -0
  24. package/dist/structures/StreamManager.js.map +1 -1
  25. package/dist/types/extension.d.ts +3 -0
  26. package/dist/types/extension.d.ts.map +1 -1
  27. package/dist/types/index.d.ts +38 -11
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/types/index.js +7 -0
  30. package/dist/types/index.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/extensions/BaseExtension.ts +31 -1
  33. package/src/extensions/index.ts +30 -7
  34. package/src/plugins/index.ts +136 -53
  35. package/src/structures/Player.ts +2940 -2544
  36. package/src/structures/PlayerManager.ts +916 -955
  37. package/src/structures/Queue.ts +621 -621
  38. package/src/structures/StreamManager.ts +2 -0
  39. package/src/types/extension.ts +3 -0
  40. package/src/types/index.ts +43 -11
@@ -52,12 +52,14 @@ class Player extends events_1.EventEmitter {
52
52
  super();
53
53
  this.connection = null;
54
54
  this.volume = 100;
55
- this.isPlaying = false;
56
- this.isPaused = false;
57
- this.forwardMode = false;
58
55
  this._lastActivity = Date.now();
59
- this.leaveTimeout = null;
56
+ this._remotePaused = false;
60
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;
61
63
  this.volumeInterval = null;
62
64
  this.stuckTimer = null;
63
65
  this.skipLoop = false;
@@ -105,7 +107,6 @@ class Player extends events_1.EventEmitter {
105
107
  this.loudnessMaxBoostDb = 8;
106
108
  this.loudnessMaxCutDb = 10;
107
109
  this.loudnessLimiterCeiling = 0.95;
108
- this.destroyed = false;
109
110
  this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
110
111
  this.ttsPlayer = null;
111
112
  this.lastDuration = 0;
@@ -378,6 +379,10 @@ class Player extends events_1.EventEmitter {
378
379
  * await player.play(null); // play from queue
379
380
  */
380
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
+ }
381
386
  const debugInfo = query === null ? "null"
382
387
  : typeof query === "string" ? query
383
388
  : "tracks" in query ? `${query.tracks.length} tracks`
@@ -808,10 +813,12 @@ class Player extends events_1.EventEmitter {
808
813
  await this.applyTrackMiddleware(track);
809
814
  const trackId = track.id || track.url || track.title;
810
815
  const existingStream = this.streamManager.getStreamByTrack(trackId);
816
+ this.playbackMode = types_1.PlaybackMode.NATIVE;
811
817
  if (existingStream && !existingStream.destroyed) {
812
818
  this.debug(`[Stream] Using existing stream from manager for: ${track.title}`);
813
819
  return { stream: existingStream, type: "arbitrary" };
814
820
  }
821
+ // FIRST: Try to get stream from extensions
815
822
  let stream = await this.extensionManager.provideStream(track);
816
823
  if (this.destroyed) {
817
824
  if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
@@ -819,10 +826,24 @@ class Player extends events_1.EventEmitter {
819
826
  }
820
827
  throw new Error("PLAYER_DESTROYED");
821
828
  }
829
+ // Handle remote playback - THIS SHOULD BE FIRST PRIORITY
830
+ if (stream?.remote && stream.handle) {
831
+ this.debug(`[Stream] Remote handle provided by extension for: ${track.title}`);
832
+ this.playbackMode = types_1.PlaybackMode.REMOTE;
833
+ this.preloadEnabled = false;
834
+ this.crossfadeEnabled = false;
835
+ // Clear any existing preload for remote mode
836
+ this.cancelPreload();
837
+ return stream;
838
+ }
839
+ // If extension returned a regular stream
822
840
  if (stream?.stream) {
823
841
  this.debug(`[Stream] Extension provided stream for: ${track.title}`);
842
+ this.playbackMode = types_1.PlaybackMode.NATIVE;
824
843
  return stream;
825
844
  }
845
+ // SECOND: Try plugins only if extension didn't handle it
846
+ this.debug(`[Stream] Extension didn't provide stream, trying plugins for: ${track.title}`);
826
847
  stream = await this.pluginManager.getStream(track);
827
848
  if (this.destroyed) {
828
849
  if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
@@ -837,10 +858,11 @@ class Player extends events_1.EventEmitter {
837
858
  stream.stream.destroy();
838
859
  return { stream: existingAgain, type: "arbitrary" };
839
860
  }
840
- // Register with StreamManager
841
861
  this.debug(`[Stream] Plugin provided stream for: ${track.title}`);
862
+ this.playbackMode = types_1.PlaybackMode.NATIVE;
842
863
  return stream;
843
864
  }
865
+ // Check if any plugin claims to support this track but failed
844
866
  if (!this.pluginManager.hasStreamCandidate(track)) {
845
867
  throw new Error(`UNRECOVERABLE_NO_PLUGIN:${track.title}`);
846
868
  }
@@ -857,13 +879,25 @@ class Player extends events_1.EventEmitter {
857
879
  async startTrack(track) {
858
880
  if (this.destroyed)
859
881
  return false;
882
+ // First, get stream info (this will handle remote detection)
883
+ let streamInfo = null;
884
+ try {
885
+ streamInfo = await this.getStream(track);
886
+ }
887
+ catch (error) {
888
+ this.debug(`[Player] Failed to get stream for track: ${track.title}`, error);
889
+ throw error;
890
+ }
891
+ // Handle remote playback
892
+ if (streamInfo?.remote && streamInfo.handle) {
893
+ return await this.playRemote(track, streamInfo);
894
+ }
895
+ // Handle native playback
860
896
  try {
861
897
  // Try to use preloaded resource
862
898
  if (this.preloadManager.hasValidPreload(track)) {
863
899
  this.debug(`[Player] Using preloaded stream for: ${track.title}`);
864
- // Stop current playback
865
900
  this.audioPlayer.stop(true);
866
- // Clean up old current stream (but delay to be safe)
867
901
  const oldStreamId = this.currentSlot.streamId;
868
902
  if (oldStreamId && this.streamManager) {
869
903
  setTimeout(() => {
@@ -872,29 +906,24 @@ class Player extends events_1.EventEmitter {
872
906
  }
873
907
  }, 3000);
874
908
  }
875
- // Set current slot from preload
876
909
  this.promotePreloadToCurrent(track);
877
910
  const currentResource = this.currentSlot.resource;
878
911
  if (!currentResource) {
879
912
  return false;
880
913
  }
881
914
  const targetVolume = this.getTrackTargetVolume(track);
882
- // Apply volume
883
915
  if (currentResource.volume) {
884
916
  currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
885
917
  }
886
- // Play
887
918
  await this.maybeAlignToBeatBoundary();
888
919
  this.audioPlayer.play(currentResource);
889
920
  await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
890
921
  await this.applyCrossfadeIn(currentResource, track);
891
- // Start preloading next track (async, don't await)
892
922
  this.preloadNextTrack().catch((err) => {
893
923
  this.debug(`[Player] Preload error:`, err);
894
924
  });
895
925
  return true;
896
926
  }
897
- // No valid preload, load fresh
898
927
  this.debug(`[Player] No preload available, loading fresh: ${track.title}`);
899
928
  return await this.loadFreshStream(track);
900
929
  }
@@ -904,53 +933,6 @@ class Player extends events_1.EventEmitter {
904
933
  return false;
905
934
  }
906
935
  }
907
- /**
908
- * Swap preload slot to current slot
909
- */
910
- async swapToCurrent(track) {
911
- if (!this.preloadManager.hasValidPreload(track)) {
912
- return false;
913
- }
914
- const oldStreamId = this.currentSlot.streamId;
915
- // Stop current playback
916
- this.audioPlayer.stop(true);
917
- // Clean up old current stream (but keep it for a moment)
918
- if (oldStreamId && this.streamManager) {
919
- // Delay cleanup to avoid destroying if still needed
920
- setTimeout(() => {
921
- if (this.currentSlot.streamId === oldStreamId) {
922
- this.streamManager.unregisterStream(oldStreamId, true);
923
- }
924
- }, 5000);
925
- }
926
- // Set new current
927
- this.promotePreloadToCurrent(track);
928
- const currentResource = this.currentSlot.resource;
929
- if (!currentResource) {
930
- return false;
931
- }
932
- const targetVolume = this.getTrackTargetVolume(track);
933
- // Apply volume
934
- if (currentResource.volume) {
935
- currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
936
- }
937
- // Play
938
- await this.maybeAlignToBeatBoundary();
939
- this.audioPlayer.play(currentResource);
940
- try {
941
- await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
942
- await this.applyCrossfadeIn(currentResource, track);
943
- // Start preloading next track
944
- this.preloadNextTrack().catch((err) => {
945
- this.debug(`[Player] Preload error:`, err);
946
- });
947
- return true;
948
- }
949
- catch (err) {
950
- this.debug(`[Player] Failed to play swapped track:`, err);
951
- return false;
952
- }
953
- }
954
936
  /**
955
937
  * Load fresh stream when no preload available
956
938
  */
@@ -961,6 +943,10 @@ class Player extends events_1.EventEmitter {
961
943
  await this.safeCancelPreload();
962
944
  try {
963
945
  const streamInfo = await this.getStream(track);
946
+ // Handle remote playback
947
+ if (streamInfo?.remote && streamInfo.handle) {
948
+ return await this.playRemote(track, streamInfo);
949
+ }
964
950
  if (!streamInfo?.stream) {
965
951
  throw new Error(`No stream available`);
966
952
  }
@@ -968,6 +954,7 @@ class Player extends events_1.EventEmitter {
968
954
  const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
969
955
  source: track.source || "stream",
970
956
  isPreload: false,
957
+ isRemote: !!streamInfo?.remote,
971
958
  priority: 10,
972
959
  });
973
960
  // Create resource
@@ -1013,8 +1000,6 @@ class Player extends events_1.EventEmitter {
1013
1000
  if (this.destroyed)
1014
1001
  return false;
1015
1002
  this.debug("[Player] playNext called");
1016
- // Don't cancel preload here unless absolutely necessary
1017
- // Let startTrack handle it
1018
1003
  while (true) {
1019
1004
  const track = this.queue.next(this.skipLoop);
1020
1005
  this.skipLoop = false;
@@ -1035,7 +1020,6 @@ class Player extends events_1.EventEmitter {
1035
1020
  }
1036
1021
  }
1037
1022
  this.debug(`[Player] No next track in queue`);
1038
- this.isPlaying = false;
1039
1023
  this.emit("queueEnd");
1040
1024
  // Clean up both slots when queue is empty
1041
1025
  this.clearSlot(this.currentSlot);
@@ -1054,6 +1038,11 @@ class Player extends events_1.EventEmitter {
1054
1038
  this.antiStuckConsecutiveFailures = 0;
1055
1039
  return true;
1056
1040
  }
1041
+ // For remote playback, if startTrack returns false, it's a failure
1042
+ if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
1043
+ this.debug(`[Player] Remote track failed to start: ${track.title}`);
1044
+ continue; // Skip to next track
1045
+ }
1057
1046
  const recovered = await this.attemptTrackRecovery(track, new Error("TRACK_START_RETURNED_FALSE"));
1058
1047
  if (recovered) {
1059
1048
  return true;
@@ -1068,6 +1057,11 @@ class Player extends events_1.EventEmitter {
1068
1057
  catch (err) {
1069
1058
  this.debug(`[Player] playNext error:`, err);
1070
1059
  this.emit("playerError", err, track);
1060
+ // For remote playback, just skip to next track
1061
+ if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
1062
+ this.debug(`[Player] Remote track error, skipping: ${track.title}`);
1063
+ continue;
1064
+ }
1071
1065
  if (this.isUnrecoverableStreamError(err)) {
1072
1066
  this.debug(`[Player] Skipping unrecoverable track (no plugin): ${track.title}`);
1073
1067
  continue;
@@ -1086,6 +1080,27 @@ class Player extends events_1.EventEmitter {
1086
1080
  }
1087
1081
  }
1088
1082
  }
1083
+ async playRemote(track, stream) {
1084
+ if (!stream.handle)
1085
+ return false;
1086
+ try {
1087
+ // Store the remote handle for later use
1088
+ this.remoteHandle = stream.handle;
1089
+ // Set current track before playing
1090
+ this.queue.setCurrentTrack(track);
1091
+ // Emit track start event before playing (so UI updates)
1092
+ this.emit("trackStart", track);
1093
+ // Start playback via remote handle
1094
+ await stream.handle.play();
1095
+ this.debug(`[Player] Remote playback started for: ${track.title}`);
1096
+ return true;
1097
+ }
1098
+ catch (error) {
1099
+ this.debug(`[Player] Remote playback error:`, error);
1100
+ this.emit("playerError", error, track);
1101
+ return false;
1102
+ }
1103
+ }
1089
1104
  //#endregion
1090
1105
  //#region TTS
1091
1106
  ensureTTSPlayer() {
@@ -1237,51 +1252,142 @@ class Player extends events_1.EventEmitter {
1237
1252
  }
1238
1253
  }
1239
1254
  /**
1240
- * Subscribe this player's voice connection
1241
- * to another player's audio stream.
1255
+ * Subscribe this player to another player's playback stream.
1256
+ *
1257
+ * This enables "forward mode", where the follower player directly subscribes
1258
+ * to the leader player's {@link audioPlayer} instead of creating its own stream.
1242
1259
  *
1243
- * This is primarily used for:
1244
- * - playback mirroring
1245
- * - radio/broadcast systems
1246
- * - multi-guild synchronized playback
1247
- * - forwardMode shared streaming
1260
+ * Greatly reduces CPU, bandwidth, and extractor usage because only the leader
1261
+ * creates and decodes the audio resource.
1248
1262
  *
1249
- * Instead of creating a separate audio stream,
1250
- * this player will directly receive audio packets
1251
- * from the target player's AudioPlayer instance.
1263
+ * ## Features
1264
+ * - Real-time shared playback
1265
+ * - Followers may join at any time
1266
+ * - Automatic track synchronization
1267
+ * - Optional volume synchronization
1268
+ * - Automatic cleanup on destroy
1269
+ * - Supports unlimited followers
1252
1270
  *
1253
- * Benefits:
1254
- * - drastically lower CPU usage
1255
- * - only one ffmpeg/extractor stream
1256
- * - lower bandwidth and memory usage
1257
- * - perfect sync across guilds
1271
+ * ## Lifecycle
1272
+ * - When the leader starts a track, followers automatically receive the same track metadata.
1273
+ * - When the leader pauses/resumes/stops, followers are synchronized.
1274
+ * - Destroying the leader automatically unsubscribes all followers.
1275
+ * - Destroying a follower only removes that follower.
1258
1276
  *
1259
- * Important:
1260
- * - both players must already have active voice connections
1261
- * - this does NOT transfer queue ownership
1262
- * - this does NOT clone playback state automatically
1277
+ * ## Notes
1278
+ * - Both players must already be connected to voice.
1279
+ * - A player cannot subscribe to itself.
1280
+ * - Existing playback subscriptions are automatically replaced.
1263
1281
  *
1264
- * @param {Player} player - Source player to subscribe to
1282
+ * @param {Player} leader The leader player to subscribe to.
1283
+ * @param options Additional playback mirror options.
1284
+ * @param options.syncVolume When true, follower volume automatically follows the leader. Default: true.
1265
1285
  *
1266
- * @returns {boolean}
1267
- * Returns true if subscription succeeded,
1268
- * otherwise false.
1286
+ * @returns {boolean} True if subscription succeeded.
1269
1287
  *
1270
1288
  * @example
1271
1289
  * follower.subscribeTo(leader);
1272
1290
  *
1273
1291
  * @example
1274
- * if (!player.subscribeTo(leader)) {
1275
- * console.log("Failed to subscribe");
1276
- * }
1292
+ * follower.subscribeTo(leader, {
1293
+ * syncVolume: true,
1294
+ * });
1277
1295
  */
1278
- subscribeTo(player) {
1279
- if (!this.connection)
1296
+ subscribeTo(leader, options) {
1297
+ if (!leader)
1298
+ return false;
1299
+ if (leader === this) {
1300
+ this.debug(`[Player] Cannot subscribe to self`);
1280
1301
  return false;
1281
- this.connection.subscribe(player.audioPlayer);
1282
- this.isPlaying = player.isPlaying;
1283
- this.isPaused = player.isPaused;
1284
- this.forwardMode = true;
1302
+ }
1303
+ if (leader.destroyed) {
1304
+ this.debug("[Player] Cannot subscribe to destroyed leader");
1305
+ return false;
1306
+ }
1307
+ if (this.destroyed) {
1308
+ this.debug("[Player] Cannot subscribe destroyed player");
1309
+ return false;
1310
+ }
1311
+ if (!!leader.forwardLeader) {
1312
+ this.debug("[Player] Cannot subscribe to follower player");
1313
+ return false;
1314
+ }
1315
+ if (!this.connection || !leader.connection) {
1316
+ this.debug(`[Player] Missing connection for subscribeTo`);
1317
+ return false;
1318
+ }
1319
+ // cleanup old leader
1320
+ if (this.forwardLeader) {
1321
+ this.unsubscribeForward("This Player new subscribeTo " + leader.guildId);
1322
+ }
1323
+ this.forwardLeader = leader;
1324
+ leader.forwardFollowers.add(this);
1325
+ try {
1326
+ // clear local playback
1327
+ this.stop();
1328
+ // detach current followers first
1329
+ for (const fp of [...this.forwardFollowers]) {
1330
+ try {
1331
+ fp.unsubscribeForward("Leader new subscribeTo " + leader.guildId);
1332
+ }
1333
+ catch { }
1334
+ }
1335
+ this.forwardFollowers.clear();
1336
+ this.queue.clear();
1337
+ if (leader.currentTrack) {
1338
+ this.queue.setCurrentTrack(leader.currentTrack);
1339
+ }
1340
+ if (options?.forwardMode ?? true)
1341
+ this.playbackMode = types_1.PlaybackMode.FORWARD;
1342
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD && this.connection) {
1343
+ this.connection.subscribe(leader.audioPlayer);
1344
+ }
1345
+ this.volume = leader.volume;
1346
+ this.emit("forwardModeStart", leader);
1347
+ this.debug(`[Player] Forward mode subscribed ${this.guildId} -> ${leader.guildId}`);
1348
+ return true;
1349
+ }
1350
+ catch (e) {
1351
+ this.debug(`[Player] subscribeTo error:`, e);
1352
+ this.forwardLeader = null;
1353
+ leader.forwardFollowers.delete(this);
1354
+ return false;
1355
+ }
1356
+ }
1357
+ /**
1358
+ * Unsubscribe this player from its current playback leader.
1359
+ *
1360
+ * This disables forward mode and restores the player's own audioPlayer
1361
+ * subscription back to its voice connection.
1362
+ *
1363
+ * Automatically emitted when:
1364
+ * - The leader player is destroyed
1365
+ * - This player is destroyed
1366
+ * - A new leader subscription replaces the old one
1367
+ *
1368
+ * Emits:
1369
+ * - `forwardModeEnd`
1370
+ *
1371
+ * @returns {boolean} True if a playback subscription existed and was removed.
1372
+ *
1373
+ * @example
1374
+ * follower.unsubscribeForward();
1375
+ */
1376
+ unsubscribeForward(reason) {
1377
+ if (!this.forwardLeader) {
1378
+ return false;
1379
+ }
1380
+ const leader = this.forwardLeader;
1381
+ leader.forwardFollowers.delete(this);
1382
+ this.forwardLeader = null;
1383
+ this.playbackMode = types_1.PlaybackMode.NATIVE;
1384
+ try {
1385
+ this.connection?.subscribe(this.audioPlayer);
1386
+ }
1387
+ catch { }
1388
+ this.queue.clear();
1389
+ this.emit("forwardModeEnd", leader, reason);
1390
+ this.debug(`[Player] Forward mode unsubscribed ${this.guildId} <- ${leader.guildId}: ${reason ?? null}`);
1285
1391
  return true;
1286
1392
  }
1287
1393
  /**
@@ -1293,10 +1399,22 @@ class Player extends events_1.EventEmitter {
1293
1399
  * console.log(`Paused: ${paused}`);
1294
1400
  */
1295
1401
  pause() {
1402
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
1403
+ this.debug("[Player] Cannot pause while subscribed to another player");
1404
+ return false;
1405
+ }
1296
1406
  this.debug(`[Player] pause called`);
1297
- if (this.isPlaying && !this.isPaused) {
1298
- return this.audioPlayer.pause();
1407
+ if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
1408
+ if (!this.remoteHandle)
1409
+ return false;
1410
+ void this.remoteHandle.pause().catch((e) => this.debug("[Player] Remote pause:", e));
1411
+ const track = this.queue.currentTrack;
1412
+ if (track)
1413
+ this.emit("playerPause", track);
1414
+ return true;
1299
1415
  }
1416
+ if (this.isPlaying && !this.isPaused)
1417
+ return this.audioPlayer.pause();
1300
1418
  return false;
1301
1419
  }
1302
1420
  /**
@@ -1308,7 +1426,20 @@ class Player extends events_1.EventEmitter {
1308
1426
  * console.log(`Resumed: ${resumed}`);
1309
1427
  */
1310
1428
  resume() {
1429
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
1430
+ this.debug("[Player] Cannot resume while subscribed to another player");
1431
+ return false;
1432
+ }
1311
1433
  this.debug(`[Player] resume called`);
1434
+ if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
1435
+ if (!this.remoteHandle)
1436
+ return false;
1437
+ void this.remoteHandle.resume().catch((e) => this.debug("[Player] Remote resume:", e));
1438
+ const track = this.queue.currentTrack;
1439
+ if (track)
1440
+ this.emit("playerResume", track);
1441
+ return true;
1442
+ }
1312
1443
  if (this.isPaused) {
1313
1444
  const result = this.audioPlayer.unpause();
1314
1445
  if (result) {
@@ -1331,16 +1462,33 @@ class Player extends events_1.EventEmitter {
1331
1462
  * console.log(`Stopped: ${stopped}`);
1332
1463
  */
1333
1464
  stop() {
1465
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
1466
+ this.debug("[Player] Cannot stop while subscribed to another player");
1467
+ return false;
1468
+ }
1334
1469
  this.debug(`[Player] stop called`);
1470
+ if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
1471
+ this.cancelPreload();
1472
+ this.queue.clear();
1473
+ void this.remoteHandle?.stop().catch((e) => this.debug("[Player] Remote stop:", e));
1474
+ this.emit("playerStop");
1475
+ return true;
1476
+ }
1335
1477
  // Cancel preload when stopping
1336
1478
  this.cancelPreload();
1337
1479
  this.queue.clear();
1338
1480
  const result = this.audioPlayer.stop();
1339
1481
  this.destroyCurrentStream();
1340
1482
  this.currentResource = null;
1341
- this.isPlaying = false;
1342
- this.isPaused = false;
1343
1483
  this.emit("playerStop");
1484
+ for (const fp of this.forwardFollowers) {
1485
+ try {
1486
+ fp.connection?.subscribe(fp.audioPlayer);
1487
+ fp.audioPlayer.stop(true);
1488
+ fp.emit("playerStop");
1489
+ }
1490
+ catch { }
1491
+ }
1344
1492
  return result;
1345
1493
  }
1346
1494
  /**
@@ -1357,7 +1505,17 @@ class Player extends events_1.EventEmitter {
1357
1505
  * await player.seek(90000);
1358
1506
  */
1359
1507
  async seek(position) {
1508
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
1509
+ this.debug("[Player] Cannot seek while subscribed to another player");
1510
+ return false;
1511
+ }
1360
1512
  this.debug(`[Player] seek called with position: ${position}ms`);
1513
+ if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
1514
+ if (!this.remoteHandle)
1515
+ return false;
1516
+ await this.remoteHandle.seek(position);
1517
+ return true;
1518
+ }
1361
1519
  const track = this.queue.currentTrack;
1362
1520
  if (!track) {
1363
1521
  this.debug(`[Player] No current track to seek`);
@@ -1382,7 +1540,21 @@ class Player extends events_1.EventEmitter {
1382
1540
  * console.log(`Skipped: ${skipped}`);
1383
1541
  */
1384
1542
  skip(index) {
1543
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
1544
+ this.debug("[Player] Cannot skip while subscribed to another player");
1545
+ return false;
1546
+ }
1385
1547
  this.debug(`[Player] skip called with index: ${index}`);
1548
+ if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
1549
+ if (typeof index === "number" && index >= 0) {
1550
+ for (let i = 0; i < index; i++)
1551
+ this.queue.remove(0);
1552
+ }
1553
+ // signal the remote backend to stop;
1554
+ void this.remoteHandle?.stop().catch((e) => this.debug("[Player] Remote skip:", e));
1555
+ this.playNext();
1556
+ return true;
1557
+ }
1386
1558
  try {
1387
1559
  if (typeof index === "number" && index >= 0) {
1388
1560
  const targetTrack = this.queue.getTrack(index);
@@ -1419,6 +1591,10 @@ class Player extends events_1.EventEmitter {
1419
1591
  * console.log(`Previous: ${previous}`);
1420
1592
  */
1421
1593
  async previous() {
1594
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
1595
+ this.debug("[Player] Cannot previous while subscribed to another player");
1596
+ return false;
1597
+ }
1422
1598
  this.debug(`[Player] previous called`);
1423
1599
  const track = this.queue.previous();
1424
1600
  if (!track)
@@ -1548,6 +1724,12 @@ class Player extends events_1.EventEmitter {
1548
1724
  * console.log(`Auto-play mode: ${autoPlayMode}`);
1549
1725
  */
1550
1726
  autoPlay(mode) {
1727
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
1728
+ if (!mode)
1729
+ return this.forwardLeader?.autoPlay() ?? false;
1730
+ this.debug("[Player] Cannot autoPlay while subscribed to another player");
1731
+ return false;
1732
+ }
1551
1733
  return this.queue.autoPlay(mode);
1552
1734
  }
1553
1735
  /**
@@ -1560,11 +1742,20 @@ class Player extends events_1.EventEmitter {
1560
1742
  * console.log(`Volume set: ${volumeSet}`);
1561
1743
  */
1562
1744
  setVolume(volume) {
1745
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
1746
+ this.debug("[Player] Cannot setVolume while subscribed to another player");
1747
+ return false;
1748
+ }
1563
1749
  this.debug(`[Player] setVolume called: ${volume}`);
1564
1750
  if (volume < 0 || volume > 200)
1565
1751
  return false;
1566
1752
  const oldVolume = this.volume;
1567
1753
  this.volume = volume;
1754
+ if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
1755
+ void this.remoteHandle?.setVolume(volume).catch((e) => this.debug("[Player] Remote volume:", e));
1756
+ this.emit("volumeChange", oldVolume, volume);
1757
+ return true;
1758
+ }
1568
1759
  const resourceVolume = this.currentResource?.volume;
1569
1760
  if (resourceVolume) {
1570
1761
  if (this.volumeInterval)
@@ -1584,6 +1775,12 @@ class Player extends events_1.EventEmitter {
1584
1775
  }, 300);
1585
1776
  }
1586
1777
  this.emit("volumeChange", oldVolume, volume);
1778
+ for (const fp of this.forwardFollowers) {
1779
+ try {
1780
+ fp.volume = volume;
1781
+ }
1782
+ catch { }
1783
+ }
1587
1784
  return true;
1588
1785
  }
1589
1786
  /**
@@ -1624,6 +1821,10 @@ class Player extends events_1.EventEmitter {
1624
1821
  */
1625
1822
  async insert(query, index, requestedBy) {
1626
1823
  try {
1824
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
1825
+ this.debug("[Player] Cannot insert while subscribed to another player");
1826
+ return false;
1827
+ }
1627
1828
  this.debug(`[Player] insert called at index ${index} with type: ${typeof query}`);
1628
1829
  let tracksToAdd = [];
1629
1830
  let isPlaylist = false;
@@ -1834,6 +2035,10 @@ class Player extends events_1.EventEmitter {
1834
2035
  if (this.destroyed)
1835
2036
  return;
1836
2037
  this.destroyed = true;
2038
+ if (this.remoteHandle?.destroy) {
2039
+ this.remoteHandle.destroy().catch(() => { });
2040
+ this.remoteHandle = undefined;
2041
+ }
1837
2042
  if (this.leaveTimeout) {
1838
2043
  clearTimeout(this.leaveTimeout);
1839
2044
  this.leaveTimeout = null;
@@ -1861,14 +2066,21 @@ class Player extends events_1.EventEmitter {
1861
2066
  this.pluginManager.clear();
1862
2067
  this.filter.destroy();
1863
2068
  this.extensionManager.destroy();
1864
- this.isPlaying = false;
1865
- this.isPaused = false;
1866
2069
  // Clear any remaining intervals
1867
2070
  if (this.volumeInterval) {
1868
2071
  clearInterval(this.volumeInterval);
1869
2072
  this.volumeInterval = null;
1870
2073
  }
1871
2074
  this.emit("playerDestroy");
2075
+ this.unsubscribeForward("Player destroy");
2076
+ // release followers
2077
+ for (const fp of [...this.forwardFollowers]) {
2078
+ try {
2079
+ fp.unsubscribeForward("Leader destroy");
2080
+ }
2081
+ catch { }
2082
+ }
2083
+ this.forwardFollowers.clear();
1872
2084
  this.removeAllListeners();
1873
2085
  }
1874
2086
  //#endregion
@@ -1940,13 +2152,7 @@ class Player extends events_1.EventEmitter {
1940
2152
  this.audioPlayer.play(resource);
1941
2153
  }
1942
2154
  // Restore playing state
1943
- if (wasPlaying && !wasPaused) {
1944
- this.isPlaying = true;
1945
- this.isPaused = false;
1946
- }
1947
- else if (wasPaused) {
1948
- this.isPlaying = false;
1949
- this.isPaused = true;
2155
+ if (wasPaused) {
1950
2156
  this.audioPlayer.pause();
1951
2157
  }
1952
2158
  this.debug(`[Player] Successfully applied filter to current track at position ${currentPosition}ms`);
@@ -2012,6 +2218,9 @@ class Player extends events_1.EventEmitter {
2012
2218
  if (track) {
2013
2219
  this.debug(`[Player] Track ended: ${track.title}`);
2014
2220
  this.emit("trackEnd", track);
2221
+ for (const fp of this.forwardFollowers) {
2222
+ fp.emit("trackEnd", track);
2223
+ }
2015
2224
  }
2016
2225
  void this.playNext();
2017
2226
  }
@@ -2019,30 +2228,41 @@ class Player extends events_1.EventEmitter {
2019
2228
  (oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) {
2020
2229
  // Track started
2021
2230
  this.clearLeaveTimeout();
2022
- this.isPlaying = true;
2023
- this.isPaused = false;
2024
2231
  const track = this.queue.currentTrack;
2025
2232
  if (track) {
2026
2233
  this.debug(`[Player] Track started: ${track.title}`);
2027
2234
  this.emit("trackStart", track);
2235
+ for (const fp of this.forwardFollowers) {
2236
+ try {
2237
+ fp.queue.clear();
2238
+ fp.connection?.subscribe(this.audioPlayer);
2239
+ fp.queue.setCurrentTrack(track);
2240
+ fp.emit("trackStart", track);
2241
+ }
2242
+ catch (e) {
2243
+ this.debug(`[Player] Failed to sync follower ${fp.guildId}:`, e);
2244
+ }
2245
+ }
2028
2246
  }
2029
2247
  }
2030
2248
  else if (newState.status === voice_1.AudioPlayerStatus.Paused && oldState.status !== voice_1.AudioPlayerStatus.Paused) {
2031
- // Track paused
2032
- this.isPaused = true;
2033
2249
  const track = this.queue.currentTrack;
2034
2250
  if (track) {
2035
2251
  this.debug(`[Player] Player paused on track: ${track.title}`);
2036
2252
  this.emit("playerPause", track);
2253
+ for (const fp of this.forwardFollowers) {
2254
+ fp.emit("playerPause", track);
2255
+ }
2037
2256
  }
2038
2257
  }
2039
2258
  else if (newState.status !== voice_1.AudioPlayerStatus.Paused && oldState.status === voice_1.AudioPlayerStatus.Paused) {
2040
- // Track resumed
2041
- this.isPaused = false;
2042
2259
  const track = this.queue.currentTrack;
2043
2260
  if (track) {
2044
2261
  this.debug(`[Player] Player resumed on track: ${track.title}`);
2045
2262
  this.emit("playerResume", track);
2263
+ for (const fp of this.forwardFollowers) {
2264
+ fp.emit("playerResume", track);
2265
+ }
2046
2266
  }
2047
2267
  }
2048
2268
  else if (newState.status === voice_1.AudioPlayerStatus.AutoPaused) {
@@ -2136,6 +2356,25 @@ class Player extends events_1.EventEmitter {
2136
2356
  plugins: this.pluginManager.getAll().map((plugin) => plugin.name),
2137
2357
  };
2138
2358
  }
2359
+ exitRemoteMode() {
2360
+ if (this.playbackMode !== types_1.PlaybackMode.REMOTE)
2361
+ return;
2362
+ this.debug("[Player] Exiting REMOTE mode, restoring native playback");
2363
+ void this.remoteHandle?.destroy().catch(() => { });
2364
+ this.remoteHandle = undefined;
2365
+ this.playbackMode = types_1.PlaybackMode.NATIVE;
2366
+ this._remotePaused = false;
2367
+ // Restore preload/crossfade from original options
2368
+ const preloadOptions = this.options.preload ?? {};
2369
+ const autoDisable = preloadOptions.autoDisableInLowPerformance ?? true;
2370
+ this.preloadEnabled = (preloadOptions.enabled ?? true) && !(this.lowPerformanceMode && autoDisable);
2371
+ const crossfadeOptions = this.options.crossfade ?? {};
2372
+ const cfAutoDisable = crossfadeOptions.autoDisableInLowPerformance ?? true;
2373
+ this.crossfadeEnabled =
2374
+ typeof crossfadeOptions.enabled === "boolean" ? crossfadeOptions.enabled : (crossfadeOptions.autoEnable ?? true);
2375
+ if (this.lowPerformanceMode && cfAutoDisable)
2376
+ this.crossfadeEnabled = false;
2377
+ }
2139
2378
  /**
2140
2379
  * Get serializable state (for manual persistence)
2141
2380
  */
@@ -2189,6 +2428,59 @@ class Player extends events_1.EventEmitter {
2189
2428
  totalStreams: this.streamManager.getStreamCount(),
2190
2429
  };
2191
2430
  }
2431
+ getForwardHealthStatus() {
2432
+ const issues = [];
2433
+ const details = {};
2434
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD && this.forwardLeader) {
2435
+ // This player is a follower
2436
+ details.leaderId = this.forwardLeader.guildId;
2437
+ details.connectionState = this.connection?.state.status;
2438
+ details.audioPlayerState = this.audioPlayer.state.status;
2439
+ if (this.forwardLeader.destroyed) {
2440
+ issues.push("Leader is destroyed");
2441
+ }
2442
+ if (!this.forwardLeader.connection) {
2443
+ issues.push("Leader has no connection");
2444
+ }
2445
+ if (this.forwardLeader.destroyed || !this.forwardLeader.connection) {
2446
+ issues.push("Leader is unavailable");
2447
+ }
2448
+ return {
2449
+ guildId: this.guildId,
2450
+ healthy: issues.length === 0,
2451
+ role: "follower",
2452
+ issues,
2453
+ details,
2454
+ };
2455
+ }
2456
+ else if (this.forwardFollowers.size > 0) {
2457
+ // This player is a leader
2458
+ details.followerCount = this.forwardFollowers.size;
2459
+ details.connectionState = this.connection?.state.status;
2460
+ const deadFollowers = [];
2461
+ for (const follower of this.forwardFollowers) {
2462
+ if (follower.destroyed)
2463
+ deadFollowers.push(follower.guildId);
2464
+ }
2465
+ if (deadFollowers.length > 0) {
2466
+ issues.push(`Has ${deadFollowers.length} dead followers: ${deadFollowers.join(", ")}`);
2467
+ }
2468
+ return {
2469
+ guildId: this.guildId,
2470
+ healthy: true, // Leader being healthy doesn't depend on followers
2471
+ role: "leader",
2472
+ issues,
2473
+ details,
2474
+ };
2475
+ }
2476
+ return {
2477
+ guildId: this.guildId,
2478
+ healthy: true,
2479
+ role: "none",
2480
+ issues: [],
2481
+ details: {},
2482
+ };
2483
+ }
2192
2484
  //#endregion
2193
2485
  //#region Getters
2194
2486
  /**
@@ -2269,8 +2561,45 @@ class Player extends events_1.EventEmitter {
2269
2561
  return this.queue.relatedTracks();
2270
2562
  }
2271
2563
  get isLive() {
2564
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD)
2565
+ return true; //forward Mode -> live from Leader
2272
2566
  return this.currentTrack?.isLive === true;
2273
2567
  }
2568
+ get isPlaying() {
2569
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
2570
+ if (!this.forwardLeader || this.forwardLeader.destroyed) {
2571
+ this.unsubscribeForward("Leader destroyed");
2572
+ return false;
2573
+ }
2574
+ return this.forwardLeader.isPlaying;
2575
+ }
2576
+ if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
2577
+ return !!this.queue.currentTrack; // driven by queue state, not audioPlayer
2578
+ }
2579
+ return (this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Playing || this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Buffering);
2580
+ }
2581
+ get isPaused() {
2582
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
2583
+ return this.forwardLeader?.isPaused ?? false;
2584
+ }
2585
+ if (this.playbackMode === types_1.PlaybackMode.REMOTE) {
2586
+ // Extension tracks pause state via handle; Player exposes a flag
2587
+ return this._remotePaused;
2588
+ }
2589
+ return this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Paused;
2590
+ }
2591
+ get isIdle() {
2592
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
2593
+ return this.forwardLeader?.isIdle ?? false;
2594
+ }
2595
+ return this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Idle;
2596
+ }
2597
+ get isBuffering() {
2598
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD) {
2599
+ return this.forwardLeader?.isBuffering ?? false;
2600
+ }
2601
+ return this.audioPlayer.state.status === voice_1.AudioPlayerStatus.Buffering;
2602
+ }
2274
2603
  }
2275
2604
  exports.Player = Player;
2276
2605
  //# sourceMappingURL=Player.js.map