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.
Files changed (39) hide show
  1. package/{AI-Guide.md → AGENTS.md} +717 -624
  2. package/README.md +658 -526
  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 +105 -51
  12. package/dist/plugins/index.js.map +1 -1
  13. package/dist/structures/Player.d.ts +90 -15
  14. package/dist/structures/Player.d.ts.map +1 -1
  15. package/dist/structures/Player.js +487 -81
  16. package/dist/structures/Player.js.map +1 -1
  17. package/dist/structures/PlayerManager.d.ts +70 -6
  18. package/dist/structures/PlayerManager.d.ts.map +1 -1
  19. package/dist/structures/PlayerManager.js +184 -19
  20. package/dist/structures/PlayerManager.js.map +1 -1
  21. package/dist/structures/Queue.d.ts +19 -0
  22. package/dist/structures/Queue.d.ts.map +1 -1
  23. package/dist/structures/Queue.js +21 -0
  24. package/dist/structures/Queue.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 +69 -2
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/types/index.js +13 -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 +137 -54
  35. package/src/structures/Player.ts +2937 -2457
  36. package/src/structures/PlayerManager.ts +916 -725
  37. package/src/structures/Queue.ts +621 -599
  38. package/src/types/extension.ts +3 -0
  39. package/src/types/index.ts +80 -2
