ziplayer 0.3.3 → 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.
Files changed (35) 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 +440 -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/types/extension.d.ts +3 -0
  22. package/dist/types/extension.d.ts.map +1 -1
  23. package/dist/types/index.d.ts +38 -11
  24. package/dist/types/index.d.ts.map +1 -1
  25. package/dist/types/index.js +7 -0
  26. package/dist/types/index.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/extensions/BaseExtension.ts +31 -1
  29. package/src/extensions/index.ts +30 -7
  30. package/src/plugins/index.ts +136 -53
  31. package/src/structures/Player.ts +2937 -2544
  32. package/src/structures/PlayerManager.ts +916 -955
  33. package/src/structures/Queue.ts +621 -621
  34. package/src/types/extension.ts +3 -0
  35. 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`
@@ -812,6 +817,7 @@ class Player extends events_1.EventEmitter {
812
817
  this.debug(`[Stream] Using existing stream from manager for: ${track.title}`);
813
818
  return { stream: existingStream, type: "arbitrary" };
814
819
  }
820
+ // FIRST: Try to get stream from extensions
815
821
  let stream = await this.extensionManager.provideStream(track);
816
822
  if (this.destroyed) {
817
823
  if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
@@ -819,10 +825,24 @@ class Player extends events_1.EventEmitter {
819
825
  }
820
826
  throw new Error("PLAYER_DESTROYED");
821
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
822
839
  if (stream?.stream) {
823
840
  this.debug(`[Stream] Extension provided stream for: ${track.title}`);
841
+ this.playbackMode = types_1.PlaybackMode.NATIVE;
824
842
  return stream;
825
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}`);
826
846
  stream = await this.pluginManager.getStream(track);