@@ -1,624 +1,717 @@
1
- # 🤖 AI Guide for ZiPlayer
2
-
3
- A comprehensive guide for AI assistants and developers working with ZiPlayer - a powerful Discord music player library.
4
-
5
- ## 📋 Table of Contents
6
-
7
- 1. [Project Overview](#project-overview)
8
- 2. [Architecture](#architecture)
9
- 3. [Core Concepts](#core-concepts)
10
- 4. [API Reference](#api-reference)
11
- 5. [Common Patterns](#common-patterns)
12
- 6. [Troubleshooting](#troubleshooting)
13
- 7. [Code Examples](#code-examples)
14
-
15
- ---
16
-
17
- ## 🎯 Project Overview
18
-
19
- **ZiPlayer** is an extensible Discord music engine built on `@discordjs/voice`.
20
-
21
- ### Key Features
22
-
23
- - Plugin-driven architecture (YouTube, SoundCloud, Spotify, TTS)
24
- - Extension system (voice commands, lyrics, Lavalink)
25
- - Audio filters (bassboost, nightcore, etc.)
26
- - Smart caching and fallback system
27
-
28
- ### Tech Stack
29
-
30
- - TypeScript
31
- - `@discordjs/voice` for audio
32
- - FFmpeg for audio processing
33
- - Node.js EventEmitter for events
34
-
35
- ---
36
-
37
- ### Component Responsibilities
38
-
39
- | Component | Responsibility |
40
- | ------------------ | -------------------------------------------------- |
41
- | `PlayerManager` | Creates/manages players, global event bus |
42
- | `Player` | Per-guild audio playback, controls, event emission |
43
- | `Queue` | Track management, loop modes, history, auto-play |
44
- | `PluginManager` | Audio source resolution, streaming, fallback logic |
45
- | `ExtensionManager` | Custom hooks (search, stream, before/after play) |
46
- | `FilterManager` | FFmpeg audio effects |
47
- | `StreamManager` | Centralized stream management/Auto cleanup |
48
-
49
- ---
50
-
51
- ## 🧠 Core Concepts
52
-
53
- ### 1. Player Lifecycle
54
-
55
- ```typescript
56
- // Create → Connect → Play → (Auto-save) → Destroy
57
- const player = await manager.create(guildId, options);
58
- await player.connect(voiceChannel);
59
- await player.play(query, userId);
60
- // ... auto-saves periodically
61
- player.destroy();
62
- ```
63
-
64
- ### 2. Queue Loop Modes
65
-
66
- ```typescript
67
- player.loop("off"); // No loop (default)
68
- player.loop("track"); // Repeat current track
69
- player.loop("queue"); // Repeat entire queue
70
- ```
71
-
72
- ### 3. Event Flow
73
-
74
- ```
75
- trackStart → playing → trackEnd → playNext → (loop/autoplay)
76
-
77
- queueEnd leave
78
- ```
79
-
80
- ### 4. Plugin Priority & Fallback
81
-
82
- ```typescript
83
- // Plugins are tried in priority order (higher = first)
84
- // If primary fails, fallback plugins are attempted sequentially
85
- // Failed plugins don't block the queue
86
-
87
- plugin.priority = 10; // Higher priority
88
- ```
89
-
90
- ### 5. Caching Strategy
91
-
92
- | Cache Type | TTL | Purpose |
93
- | --------------- | ------- | --------------------------- |
94
- | Search cache | 2 min | Avoid duplicate API calls |
95
- | Stream cache | 5 min | Cache resolved streams |
96
- | Extension cache | 1-5 min | Extension operation results |
97
-
98
- ---
99
-
100
- ## 📚 API Reference
101
-
102
- ### PlayerManager
103
-
104
- #### Constructor Options
105
-
106
- ```typescript
107
- interface PlayerManagerOptions {
108
- plugins?: SourcePlugin[]; // Audio source plugins
109
- extensions?: BaseExtension[]; // Custom extensions
110
- extractorTimeout?: number; // Default: 10000ms
111
- autoCleanup?: boolean; // Default: true
112
- cleanupInterval?: number; // Default: 60000ms
113
- enableSearchCache?: boolean; // Default: true
114
- enableStatsCollection?: boolean; // Default: false
115
- }
116
- ```
117
-
118
- #### Player Runtime Options (Performance Profile)
119
-
120
- ```typescript
121
- interface PlayerOptions {
122
- lowPerformance?: boolean; // Default: false (or true when quality === "low")
123
- preload?: {
124
- enabled?: boolean; // Default: true
125
- autoDisableInLowPerformance?: boolean; // Default: true
126
- };
127
- crossfade?: {
128
- enabled?: boolean; // Explicit on/off
129
- autoEnable?: boolean; // Default: true when enabled is undefined
130
- autoDisableInLowPerformance?: boolean; // Default: true
131
- durationMs?: number; // Default: 5000
132
- };
133
- smartTransition?: {
134
- enabled?: boolean;
135
- genreAware?: boolean;
136
- beatAlign?: boolean;
137
- baseDurationMs?: number;
138
- minDurationMs?: number;
139
- maxDurationMs?: number;
140
- genreDurations?: Record<string, number>;
141
- beatAlignMaxWaitMs?: number;
142
- };
143
- antiStuck?: {
144
- enabled?: boolean;
145
- maxRetries?: number;
146
- retryDelayMs?: number;
147
- reusePreloadFirst?: boolean;
148
- reduceQualityOnRetry?: boolean;
149
- controlledSkipThreshold?: number;
150
- };
151
- loudnessNormalization?: {
152
- enabled?: boolean;
153
- targetLUFS?: number;
154
- maxBoostDb?: number;
155
- maxCutDb?: number;
156
- limiterCeiling?: number;
157
- };
158
- }
159
- ```
160
-
161
- - If `lowPerformance=true`, preload and crossfade are auto-disabled by default.
162
- - `crossfade.autoEnable=true` allows crossfade to be enabled automatically when `crossfade.enabled` is not explicitly set.
163
- - You can still force behavior by setting `enabled` flags directly.
164
- - Runtime behavior: crossfade is used for next-track transitions and `skip()`.
165
- - Smart transition can tune fade by `metadata.genre` and beat-align by `metadata.bpm`.
166
- - Loudness normalization uses `metadata.lufs` with limiter ceiling protection.
167
- - Anti-stuck retries in-place before controlled skip to avoid skip storms.
168
-
169
- #### Key Methods
170
-
171
- | Method | Description |
172
- | ---------------------------- | -------------------------- |
173
- | `create(guildId, options)` | Create new player |
174
- | `get(guildId)` | Get existing player |
175
- | `delete(guildId)` | Destroy and remove player |
176
- | `getAll()` | Get all players |
177
- | `broadcast(action, ...args)` | Send action to all players |
178
-
179
- ### Player
180
-
181
- #### Core Methods
182
-
183
- | Method | Description | Returns |
184
- | ------------------------------ | ----------------------- | ------------------- |
185
- | `play(query, userId)` | Play track/search/queue | `Promise<boolean>` |
186
- | `pause()` | Pause current | `boolean` |
187
- | `resume()` | Resume playback | `boolean` |
188
- | `skip(index?)` | Skip to next/index | `boolean` |
189
- | `stop()` | Stop and clear queue | `boolean` |
190
- | `seek(position)` | Seek to position (ms) | `Promise<boolean>` |
191
- | `previous()` | Play previous track | `Promise<boolean>` |
192
- | `setVolume(vol)` | Set volume (0-200) | `boolean` |
193
- | `loop(mode)` | Set loop mode | `LoopMode` |
194
- | `shuffle()` | Shuffle queue | `void` |
195
- | `insert(query, index, userId)` | Insert at position | `Promise<boolean>` |
196
- | `save(track, options)` | Save track to stream | `Promise<Readable>` |
197
-
198
- #### Getters
199
-
200
- ```typescript
201
- player.currentTrack; // Track | null
202
- player.queueSize; // number
203
- player.isPlaying; // boolean
204
- player.isPaused; // boolean
205
- player.volume; // number
206
- player.upcomingTracks; // Track[]
207
- player.previousTracks; // Track[]
208
- player.relatedTracks; // Track[] | null
209
- ```
210
-
211
- ### Queue
212
-
213
- #### Methods
214
-
215
- | Method | Description |
216
- | ------------------------- | -------------------- |
217
- | `add(track)` | Add single track |
218
- | `addMultiple(tracks)` | Add multiple tracks |
219
- | `insert(track, index)` | Insert at position |
220
- | `remove(index)` | Remove at index |
221
- | `removeMultiple(indices)` | Remove multiple |
222
- | `removeWhere(predicate)` | Remove by condition |
223
- | `move(from, to)` | Move track |
224
- | `swap(a, b)` | Swap tracks |
225
- | `shuffle()` | Randomize order |
226
- | `clear()` | Clear all tracks |
227
- | `loop(mode)` | Set loop mode |
228
- | `autoPlay(enabled)` | Enable/disable |
229
- | `previous()` | Get previous track |
230
- | `jumpToHistory(steps)` | Jump back in history |
231
-
232
- #### Properties
233
-
234
- ```typescript
235
- queue.size; // number
236
- queue.isEmpty; // boolean
237
- queue.currentTrack; // Track | null
238
- queue.nextTrack; // Track | null
239
- queue.lastTrack; // Track | null
240
- queue.previousTracks; // Track[]
241
- ```
242
-
243
- ### FilterManager
244
-
245
- ```typescript
246
- // Apply filters
247
- await player.filter.applyFilter("bassboost");
248
- await player.filter.applyFilters(["bassboost", "nightcore"]);
249
-
250
- // Available filters
251
- // bassboost, trebleboost, nightcore, lofi, vaporwave,
252
- // echo, reverb, chorus, karaoke, normalize, compressor, limiter
253
-
254
- // Clear filters
255
- await player.filter.clearAll();
256
- await player.filter.clear("bassboost");
257
-
258
- // Get current filters
259
- const filterString = player.filter.getFilterString(); // "bassboost,nightcore"
260
- ```
261
-
262
- ## 🔧 Common Patterns
263
-
264
- ### 1. Basic Music Bot Setup
265
-
266
- ```typescript
267
- import { Client, GatewayIntentBits } from "discord.js";
268
- import { PlayerManager } from "ziplayer";
269
- import { YouTubePlugin, SpotifyPlugin } from "@ziplayer/plugin";
270
-
271
- const client = new Client({
272
- intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
273
- });
274
-
275
- const manager = new PlayerManager({
276
- plugins: [new YouTubePlugin(), new SpotifyPlugin()],
277
- autoCleanup: true,
278
- });
279
-
280
- client.on("ready", async () => {
281
- // Auto-load saved players
282
- await manager.loadAllPlayers();
283
- });
284
-
285
- client.on("messageCreate", async (msg) => {
286
- if (msg.content.startsWith("!play")) {
287
- const player = await manager.create(msg.guildId);
288
- const voiceChannel = msg.member?.voice.channel;
289
-
290
- if (!player.connection) {
291
- await player.connect(voiceChannel);
292
- }
293
-
294
- const query = msg.content.slice(6);
295
- await player.play(query, msg.author.id);
296
- }
297
- });
298
-
299
- const player = await manager.create(guildId, {
300
- lowPerformance: false,
301
- preload: { enabled: true, autoDisableInLowPerformance: true },
302
- crossfade: { autoEnable: true, autoDisableInLowPerformance: true, durationMs: 5000 },
303
- smartTransition: {
304
- enabled: true,
305
- genreAware: true,
306
- beatAlign: true,
307
- baseDurationMs: 5000,
308
- genreDurations: { chill: 7000, edm: 2200 },
309
- },
310
- antiStuck: {
311
- enabled: true,
312
- maxRetries: 2,
313
- retryDelayMs: 900,
314
- reusePreloadFirst: true,
315
- reduceQualityOnRetry: true,
316
- controlledSkipThreshold: 3,
317
- },
318
- loudnessNormalization: {
319
- enabled: true,
320
- targetLUFS: -14,
321
- limiterCeiling: 0.95,
322
- },
323
- });
324
- ```
325
-
326
- ### 2. Progress Bar with Time Format
327
-
328
- ```typescript
329
- // Get formatted time
330
- const time = player.getTime();
331
- console.log(`Current: ${time.formatted.current}`); // "1:22:12"
332
- console.log(`Total: ${time.formatted.total}`); // "3:45:30"
333
-
334
- // Progress bar
335
- const progressBar = player.getProgressBar({
336
- size: 20,
337
- barChar: "▬",
338
- progressChar: "🔘",
339
- timeFormat: "compact",
340
- showPercentage: true,
341
- });
342
- // Output: "1:22:12 ▬▬▬▬▬▬▬▬▬🔘▬▬▬▬▬▬▬▬ 3:45:30 (36%)"
343
- ```
344
-
345
- ### 3. Queue Management Commands
346
-
347
- ```typescript
348
- // Skip to specific track
349
- await player.skip(3); // Skip to index 3
350
-
351
- // Move track to front
352
- player.queue.move(5, 0);
353
-
354
- // Remove all tracks from specific source
355
- player.queue.removeWhere((t) => t.source === "soundcloud");
356
-
357
- // Jump back 2 tracks
358
- await player.queue.jumpToHistory(2);
359
-
360
- // Insert as next track
361
- await player.insert("song name", 0, userId);
362
- ```
363
-
364
- ### 4. Custom Plugin Implementation
365
-
366
- ```typescript
367
- import { BasePlugin, Track, StreamInfo } from "ziplayer";
368
-
369
- class CustomPlugin extends BasePlugin {
370
- name = "CustomPlugin";
371
- priority = 5;
372
-
373
- canHandle(query: string): boolean {
374
- return query.startsWith("custom:");
375
- }
376
-
377
- async search(query: string, requestedBy: string): Promise<SearchResult> {
378
- // Implementation
379
- return { tracks: [] };
380
- }
381
-
382
- async getStream(track: Track, signal: AbortSignal): Promise<StreamInfo> {
383
- // Return audio stream
384
- return { stream: readableStream, type: "arbitrary" };
385
- }
386
-
387
- async getRelatedTracks(track: Track): Promise<Track[]> {
388
- // Return recommendations
389
- return [];
390
- }
391
- }
392
- ```
393
-
394
- ### 5. Custom Extension Implementation
395
-
396
- ```typescript
397
- import { BaseExtension, ExtensionContext } from "ziplayer";
398
-
399
- class LoggerExtension extends BaseExtension {
400
- name = "Logger";
401
- priority = 10;
402
-
403
- async beforePlay(context: ExtensionContext, request: any) {
404
- console.log(`Playing: ${request.query}`);
405
- return { handled: false };
406
- }
407
-
408
- async afterPlay(context: ExtensionContext, payload: any) {
409
- if (payload.success) {
410
- console.log(`Successfully played ${payload.tracks?.length} tracks`);
411
- }
412
- }
413
- }
414
- ```
415
-
416
- ### 6. Event Handling
417
-
418
- ```typescript
419
- manager.on("trackStart", (player, track) => {
420
- console.log(`Now playing: ${track.title}`);
421
- });
422
-
423
- manager.on("queueEnd", (player) => {
424
- console.log("Queue finished!");
425
- });
426
-
427
- manager.on("playerError", (player, error, track) => {
428
- console.error(`Error on ${track?.title}:`, error.message);
429
- });
430
-
431
- manager.on("playerSaved", (guildId) => {
432
- console.log(`Saved state for guild ${guildId}`);
433
- });
434
-
435
- manager.on("stats", (stats) => {
436
- console.log(`Active players: ${stats.activePlayers}`);
437
- });
438
- ```
439
-
440
- ---
441
-
442
- ## 🐛 Troubleshooting
443
-
444
- ### Common Issues
445
-
446
- | Issue | Solution |
447
- | ------------------------ | ----------------------------------------------------------- |
448
- | **No audio** | Check `player.connection` exists, voice channel permissions |
449
- | **Plugin not working** | Verify `canHandle()` returns true, check priority |
450
- | **Filters not applying** | Call `refreshPlayerResource(true)` after applying |
451
- | **Memory leak** | Enable `autoCleanup`, call `player.destroy()` when done |
452
- | **Rate limiting** | Use search cache, increase `extractorTimeout` |
453
-
454
- ### Debug Mode
455
-
456
- ```typescript
457
- // Enable debug logging
458
- manager.on("debug", (message) => {
459
- console.log("[DEBUG]", message);
460
- });
461
-
462
- // Or check debug flag
463
- if (manager.debugEnabled) {
464
- // Debug-specific logic
465
- }
466
- ```
467
-
468
- ### Performance Tips
469
-
470
- 1. **Enable caching** for search and stream results
471
- 2. **Set appropriate timeouts** based on network conditions
472
- 3. **Batch operations** when modifying queue
473
- 4. **Destroy players** when no longer needed
474
-
475
- ---
476
-
477
- ## 📝 Code Examples
478
-
479
- ### Full Bot Example
480
-
481
- ```typescript
482
- import { Client, GatewayIntentBits, EmbedBuilder } from "discord.js";
483
- import { PlayerManager } from "ziplayer";
484
- import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
485
-
486
- const client = new Client({
487
- intents: [
488
- GatewayIntentBits.Guilds,
489
- GatewayIntentBits.GuildVoiceStates,
490
- GatewayIntentBits.GuildMessages,
491
- GatewayIntentBits.MessageContent,
492
- ],
493
- });
494
-
495
- const manager = new PlayerManager({
496
- plugins: [new YouTubePlugin(), new SpotifyPlugin(), new TTSPlugin()],
497
- autoCleanup: true,
498
- extractorTimeout: 30000,
499
- });
500
-
501
- client.on("messageCreate", async (msg) => {
502
- if (!msg.guildId || msg.author.bot) return;
503
-
504
- const args = msg.content.slice(1).split(" ");
505
- const command = args[0].toLowerCase();
506
- const query = args.slice(1).join(" ");
507
-
508
- const player = await manager.create(msg.guildId);
509
- const voiceChannel = msg.member?.voice.channel;
510
-
511
- switch (command) {
512
- case "play":
513
- if (!voiceChannel) return msg.reply("Join a voice channel!");
514
- if (!player.connection) await player.connect(voiceChannel);
515
- await player.play(query, msg.author.id);
516
- break;
517
-
518
- case "pause":
519
- player.pause();
520
- break;
521
-
522
- case "resume":
523
- player.resume();
524
- break;
525
-
526
- case "skip":
527
- player.skip();
528
- break;
529
-
530
- case "stop":
531
- player.stop();
532
- break;
533
-
534
- case "volume":
535
- const vol = parseInt(query);
536
- if (isNaN(vol)) return msg.reply("Volume must be a number!");
537
- player.setVolume(vol);
538
- break;
539
-
540
- case "queue":
541
- const tracks = player.upcomingTracks.slice(0, 10);
542
- const embed = new EmbedBuilder()
543
- .setTitle("Queue")
544
- .setDescription(tracks.map((t, i) => `${i + 1}. ${t.title}`).join("\n") || "Empty");
545
- msg.reply({ embeds: [embed] });
546
- break;
547
-
548
- case "nowplaying":
549
- const track = player.currentTrack;
550
- if (!track) return msg.reply("Nothing playing!");
551
-
552
- const progress = player.getProgressBar({ size: 15 });
553
- const time = player.getTime();
554
-
555
- const embed = new EmbedBuilder()
556
- .setTitle(track.title)
557
- .setURL(track.url)
558
- .setThumbnail(track.thumbnail)
559
- .setDescription(`\`${progress}\`\n${time.formatted.current} / ${time.formatted.total}`);
560
- msg.reply({ embeds: [embed] });
561
- break;
562
- }
563
- });
564
-
565
- client.login(process.env.DISCORD_TOKEN);
566
- ```
567
-
568
- ---
569
-
570
- ## 🔗 Quick Reference
571
-
572
- ### Import Paths
573
-
574
- ```typescript
575
- // Core
576
- import { PlayerManager, Player, Queue } from "ziplayer";
577
-
578
- // Types
579
- import type { Track, SearchResult, LoopMode, StreamInfo } from "ziplayer";
580
-
581
- // Plugins (external package)
582
- import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
583
-
584
- // infinity plugin support stream audio from YouTube, TikTok, Instagram, Twitter/X, SoundCloud, Reddit, Twitch, Bilibili, and 1000+ other sites
585
-
586
- import { InfinityPlugin } from "@ziplayer/infinity";
587
-
588
- // Extensions (external package)
589
- import { voiceExt, lyricsExt, lavalinkExt } from "@ziplayer/extension";
590
- ```
591
-
592
- ### Type Definitions
593
-
594
- ```typescript
595
- interface Track {
596
- id: string;
597
- title: string;
598
- url: string;
599
- source: string;
600
- duration: number;
601
- thumbnail?: string;
602
- requestedBy?: string;
603
- isLive?: boolean;
604
- }
605
-
606
- type LoopMode = "off" | "track" | "queue";
607
-
608
- interface SearchResult {
609
- tracks: Track[];
610
- playlist?: { name: string; url?: string };
611
- }
612
- ```
613
-
614
- ---
615
-
616
- ## 📖 Additional Resources
617
-
618
- - [GitHub Repository](https://github.com/ZiProject/ZiPlayer)
619
- - [npm Package](https://www.npmjs.com/package/ziplayer)
620
- - [Examples Folder](https://github.com/ZiProject/ZiPlayer/tree/main/examples)
621
-
622
- ---
623
-
624
- _This guide is maintained for AI assistants and developers. For questions or contributions, please open an issue on GitHub._
1
+ # AI / agent notes — ZiPlayer core
2
+
3
+ A comprehensive guide for AI assistants and developers working with ZiPlayer - a powerful Discord music player library.
4
+
5
+ ## 📋 Table of Contents
6
+
7
+ 1. [Project Overview](#project-overview)
8
+ 2. [Architecture](#architecture)
9
+ 3. [Core Concepts](#core-concepts)
10
+ 4. [API Reference](#api-reference)
11
+ 5. [Common Patterns](#common-patterns)
12
+ 6. [Troubleshooting](#troubleshooting)
13
+ 7. [Code Examples](#code-examples)
14
+
15
+ ---
16
+
17
+ ## 🎯 Project Overview
18
+
19
+ **ZiPlayer** is an extensible Discord music engine built on `@discordjs/voice`.
20
+
21
+ ### Key Features
22
+
23
+ - Plugin-driven architecture (YouTube, SoundCloud, Spotify, TTS)
24
+ - Extension system (voice commands, lyrics, Lavalink)
25
+ - Audio filters (bassboost, nightcore, etc.)
26
+ - Smart caching and fallback system
27
+
28
+ ### Tech Stack
29
+
30
+ - TypeScript
31
+ - `@discordjs/voice` for audio
32
+ - FFmpeg for audio processing
33
+ - Node.js EventEmitter for events
34
+
35
+ ## 📦 Installation
36
+
37
+ ```bash
38
+ npm install ziplayer @ziplayer/plugin @ziplayer/extension @ziplayer/infinity @discordjs/voice discord.js opusscript
39
+ ```
40
+
41
+ ---
42
+
43
+ ### Component Responsibilities
44
+
45
+ | Component | Responsibility |
46
+ | ------------------ | -------------------------------------------------- |
47
+ | `PlayerManager` | Creates/manages players, global event bus |
48
+ | `Player` | Per-guild audio playback, controls, event emission |
49
+ | `Queue` | Track management, loop modes, history, auto-play |
50
+ | `PluginManager` | Audio source resolution, streaming, fallback logic |
51
+ | `ExtensionManager` | Custom hooks (search, stream, before/after play) |
52
+ | `FilterManager` | FFmpeg audio effects |
53
+ | `StreamManager` | Centralized stream management/Auto cleanup |
54
+
55
+ ---
56
+
57
+ ## 🧠 Core Concepts
58
+
59
+ ### 1. Player Lifecycle
60
+
61
+ ```typescript
62
+ // Create → Connect → Play → Destroy
63
+ const player = await manager.create(guildId, options);
64
+ await player.connect(voiceChannel);
65
+ await player.play(query, userId);
66
+ player.destroy();
67
+ ```
68
+
69
+ ### 2. Queue Loop Modes
70
+
71
+ ```typescript
72
+ player.loop("off"); // No loop (default)
73
+ player.loop("track"); // Repeat current track
74
+ player.loop("queue"); // Repeat entire queue
75
+ ```
76
+
77
+ ### 3. Event Flow
78
+
79
+ ```
80
+ trackStart playing trackEnd → playNext → (loop/autoplay)
81
+
82
+ queueEnd → leave
83
+ ```
84
+
85
+ ### 4. Plugin Priority & Fallback
86
+
87
+ ```typescript
88
+ // Plugins are tried in priority order (higher = first)
89
+ // If primary fails, fallback plugins are attempted sequentially
90
+ // Failed plugins don't block the queue
91
+
92
+ plugin.priority = 10; // Higher priority
93
+ ```
94
+
95
+ ### 5. Caching Strategy
96
+
97
+ | Cache Type | TTL | Purpose |
98
+ | --------------- | ------- | --------------------------- |
99
+ | Search cache | 2 min | Avoid duplicate API calls |
100
+ | Stream cache | 5 min | Cache resolved streams |
101
+ | Extension cache | 1-5 min | Extension operation results |
102
+
103
+ ---
104
+
105
+ ## 📚 API Reference
106
+
107
+ ### PlayerManager
108
+
109
+ #### Constructor Options
110
+
111
+ ```typescript
112
+ interface PlayerManagerOptions {
113
+ plugins?: SourcePlugin[]; // Audio source plugins
114
+ extensions?: BaseExtension[]; // Custom extensions
115
+ extractorTimeout?: number; // Default: 10000ms
116
+ autoCleanup?: boolean; // Default: true
117
+ cleanupInterval?: number; // Default: 60000ms
118
+ enableSearchCache?: boolean; // Default: true
119
+ enableStatsCollection?: boolean; // Default: false
120
+ }
121
+ ```
122
+
123
+ #### Player Runtime Options (Performance Profile)
124
+
125
+ ```typescript
126
+ interface PlayerOptions {
127
+ lowPerformance?: boolean; // Default: false (or true when quality === "low")
128
+ preload?: {
129
+ enabled?: boolean; // Default: true
130
+ autoDisableInLowPerformance?: boolean; // Default: true
131
+ };
132
+ crossfade?: {
133
+ enabled?: boolean; // Explicit on/off
134
+ autoEnable?: boolean; // Default: true when enabled is undefined
135
+ autoDisableInLowPerformance?: boolean; // Default: true
136
+ durationMs?: number; // Default: 5000
137
+ };
138
+ smartTransition?: {
139
+ enabled?: boolean;
140
+ genreAware?: boolean;
141
+ beatAlign?: boolean;
142
+ baseDurationMs?: number;
143
+ minDurationMs?: number;
144
+ maxDurationMs?: number;
145
+ genreDurations?: Record<string, number>;
146
+ beatAlignMaxWaitMs?: number;
147
+ };
148
+ antiStuck?: {
149
+ enabled?: boolean;
150
+ maxRetries?: number;
151
+ retryDelayMs?: number;
152
+ reusePreloadFirst?: boolean;
153
+ reduceQualityOnRetry?: boolean;
154
+ controlledSkipThreshold?: number;
155
+ };
156
+ loudnessNormalization?: {
157
+ enabled?: boolean;
158
+ targetLUFS?: number;
159
+ maxBoostDb?: number;
160
+ maxCutDb?: number;
161
+ limiterCeiling?: number;
162
+ };
163
+ }
164
+ ```
165
+
166
+ - If `lowPerformance=true`, preload and crossfade are auto-disabled by default.
167
+ - `crossfade.autoEnable=true` allows crossfade to be enabled automatically when `crossfade.enabled` is not explicitly set.
168
+ - You can still force behavior by setting `enabled` flags directly.
169
+ - Runtime behavior: crossfade is used for next-track transitions and `skip()`.
170
+ - Smart transition can tune fade by `metadata.genre` and beat-align by `metadata.bpm`.
171
+ - Loudness normalization uses `metadata.lufs` with limiter ceiling protection.
172
+ - Anti-stuck retries in-place before controlled skip to avoid skip storms.
173
+
174
+ #### Key Methods
175
+
176
+ | Method | Description |
177
+ | ---------------------------- | -------------------------- |
178
+ | `create(guildId, options)` | Create new player |
179
+ | `get(guildId)` | Get existing player |
180
+ | `delete(guildId)` | Destroy and remove player |
181
+ | `getAll()` | Get all players |
182
+ | `broadcast(action, ...args)` | Send action to all players |
183
+
184
+ ### Player
185
+
186
+ #### Core Methods
187
+
188
+ | Method | Description | Returns |
189
+ | ------------------------------ | ---------------------------------------------------------- | ------------------- |
190
+ | `play(query, userId)` | Play track/search/queue | `Promise<boolean>` |
191
+ | `pause()` | Pause current | `boolean` |
192
+ | `resume()` | Resume playback | `boolean` |
193
+ | `skip(index?)` | Skip to next/index | `boolean` |
194
+ | `stop()` | Stop and clear queue | `boolean` |
195
+ | `seek(position)` | Seek to position (ms) | `Promise<boolean>` |
196
+ | `previous()` | Play previous track | `Promise<boolean>` |
197
+ | `setVolume(vol)` | Set volume (0-200) | `boolean` |
198
+ | `loop(mode)` | Set loop mode | `LoopMode` |
199
+ | `shuffle()` | Shuffle queue | `void` |
200
+ | `insert(query, index, userId)` | Insert at position | `Promise<boolean>` |
201
+ | `save(track, options)` | Save track to stream | `Promise<Readable>` |
202
+ | `subscribeTo(leader, options)` | Subscribe this player to another player's playback stream. | `boolean` |
203
+ | `unsubscribeForward()` | Unsubscribe this player from its current playback leader. | `boolean` |
204
+
205
+ #### Getters
206
+
207
+ ```typescript
208
+ player.currentTrack; // Track | null
209
+ player.queueSize; // number
210
+ player.isPlaying; // boolean
211
+ player.isPaused; // boolean
212
+ player.volume; // number
213
+ player.upcomingTracks; // Track[]
214
+ player.previousTracks; // Track[]
215
+ player.relatedTracks; // Track[] | null
216
+ ```
217
+
218
+ ### Queue
219
+
220
+ #### Methods
221
+
222
+ | Method | Description |
223
+ | ------------------------- | -------------------- |
224
+ | `add(track)` | Add single track |
225
+ | `addMultiple(tracks)` | Add multiple tracks |
226
+ | `insert(track, index)` | Insert at position |
227
+ | `remove(index)` | Remove at index |
228
+ | `removeMultiple(indices)` | Remove multiple |
229
+ | `removeWhere(predicate)` | Remove by condition |
230
+ | `move(from, to)` | Move track |
231
+ | `swap(a, b)` | Swap tracks |
232
+ | `shuffle()` | Randomize order |
233
+ | `clear()` | Clear all tracks |
234
+ | `loop(mode)` | Set loop mode |
235
+ | `autoPlay(enabled)` | Enable/disable |
236
+ | `previous()` | Get previous track |
237
+ | `jumpToHistory(steps)` | Jump back in history |
238
+
239
+ #### Properties
240
+
241
+ ```typescript
242
+ queue.size; // number
243
+ queue.isEmpty; // boolean
244
+ queue.currentTrack; // Track | null
245
+ queue.nextTrack; // Track | null
246
+ queue.lastTrack; // Track | null
247
+ queue.previousTracks; // Track[]
248
+ ```
249
+
250
+ ### FilterManager
251
+
252
+ ```typescript
253
+ // Apply filters
254
+ await player.filter.applyFilter("bassboost");
255
+ await player.filter.applyFilters(["bassboost", "nightcore"]);
256
+
257
+ // Available filters
258
+ // bassboost, trebleboost, nightcore, lofi, vaporwave,
259
+ // echo, reverb, chorus, karaoke, normalize, compressor, limiter
260
+
261
+ // Clear filters
262
+ await player.filter.clearAll();
263
+ await player.filter.clear("bassboost");
264
+
265
+ // Get current filters
266
+ const filterString = player.filter.getFilterString(); // "bassboost,nightcore"
267
+ ```
268
+
269
+ ## 🔧 Common Patterns
270
+
271
+ ### 1. Basic Music Bot Setup
272
+
273
+ ```typescript
274
+ import { Client, GatewayIntentBits } from "discord.js";
275
+ import { PlayerManager } from "ziplayer";
276
+ import { YouTubePlugin, SpotifyPlugin } from "@ziplayer/plugin";
277
+
278
+ const client = new Client({
279
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
280
+ });
281
+
282
+ const manager = new PlayerManager({
283
+ plugins: [new YouTubePlugin({}), new SpotifyPlugin()],
284
+ autoCleanup: true,
285
+ });
286
+
287
+ client.on("messageCreate", async (msg) => {
288
+ if (msg.content.startsWith("!play")) {
289
+ const player = await manager.create(msg.guildId);
290
+ const voiceChannel = msg.member?.voice.channel;
291
+
292
+ if (!player.connection) {
293
+ await player.connect(voiceChannel);
294
+ }
295
+
296
+ const query = msg.content.slice(6);
297
+ await player.play(query, msg.author.id);
298
+ }
299
+ });
300
+
301
+ const player = await manager.create(guildId, {
302
+ lowPerformance: false,
303
+ preload: { enabled: true, autoDisableInLowPerformance: true },
304
+ crossfade: { autoEnable: true, autoDisableInLowPerformance: true, durationMs: 5000 },
305
+ smartTransition: {
306
+ enabled: true,
307
+ genreAware: true,
308
+ beatAlign: true,
309
+ baseDurationMs: 5000,
310
+ genreDurations: { chill: 7000, edm: 2200 },
311
+ },
312
+ antiStuck: {
313
+ enabled: true,
314
+ maxRetries: 2,
315
+ retryDelayMs: 900,
316
+ reusePreloadFirst: true,
317
+ reduceQualityOnRetry: true,
318
+ controlledSkipThreshold: 3,
319
+ },
320
+ loudnessNormalization: {
321
+ enabled: true,
322
+ targetLUFS: -14,
323
+ limiterCeiling: 0.95,
324
+ },
325
+ });
326
+ ```
327
+
328
+ ### 2. Progress Bar with Time Format
329
+
330
+ ```typescript
331
+ // Get formatted time
332
+ const time = player.getTime();
333
+ console.log(`Current: ${time.formatted.current}`); // "1:22:12"
334
+ console.log(`Total: ${time.formatted.total}`); // "3:45:30"
335
+
336
+ // Progress bar
337
+ const progressBar = player.getProgressBar({
338
+ size: 20,
339
+ barChar: "",
340
+ progressChar: "🔘",
341
+ timeFormat: "compact",
342
+ showPercentage: true,
343
+ });
344
+ // Output: "1:22:12 ▬▬▬▬▬▬▬▬▬🔘▬▬▬▬▬▬▬▬ 3:45:30 (36%)"
345
+ ```
346
+
347
+ ### 3. Queue Management Commands
348
+
349
+ ```typescript
350
+ // Skip to specific track
351
+ await player.skip(3); // Skip to index 3
352
+
353
+ // Move track to front
354
+ player.queue.move(5, 0);
355
+
356
+ // Remove all tracks from specific source
357
+ player.queue.removeWhere((t) => t.source === "soundcloud");
358
+
359
+ // Jump back 2 tracks
360
+ await player.queue.jumpToHistory(2);
361
+
362
+ // Insert as next track
363
+ await player.insert("song name", 0, userId);
364
+ ```
365
+
366
+ ### 4. Custom Plugin Implementation
367
+
368
+ ```typescript
369
+ import { BasePlugin, Track, StreamInfo } from "ziplayer";
370
+
371
+ class CustomPlugin extends BasePlugin {
372
+ name = "CustomPlugin";
373
+ priority = 5;
374
+
375
+ canHandle(query: string): boolean {
376
+ return query.startsWith("custom:");
377
+ }
378
+
379
+ async search(query: string, requestedBy: string): Promise<SearchResult> {
380
+ // Implementation
381
+ return { tracks: [] };
382
+ }
383
+
384
+ async getStream(track: Track, signal: AbortSignal): Promise<StreamInfo> {
385
+ // Return audio stream
386
+ return { stream: readableStream, type: "arbitrary" };
387
+ }
388
+
389
+ async getRelatedTracks(track: Track): Promise<Track[]> {
390
+ // Return recommendations
391
+ return [];
392
+ }
393
+ }
394
+ ```
395
+
396
+ ### 5. Custom Extension Implementation
397
+
398
+ ```typescript
399
+ import { BaseExtension, ExtensionContext } from "ziplayer";
400
+
401
+ class LoggerExtension extends BaseExtension {
402
+ name = "Logger";
403
+ priority = 10;
404
+
405
+ async beforePlay(context: ExtensionContext, request: any) {
406
+ console.log(`Playing: ${request.query}`);
407
+ return { handled: false };
408
+ }
409
+
410
+ async afterPlay(context: ExtensionContext, payload: any) {
411
+ if (payload.success) {
412
+ console.log(`Successfully played ${payload.tracks?.length} tracks`);
413
+ }
414
+ }
415
+ }
416
+ ```
417
+
418
+ ### 6. Event Handling
419
+
420
+ ```typescript
421
+ manager.on("trackStart", (player, track) => {
422
+ console.log(`Now playing: ${track.title}`);
423
+ });
424
+
425
+ manager.on("queueEnd", (player) => {
426
+ console.log("Queue finished!");
427
+ });
428
+
429
+ manager.on("playerError", (player, error, track) => {
430
+ console.error(`Error on ${track?.title}:`, error.message);
431
+ });
432
+
433
+ manager.on("stats", (stats) => {
434
+ console.log(`Active players: ${stats.activePlayers}`);
435
+ });
436
+
437
+ // Listen globally via manager:
438
+ manager.on("trackStart", (player, track) => {});
439
+ manager.on("trackEnd", (player, track) => {});
440
+ manager.on("queueEnd", (player) => {});
441
+ manager.on("playerError", (player, error, track) => {});
442
+ manager.on("playerPause", (player, track) => {});
443
+ manager.on("playerResume", (player, track) => {});
444
+ manager.on("volumeChange", (player, oldVolume, newVolume) => {});
445
+ manager.on("queueAdd", (player, track) => {});
446
+ manager.on("queueAddList", (player, tracks) => {});
447
+ manager.on("queueRemove", (player, track, index) => {});
448
+ manager.on("playerDestroy", (player) => {});
449
+ manager.on("ttsStart", (player, payload) => {});
450
+ manager.on("ttsEnd", (player) => {});
451
+ manager.on("stats", (PlayerStats) => {});
452
+ manager.on("forwardModeStart", (player, leader) => {});
453
+ manager.on("forwardModeEnd", (player, leader) => {});
454
+ ```
455
+
456
+ ---
457
+
458
+ ## 🐛 Troubleshooting
459
+
460
+ ### Common Issues
461
+
462
+ | Issue | Solution |
463
+ | ------------------------ | ----------------------------------------------------------- |
464
+ | **No audio** | Check `player.connection` exists, voice channel permissions |
465
+ | **Plugin not working** | Verify `canHandle()` returns true, check priority |
466
+ | **Filters not applying** | Call `refreshPlayerResource(true)` after applying |
467
+ | **Memory leak** | Enable `autoCleanup`, call `player.destroy()` when done |
468
+ | **Rate limiting** | Use search cache, increase `extractorTimeout` |
469
+
470
+ ### Debug Mode
471
+
472
+ ```typescript
473
+ // Enable debug logging
474
+ manager.on("debug", (message) => {
475
+ console.log("[DEBUG]", message);
476
+ });
477
+
478
+ // Or check debug flag
479
+ if (manager.debugEnabled) {
480
+ // Debug-specific logic
481
+ }
482
+ ```
483
+
484
+ ### Performance Tips
485
+
486
+ 1. **Enable caching** for search and stream results
487
+ 2. **Set appropriate timeouts** based on network conditions
488
+ 3. **Batch operations** when modifying queue
489
+ 4. **Destroy players** when no longer needed
490
+
491
+ ---
492
+
493
+ ## 📝 Code Examples
494
+
495
+ ### Full Bot Example
496
+
497
+ ```typescript
498
+ import { Client, GatewayIntentBits, EmbedBuilder } from "discord.js";
499
+ import { PlayerManager } from "ziplayer";
500
+ import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
501
+
502
+ const client = new Client({
503
+ intents: [
504
+ GatewayIntentBits.Guilds,
505
+ GatewayIntentBits.GuildVoiceStates,
506
+ GatewayIntentBits.GuildMessages,
507
+ GatewayIntentBits.MessageContent,
508
+ ],
509
+ });
510
+
511
+ const manager = new PlayerManager({
512
+ plugins: [new YouTubePlugin(), new SpotifyPlugin(), new TTSPlugin()],
513
+ autoCleanup: true,
514
+ extractorTimeout: 30000,
515
+ });
516
+
517
+ client.on("messageCreate", async (msg) => {
518
+ if (!msg.guildId || msg.author.bot) return;
519
+
520
+ const args = msg.content.slice(1).split(" ");
521
+ const command = args[0].toLowerCase();
522
+ const query = args.slice(1).join(" ");
523
+
524
+ const player = await manager.create(msg.guildId);
525
+ const voiceChannel = msg.member?.voice.channel;
526
+
527
+ switch (command) {
528
+ case "play":
529
+ if (!voiceChannel) return msg.reply("Join a voice channel!");
530
+ if (!player.connection) await player.connect(voiceChannel);
531
+ await player.play(query, msg.author.id);
532
+ break;
533
+
534
+ case "pause":
535
+ player.pause();
536
+ break;
537
+
538
+ case "resume":
539
+ player.resume();
540
+ break;
541
+
542
+ case "skip":
543
+ player.skip();
544
+ break;
545
+
546
+ case "stop":
547
+ player.stop();
548
+ break;
549
+
550
+ case "volume":
551
+ const vol = parseInt(query);
552
+ if (isNaN(vol)) return msg.reply("Volume must be a number!");
553
+ player.setVolume(vol);
554
+ break;
555
+
556
+ case "queue":
557
+ const tracks = player.upcomingTracks.slice(0, 10);
558
+ const embed = new EmbedBuilder()
559
+ .setTitle("Queue")
560
+ .setDescription(tracks.map((t, i) => `${i + 1}. ${t.title}`).join("\n") || "Empty");
561
+ msg.reply({ embeds: [embed] });
562
+ break;
563
+
564
+ case "nowplaying":
565
+ const track = player.currentTrack;
566
+ if (!track) return msg.reply("Nothing playing!");
567
+
568
+ const progress = player.getProgressBar({ size: 15 });
569
+ const time = player.getTime();
570
+
571
+ const embed = new EmbedBuilder()
572
+ .setTitle(track.title)
573
+ .setURL(track.url)
574
+ .setThumbnail(track.thumbnail)
575
+ .setDescription(`\`${progress}\`\n${time.formatted.current} / ${time.formatted.total}`);
576
+ msg.reply({ embeds: [embed] });
577
+ break;
578
+ }
579
+ });
580
+
581
+ client.login(process.env.DISCORD_TOKEN);
582
+ ```
583
+
584
+ ---
585
+
586
+ ## 🔗 Quick Reference
587
+
588
+ ### Import Paths
589
+
590
+ ```typescript
591
+ // Core
592
+ import { PlayerManager, Player, Queue } from "ziplayer";
593
+
594
+ // Types
595
+ import type { Track, SearchResult, LoopMode, StreamInfo } from "ziplayer";
596
+
597
+ // Plugins (external package)
598
+ import { YouTubePlugin, SpotifyPlugin, TTSPlugin } from "@ziplayer/plugin";
599
+
600
+ // infinity plugin support stream audio from YouTube, TikTok, Instagram, Twitter/X, SoundCloud, Reddit, Twitch, Bilibili, and 1000+ other sites
601
+
602
+ import { InfinityPlugin } from "@ziplayer/infinity";
603
+
604
+ // Extensions (external package)
605
+ import { voiceExt, lyricsExt, lavalinkExt } from "@ziplayer/extension";
606
+
607
+ //fallback for youtube plugin if YouTubePlugin getStream error tunnel
608
+ import { YTexec } from "@ziplayer/ytexecplug"; //ytexecplug needs python, install python fist
609
+
610
+ const ytbplg = new YouTubePlugin({ fistStream: new YTexec().getStream });
611
+ ```
612
+
613
+ ### Type Definitions
614
+
615
+ ```typescript
616
+ interface Track {
617
+ id: string;
618
+ title: string;
619
+ url: string;
620
+ source: string;
621
+ duration: number;
622
+ thumbnail?: string;
623
+ requestedBy?: string;
624
+ isLive?: boolean;
625
+ }
626
+
627
+ type LoopMode = "off" | "track" | "queue";
628
+
629
+ interface SearchResult {
630
+ tracks: Track[];
631
+ playlist?: { name: string; url?: string };
632
+ }
633
+ ```
634
+
635
+ ## Terminology
636
+
637
+ - **Plugins** (`SourcePlugin`): search, `getStream`, playlists — audio **sources** (YouTube, SoundCloud, etc.).
638
+ - **Extensions** (`SourceExtension` / `BaseExtension`): cross-cutting behavior — **not** the same as **`trackMiddleware`**.
639
+
640
+ ## Track transforms before stream
641
+
642
+ | Goal | Mechanism |
643
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
644
+ | Enrich every queued track right before stream extraction (preload, playback, `save`, TTS interrupt path uses middleware before raw plugin stream where applicable) | **`trackMiddleware`** on `PlayerManagerOptions` / `PlayerOptions` — ordered chain; merged order: manager chain first, then per-player chain. Implemented in `Player.applyTrackMiddleware` → called at start of `Player.getStream`, and before direct `pluginManager.getStream` in TTS interrupt / `save`. |
645
+ | Change query or inject tracks before search resolves | Extension **`beforePlay`** — mutate `payload.query` or return `{ tracks }`. |
646
+ | Custom stream backend | Extension **`provideStream`** — runs **after** track middleware, before plugins. |
647
+
648
+ Types: `TrackMiddleware`, `TrackMiddlewareContext`, `normalizeTrackMiddleware` in `src/types/index.ts`.
649
+
650
+ When returning a **new** `Track` from middleware, the core **merges** into the original reference (`mergeTrackPreserveRef`) so
651
+ queue pointers stay valid.
652
+
653
+ ## Metadata: BPM, LUFS, genre
654
+
655
+ Advanced playback reads **`track.metadata`**:
656
+
657
+ - **`bpm`**, **`genre`**, **`lufs`** — smart transition + loudness (see `Player`).
658
+
659
+ ## Multi-guild broadcast
660
+
661
+ | API | Behavior |
662
+ | -------------------------------------------- | ----------------------------------------------------------------- |
663
+ | `broadcast(action, ...args)` | Sync fan-out of `player[action]` to **all** players. |
664
+ | `broadcastAsync(action, ...args)` | Same, but `Promise.allSettled` on return values (use for `play`). |
665
+ | `broadcastGuilds(guildIds, action, ...args)` | Subset of guilds. |
666
+
667
+ Followers must already exist (`create`). One mirror subscription per leader id replaces the previous.
668
+
669
+ ### Playback Mirror / Forward Mode
670
+
671
+ Ziplayer supports built-in multi-guild playback mirroring using shared audio forwarding. A leader player streams audio normally,
672
+ while followers directly subscribe to the leader's internal audioPlayer.
673
+
674
+ This allows multiple guilds to hear the exact same playback while using only:
675
+
676
+ - one stream
677
+ - one decoder
678
+ - one extractor pipeline
679
+
680
+ Resulting in extremely low CPU and bandwidth usage.
681
+
682
+ ```ts
683
+ const stopMirror = manager.subscribeForwardMirror({
684
+ leaderGuildId: "123",
685
+ followerGuildIds: ["456", "789"],
686
+ syncVolume: true,
687
+ });
688
+
689
+ // later
690
+ stopMirror();
691
+ ```
692
+
693
+ **Direct Player Subscription:**
694
+
695
+ Followers may also subscribe manually:
696
+
697
+ ````ts
698
+ const leader = manager.get("123");
699
+ const follower = manager.get("456");
700
+
701
+ follower.subscribeTo(leader);
702
+ //Unsubscribe:
703
+ //follower.unsubscribeForward();
704
+ ```
705
+
706
+ ---
707
+
708
+ ## 📖 Additional Resources
709
+
710
+ - [GitHub Repository](https://github.com/ZiProject/ZiPlayer)
711
+ - [npm Package](https://www.npmjs.com/package/ziplayer)
712
+ - [Examples Folder](https://github.com/ZiProject/ZiPlayer/tree/main/examples)
713
+
714
+ ---
715
+
716
+ _This guide is maintained for AI assistants and developers. For questions or contributions, please open an issue on GitHub._
717
+ ````