827
847
  if (this.destroyed) {
828
848
  if (stream?.stream && typeof stream.stream.destroy === "function" && !stream.stream.destroyed) {
@@ -837,10 +857,11 @@ class Player extends events_1.EventEmitter {
837
857
  stream.stream.destroy();
838
858
  return { stream: existingAgain, type: "arbitrary" };
839
859
  }
840
- // Register with StreamManager
841
860
  this.debug(`[Stream] Plugin provided stream for: ${track.title}`);
861
+ this.playbackMode = types_1.PlaybackMode.NATIVE;
842
862
  return stream;
843
863
  }
864
+ // Check if any plugin claims to support this track but failed
844
865
  if (!this.pluginManager.hasStreamCandidate(track)) {
845
866
  throw new Error(`UNRECOVERABLE_NO_PLUGIN:${track.title}`);
846
867
  }
@@ -857,13 +878,25 @@ class Player extends events_1.EventEmitter {
857
878
  async startTrack(track) {
858
879
  if (this.destroyed)
859
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
860
895
  try {
861
896
  // Try to use preloaded resource
862
897
  if (this.preloadManager.hasValidPreload(track)) {
863
898
  this.debug(`[Player] Using preloaded stream for: ${track.title}`);
864
- // Stop current playback
865
899
  this.audioPlayer.stop(true);
866
- // Clean up old current stream (but delay to be safe)
867
900
  const oldStreamId = this.currentSlot.streamId;
868
901
  if (oldStreamId && this.streamManager) {
869
902
  setTimeout(() => {
@@ -872,29 +905,24 @@ class Player extends events_1.EventEmitter {
872
905
  }
873
906
  }, 3000);
874
907
  }
875
- // Set current slot from preload
876
908
  this.promotePreloadToCurrent(track);
877
909
  const currentResource = this.currentSlot.resource;
878
910
  if (!currentResource) {
879
911
  return false;
880
912
  }
881
913
  const targetVolume = this.getTrackTargetVolume(track);
882
- // Apply volume
883
914
  if (currentResource.volume) {
884
915
  currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
885
916
  }
886
- // Play
887
917
  await this.maybeAlignToBeatBoundary();
888
918
  this.audioPlayer.play(currentResource);
889
919
  await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
890
920
  await this.applyCrossfadeIn(currentResource, track);
891
- // Start preloading next track (async, don't await)
892
921
  this.preloadNextTrack().catch((err) => {
893
922
  this.debug(`[Player] Preload error:`, err);
894
923
  });
895
924
  return true;
896
925
  }
897
- // No valid preload, load fresh
898
926
  this.debug(`[Player] No preload available, loading fresh: ${track.title}`);
899
927
  return await this.loadFreshStream(track);
900
928
  }
@@ -904,53 +932,6 @@ class Player extends events_1.EventEmitter {
904
932
  return false;
905
933
  }
906
934
  }
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
935
  /**
955
936
  * Load fresh stream when no preload available
956
937
  */
@@ -961,6 +942,10 @@ class Player extends events_1.EventEmitter {
961
942
  await this.safeCancelPreload();
962
943
  try {
963
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
+ }
964
949
  if (!streamInfo?.stream) {
965
950
  throw new Error(`No stream available`);
966
951
  }
@@ -1013,8 +998,6 @@ class Player extends events_1.EventEmitter {
1013
998
  if (this.destroyed)
1014
999
  return false;
1015
1000
  this.debug("[Player] playNext called");
1016
- // Don't cancel preload here unless absolutely necessary
1017
- // Let startTrack handle it
1018
1001
  while (true) {
1019
1002
  const track = this.queue.next(this.skipLoop);
1020
1003
  this.skipLoop = false;
@@ -1035,7 +1018,6 @@ class Player extends events_1.EventEmitter {
1035
1018
  }
1036
1019
  }
1037
1020
  this.debug(`[Player] No next track in queue`);
1038
- this.isPlaying = false;
1039
1021
  this.emit("queueEnd");
1040
1022
  // Clean up both slots when queue is empty
1041
1023
  this.clearSlot(this.currentSlot);
@@ -1054,6 +1036,11 @@ class Player extends events_1.EventEmitter {
1054
1036
  this.antiStuckConsecutiveFailures = 0;
1055
1037
  return true;
1056
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
+ }
1057
1044
  const recovered = await this.attemptTrackRecovery(track, new Error("TRACK_START_RETURNED_FALSE"));
1058
1045
  if (recovered) {
1059
1046
  return true;
@@ -1068,6 +1055,11 @@ class Player extends events_1.EventEmitter {
1068
1055
  catch (err) {
1069
1056
  this.debug(`[Player] playNext error:`, err);
1070
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
+ }
1071
1063
  if (this.isUnrecoverableStreamError(err)) {
1072
1064
  this.debug(`[Player] Skipping unrecoverable track (no plugin): ${track.title}`);
1073
1065
  continue;
@@ -1086,6 +1078,27 @@ class Player extends events_1.EventEmitter {
1086
1078
  }
1087
1079
  }
1088
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
+ }
1089
1102
  //#endregion
1090
1103
  //#region TTS
1091
1104
  ensureTTSPlayer() {
@@ -1237,51 +1250,142 @@ class Player extends events_1.EventEmitter {
1237
1250
  }
1238
1251
  }
1239
1252
  /**
1240
- * Subscribe this player's voice connection
1241
- * to another player's audio stream.
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.
1242
1257
  *
1243
- * This is primarily used for:
1244
- * - playback mirroring
1245
- * - radio/broadcast systems
1246
- * - multi-guild synchronized playback
1247
- * - forwardMode shared streaming
1258
+ * Greatly reduces CPU, bandwidth, and extractor usage because only the leader
1259
+ * creates and decodes the audio resource.
1248
1260
  *
1249
- * Instead of creating a separate audio stream,
1250
- * this player will directly receive audio packets
1251
- * from the target player's AudioPlayer instance.
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
1252
1268
  *
1253
- * Benefits:
1254
- * - drastically lower CPU usage
1255
- * - only one ffmpeg/extractor stream
1256
- * - lower bandwidth and memory usage
1257
- * - perfect sync across guilds
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.
1258
1274
  *
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
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.
1263
1279
  *
1264
- * @param {Player} player - Source player to subscribe to
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.
1265
1283
  *
1266
- * @returns {boolean}
1267
- * Returns true if subscription succeeded,
1268
- * otherwise false.
1284
+ * @returns {boolean} True if subscription succeeded.
1269
1285
  *
1270
1286
  * @example
1271
1287
  * follower.subscribeTo(leader);
1272
1288
  *
1273
1289
  * @example
1274
- * if (!player.subscribeTo(leader)) {
1275
- * console.log("Failed to subscribe");
1276
- * }
1290
+ * follower.subscribeTo(leader, {
1291
+ * syncVolume: true,
1292
+ * });
1277
1293
  */
1278
- subscribeTo(player) {
1279
- if (!this.connection)
1294
+ subscribeTo(leader, options) {
1295
+ if (!leader)
1296
+ return false;
1297
+ if (leader === this) {
1298
+ this.debug(`[Player] Cannot subscribe to self`);
1280
1299
  return false;
1281
- this.connection.subscribe(player.audioPlayer);
1282
- this.isPlaying = player.isPlaying;
1283
- this.isPaused = player.isPaused;
1284
- this.forwardMode = true;
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}`);
1285
1389
  return true;
1286
1390
  }
1287
1391
  /**
@@ -1293,10 +1397,22 @@ class Player extends events_1.EventEmitter {
1293
1397
  * console.log(`Paused: ${paused}`);
1294
1398
  */
1295
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
+ }
1296
1404
  this.debug(`[Player] pause called`);
1297
- if (this.isPlaying && !this.isPaused) {
1298
- return this.audioPlayer.pause();
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;
1299
1413
  }
1414
+ if (this.isPlaying && !this.isPaused)
1415
+ return this.audioPlayer.pause();
1300
1416
  return false;
1301
1417
  }
1302
1418
  /**
@@ -1308,7 +1424,20 @@ class Player extends events_1.EventEmitter {
1308
1424
  * console.log(`Resumed: ${resumed}`);
1309
1425
  */
1310
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
+ }
1311
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
+ }
1312
1441
  if (this.isPaused) {
1313
1442
  const result = this.audioPlayer.unpause();
1314
1443
  if (result) {
@@ -1331,16 +1460,33 @@ class Player extends events_1.EventEmitter {
1331
1460
  * console.log(`Stopped: ${stopped}`);
1332
1461
  */
1333
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
+ }
1334
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
+ }
1335
1475
  // Cancel preload when stopping
1336
1476
  this.cancelPreload();
1337
1477
  this.queue.clear();
1338
1478
  const result = this.audioPlayer.stop();
1339
1479
  this.destroyCurrentStream();
1340
1480
  this.currentResource = null;
1341
- this.isPlaying = false;
1342
- this.isPaused = false;
1343
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
+ }
1344
1490
  return result;
1345
1491
  }
1346
1492
  /**
@@ -1357,7 +1503,17 @@ class Player extends events_1.EventEmitter {
1357
1503
  * await player.seek(90000);
1358
1504
  */
1359
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
+ }
1360
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
+ }
1361
1517
  const track = this.queue.currentTrack;
1362
1518
  if (!track) {
1363
1519
  this.debug(`[Player] No current track to seek`);
@@ -1382,7 +1538,20 @@ class Player extends events_1.EventEmitter {
1382
1538
  * console.log(`Skipped: ${skipped}`);
1383
1539
  */
1384
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
+ }
1385
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
+ }
1386
1555
  try {
1387
1556
  if (typeof index === "number" && index >= 0) {
1388
1557
  const targetTrack = this.queue.getTrack(index);
@@ -1419,6 +1588,10 @@ class Player extends events_1.EventEmitter {
1419
1588
  * console.log(`Previous: ${previous}`);
1420
1589
  */
1421
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
+ }
1422
1595
  this.debug(`[Player] previous called`);
1423
1596
  const track = this.queue.previous();
1424
1597
  if (!track)
@@ -1548,6 +1721,12 @@ class Player extends events_1.EventEmitter {
1548
1721
  * console.log(`Auto-play mode: ${autoPlayMode}`);
1549
1722
  */
1550
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
+ }
1551
1730
  return this.queue.autoPlay(mode);
1552
1731
  }
1553
1732
  /**
@@ -1560,11 +1739,20 @@ class Player extends events_1.EventEmitter {
1560
1739
  * console.log(`Volume set: ${volumeSet}`);
1561
1740
  */
1562
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
+ }
1563
1746
  this.debug(`[Player] setVolume called: ${volume}`);
1564
1747
  if (volume < 0 || volume > 200)
1565
1748
  return false;
1566
1749
  const oldVolume = this.volume;
1567
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
+ }
1568
1756
  const resourceVolume = this.currentResource?.volume;
1569
1757
  if (resourceVolume) {
1570
1758
  if (this.volumeInterval)
@@ -1584,6 +1772,12 @@ class Player extends events_1.EventEmitter {
1584
1772
  }, 300);
1585
1773
  }
1586
1774
  this.emit("volumeChange", oldVolume, volume);
1775
+ for (const fp of this.forwardFollowers) {
1776
+ try {
1777
+ fp.volume = volume;
1778
+ }
1779
+ catch { }
1780
+ }
1587
1781
  return true;
1588
1782
  }
1589
1783
  /**
@@ -1624,6 +1818,10 @@ class Player extends events_1.EventEmitter {
1624
1818
  */
1625
1819
  async insert(query, index, requestedBy) {
1626
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
+ }
1627
1825
  this.debug(`[Player] insert called at index ${index} with type: ${typeof query}`);
1628
1826
  let tracksToAdd = [];
1629
1827
  let isPlaylist = false;
@@ -1834,6 +2032,10 @@ class Player extends events_1.EventEmitter {
1834
2032
  if (this.destroyed)
1835
2033
  return;
1836
2034
  this.destroyed = true;
2035
+ if (this.remoteHandle?.destroy) {
2036
+ this.remoteHandle.destroy().catch(() => { });
2037
+ this.remoteHandle = undefined;
2038
+ }
1837
2039
  if (this.leaveTimeout) {
1838
2040
  clearTimeout(this.leaveTimeout);
1839
2041
  this.leaveTimeout = null;
@@ -1861,14 +2063,21 @@ class Player extends events_1.EventEmitter {
1861
2063
  this.pluginManager.clear();
1862
2064
  this.filter.destroy();
1863
2065
  this.extensionManager.destroy();
1864
- this.isPlaying = false;
1865
- this.isPaused = false;
1866
2066
  // Clear any remaining intervals
1867
2067
  if (this.volumeInterval) {
1868
2068
  clearInterval(this.volumeInterval);
1869
2069
  this.volumeInterval = null;
1870
2070
  }
1871
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();
1872
2081
  this.removeAllListeners();
1873
2082
  }
1874
2083
  //#endregion
@@ -1940,13 +2149,7 @@ class Player extends events_1.EventEmitter {
1940
2149
  this.audioPlayer.play(resource);
1941
2150
  }
1942
2151
  // 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;
2152
+ if (wasPaused) {
1950
2153
  this.audioPlayer.pause();
1951
2154
  }
1952
2155
  this.debug(`[Player] Successfully applied filter to current track at position ${currentPosition}ms`);
@@ -2012,6 +2215,9 @@ class Player extends events_1.EventEmitter {
2012
2215
  if (track) {
2013
2216
  this.debug(`[Player] Track ended: ${track.title}`);
2014
2217
  this.emit("trackEnd", track);
2218
+ for (const fp of this.forwardFollowers) {
2219
+ fp.emit("trackEnd", track);
2220
+ }
2015
2221
  }
2016
2222
  void this.playNext();
2017
2223
  }
@@ -2019,30 +2225,41 @@ class Player extends events_1.EventEmitter {
2019
2225
  (oldState.status === voice_1.AudioPlayerStatus.Idle || oldState.status === voice_1.AudioPlayerStatus.Buffering)) {
2020
2226
  // Track started
2021
2227
  this.clearLeaveTimeout();
2022
- this.isPlaying = true;
2023
- this.isPaused = false;
2024
2228
  const track = this.queue.currentTrack;
2025
2229
  if (track) {
2026
2230
  this.debug(`[Player] Track started: ${track.title}`);
2027
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
+ }
2028
2243
  }
2029
2244
  }
2030
2245
  else if (newState.status === voice_1.AudioPlayerStatus.Paused && oldState.status !== voice_1.AudioPlayerStatus.Paused) {
2031
- // Track paused
2032
- this.isPaused = true;
2033
2246
  const track = this.queue.currentTrack;
2034
2247
  if (track) {
2035
2248
  this.debug(`[Player] Player paused on track: ${track.title}`);
2036
2249
  this.emit("playerPause", track);
2250
+ for (const fp of this.forwardFollowers) {
2251
+ fp.emit("playerPause", track);
2252
+ }
2037
2253
  }
2038
2254
  }
2039
2255
  else if (newState.status !== voice_1.AudioPlayerStatus.Paused && oldState.status === voice_1.AudioPlayerStatus.Paused) {
2040
- // Track resumed
2041
- this.isPaused = false;
2042
2256
  const track = this.queue.currentTrack;
2043
2257
  if (track) {
2044
2258
  this.debug(`[Player] Player resumed on track: ${track.title}`);
2045
2259
  this.emit("playerResume", track);
2260
+ for (const fp of this.forwardFollowers) {
2261
+ fp.emit("playerResume", track);
2262
+ }
2046
2263
  }
2047
2264
  }
2048
2265
  else if (newState.status === voice_1.AudioPlayerStatus.AutoPaused) {
@@ -2136,6 +2353,25 @@ class Player extends events_1.EventEmitter {
2136
2353
  plugins: this.pluginManager.getAll().map((plugin) => plugin.name),
2137
2354
  };
2138
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
+ }
2139
2375
  /**
2140
2376
  * Get serializable state (for manual persistence)
2141
2377
  */
@@ -2189,6 +2425,59 @@ class Player extends events_1.EventEmitter {
2189
2425
  totalStreams: this.streamManager.getStreamCount(),
2190
2426
  };
2191
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
+ }
2192
2481
  //#endregion
2193
2482
  //#region Getters
2194
2483
  /**
@@ -2269,8 +2558,45 @@ class Player extends events_1.EventEmitter {
2269
2558
  return this.queue.relatedTracks();
2270
2559
  }
2271
2560
  get isLive() {
2561
+ if (this.playbackMode === types_1.PlaybackMode.FORWARD)
2562
+ return true; //forward Mode -> live from Leader
2272
2563
  return this.currentTrack?.isLive === true;
2273
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
+ }
2274
2600
  }
2275
2601
  exports.Player = Player;
2276
2602
  //# sourceMappingURL=Player.js.map