enerthya.dev-audio-server 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enerthya.dev-audio-server",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Enerthya Audio Server — Lavalink-compatible WebSocket audio server in Node.js. Uses enerthya.dev-audio-core for stream processing. Runs on port 80 (Shardcloud compatible).",
5
5
  "main": "src/index.js",
6
6
  "license": "MIT",
@@ -15,7 +15,7 @@
15
15
  "websocket"
16
16
  ],
17
17
  "dependencies": {
18
- "enerthya.dev-audio-core": "1.0.8",
18
+ "enerthya.dev-audio-core": "1.3.0",
19
19
  "ws": "^8.18.0"
20
20
  },
21
21
  "scripts": {
@@ -1,14 +1,9 @@
1
1
  /**
2
2
  * server/AudioServer.js — Servidor WebSocket de áudio
3
- *
4
- * Equivalente ao servidor Lavalink em Java, mas em Node.js.
5
- * Aceita conexões WebSocket de bots Discord autenticados por senha.
6
- * Cada conexão representa um bot (identificado pelo userId do bot).
7
- *
8
- * Protocolo:
9
- * - Autenticação via header "Authorization" na abertura do WS
10
- * - Mensagens JSON com campo "op" definindo a operação
11
- * - Compatível com o protocolo do Lavalink v4
3
+ *
4
+ * Protocolo binário para audio frames:
5
+ * [4 bytes uint32BE = tamanho do guildId] [N bytes guildId UTF-8] [resto = Ogg/Opus]
6
+ * Isso garante que o bot saiba de qual guild é cada chunk, com múltiplas guilds simultâneas.
12
7
  */
13
8
 
14
9
  const { WebSocketServer, WebSocket } = require("ws");
@@ -18,66 +13,36 @@ const { Op, TrackEvent, EndReason, PlayerState, Defaults } = require("../constan
18
13
  const { loadItem } = require("enerthya.dev-audio-core");
19
14
 
20
15
  class AudioServer {
21
- /**
22
- * @param {object} opts
23
- * @param {string} [opts.password] — senha de autenticação (padrão: "enerthya")
24
- * @param {http.Server} [opts.httpServer] — servidor HTTP existente para attach (porta 80)
25
- * @param {number} [opts.port] — porta standalone (padrão: 2333)
26
- * @param {Function} [opts.logger] — função de log (padrão: console.log)
27
- */
28
16
  constructor(opts = {}) {
29
- this.password = opts.password || process.env.AUDIO_PASSWORD || Defaults.PASSWORD;
30
- this.logger = opts.logger || console.log;
17
+ this.password = opts.password || process.env.AUDIO_PASSWORD || Defaults.PASSWORD;
18
+ this.logger = opts.logger || console.log;
19
+ this.sessions = new Map();
31
20
 
32
- // sessions: sessionId → { ws, userId, players: Map<guildId, GuildPlayer> }
33
- this.sessions = new Map();
34
-
35
- // Cria o WebSocket server
36
21
  if (opts.httpServer) {
37
- // Attach no servidor HTTP existente (porta 80 da dashboard)
38
22
  this.wss = new WebSocketServer({ server: opts.httpServer, path: "/audio" });
39
23
  } else {
40
- // Standalone
41
24
  const port = opts.port || Defaults.PORT;
42
25
  this.wss = new WebSocketServer({ port, path: "/audio" });
43
- this.logger(`[AudioServer] Rodando standalone na porta ${port}`);
26
+ this.logger(`[AudioServer] Rodando na porta ${port}`);
44
27
  }
45
28
 
46
29
  this._setupWss();
47
30
  this._startStatsInterval();
48
31
  }
49
32
 
50
- // ── Setup WebSocket ────────────────────────────────────────────────────────
51
-
52
33
  _setupWss() {
53
34
  this.wss.on("connection", (ws, req) => {
54
- // Autenticação via header Authorization
55
35
  const auth = req.headers["authorization"];
56
- if (auth !== this.password) {
57
- ws.close(4001, "Unauthorized");
58
- return;
59
- }
36
+ if (auth !== this.password) { ws.close(4001, "Unauthorized"); return; }
60
37
 
61
38
  const userId = req.headers["user-id"] || "unknown";
62
39
  const sessionId = crypto.randomBytes(8).toString("hex");
63
-
64
- const session = {
65
- ws,
66
- userId,
67
- sessionId,
68
- players: new Map(),
69
- connectedAt: Date.now(),
70
- };
40
+ const session = { ws, userId, sessionId, players: new Map(), connectedAt: Date.now() };
71
41
 
72
42
  this.sessions.set(sessionId, session);
73
- this.logger(`[AudioServer] Conexão estabelecida — userId: ${userId} sessionId: ${sessionId}`);
43
+ this.logger(`[AudioServer] Conexão — userId: ${userId} session: ${sessionId}`);
74
44
 
75
- // Envia READY
76
- this._send(ws, {
77
- op: Op.READY,
78
- resumed: false,
79
- sessionId,
80
- });
45
+ this._send(ws, { op: Op.READY, resumed: false, sessionId });
81
46
 
82
47
  ws.on("message", (data) => {
83
48
  try {
@@ -89,65 +54,44 @@ class AudioServer {
89
54
  });
90
55
 
91
56
  ws.on("close", () => {
92
- this.logger(`[AudioServer] Conexão encerrada sessionId: ${sessionId}`);
93
- // Destroi todos os players desta sessão
94
- for (const player of session.players.values()) {
95
- player.destroy();
96
- }
57
+ this.logger(`[AudioServer] Encerradosession: ${sessionId}`);
58
+ for (const player of session.players.values()) player.destroy();
97
59
  this.sessions.delete(sessionId);
98
60
  });
99
61
 
100
- ws.on("error", (err) => {
101
- this.logger(`[AudioServer] Erro WS — ${err.message}`);
102
- });
62
+ ws.on("error", (err) => this.logger(`[AudioServer] Erro WS: ${err.message}`));
103
63
  });
104
64
  }
105
65
 
106
- // ── Handler de mensagens ───────────────────────────────────────────────────
107
-
108
66
  async _handleMessage(session, msg) {
109
67
  const { op, guildId } = msg;
110
68
  if (!op) return;
111
69
 
112
70
  switch (op) {
113
-
114
71
  case Op.LOAD: {
115
- const result = await loadItem(msg.query, {
116
- limit: msg.limit || 1,
117
- requesterId: session.userId,
118
- }).catch((err) => ({ loadType: "error", tracks: [], error: err.message }));
72
+ const result = await loadItem(msg.query, { limit: msg.limit || 1, requesterId: session.userId })
73
+ .catch((err) => ({ loadType: "error", tracks: [], error: err.message }));
119
74
  this._send(session.ws, { op: "loadResult", guildId, result });
120
75
  break;
121
76
  }
122
-
123
77
  case Op.SEARCH: {
124
- const result = await loadItem(msg.query, {
125
- limit: msg.limit || 5,
126
- requesterId: session.userId,
127
- }).catch(() => ({ loadType: "empty", tracks: [] }));
78
+ const result = await loadItem(msg.query, { limit: msg.limit || 5, requesterId: session.userId })
79
+ .catch(() => ({ loadType: "empty", tracks: [] }));
128
80
  this._send(session.ws, { op: "searchResult", guildId, result });
129
81
  break;
130
82
  }
131
-
132
83
  case Op.PLAY: {
133
84
  if (!guildId || !msg.track) return;
134
85
  const player = this._getOrCreatePlayer(session, guildId);
135
-
136
- if (msg.noReplace && player.state === PlayerState.PLAYING) return;
137
-
138
- // Adiciona à fila ou toca imediatamente
139
86
  if (msg.addToQueue && (player.state === PlayerState.PLAYING || player.queue.length > 0)) {
140
87
  player.addToQueue(msg.track);
141
88
  this._send(session.ws, { op: "queueUpdated", guildId, queue: player.queue });
142
89
  } else {
143
- if (msg.track) {
144
- player.clearQueue();
145
- await player.play(msg.track, { startTime: msg.startTime || 0 });
146
- }
90
+ player.clearQueue();
91
+ await player.play(msg.track, { startTime: msg.startTime || 0 });
147
92
  }
148
93
  break;
149
94
  }
150
-
151
95
  case Op.QUEUE_ADD: {
152
96
  if (!guildId || !msg.track) return;
153
97
  const player = this._getOrCreatePlayer(session, guildId);
@@ -155,42 +99,33 @@ class AudioServer {
155
99
  this._send(session.ws, { op: "queueUpdated", guildId, queue: player.queue });
156
100
  break;
157
101
  }
158
-
159
102
  case Op.STOP: {
160
103
  const player = session.players.get(guildId);
161
104
  if (player) player.stop();
162
105
  break;
163
106
  }
164
-
165
107
  case Op.PAUSE: {
166
108
  const player = session.players.get(guildId);
167
109
  if (!player) return;
168
110
  msg.pause ? player.pause() : player.resume();
169
- this._send(session.ws, {
170
- op: Op.PLAYER_UPDATE, guildId,
171
- state: { position: player.getPosition(), paused: player.paused },
172
- });
111
+ this._send(session.ws, { op: Op.PLAYER_UPDATE, guildId, state: { position: player.getPosition(), paused: player.paused } });
173
112
  break;
174
113
  }
175
-
176
114
  case Op.SEEK: {
177
115
  const player = session.players.get(guildId);
178
116
  if (player && msg.position !== undefined) player.seek(msg.position);
179
117
  break;
180
118
  }
181
-
182
119
  case Op.VOLUME: {
183
120
  const player = session.players.get(guildId);
184
121
  if (player && msg.volume !== undefined) player.setVolume(msg.volume);
185
122
  break;
186
123
  }
187
-
188
124
  case Op.SKIP: {
189
125
  const player = session.players.get(guildId);
190
126
  if (player) player.skip();
191
127
  break;
192
128
  }
193
-
194
129
  case Op.QUEUE_REMOVE: {
195
130
  const player = session.players.get(guildId);
196
131
  if (player && msg.index !== undefined) {
@@ -199,145 +134,84 @@ class AudioServer {
199
134
  }
200
135
  break;
201
136
  }
202
-
203
137
  case Op.QUEUE_CLEAR: {
204
138
  const player = session.players.get(guildId);
205
- if (player) {
206
- player.clearQueue();
207
- this._send(session.ws, { op: "queueUpdated", guildId, queue: [] });
208
- }
139
+ if (player) { player.clearQueue(); this._send(session.ws, { op: "queueUpdated", guildId, queue: [] }); }
209
140
  break;
210
141
  }
211
-
212
142
  case Op.QUEUE_SHUFFLE: {
213
143
  const player = session.players.get(guildId);
214
- if (player) {
215
- player.shuffleQueue();
216
- this._send(session.ws, { op: "queueUpdated", guildId, queue: player.queue });
217
- }
144
+ if (player) { player.shuffleQueue(); this._send(session.ws, { op: "queueUpdated", guildId, queue: player.queue }); }
218
145
  break;
219
146
  }
220
-
221
147
  case Op.QUEUE_GET: {
222
148
  const player = session.players.get(guildId);
223
- this._send(session.ws, {
224
- op: "queueResult",
225
- guildId,
226
- player: player ? player.toJSON() : null,
227
- });
149
+ this._send(session.ws, { op: "queueResult", guildId, player: player ? player.toJSON() : null });
150
+ break;
151
+ }
152
+ case "repeat": {
153
+ const player = session.players.get(guildId);
154
+ if (player && msg.mode) player.setRepeat(msg.mode);
228
155
  break;
229
156
  }
230
-
231
157
  case Op.DESTROY: {
232
158
  const player = session.players.get(guildId);
233
- if (player) {
234
- player.destroy();
235
- session.players.delete(guildId);
236
- }
159
+ if (player) { player.destroy(); session.players.delete(guildId); }
237
160
  break;
238
161
  }
239
162
  }
240
163
  }
241
164
 
242
- // ── Player helpers ─────────────────────────────────────────────────────────
243
-
244
165
  _getOrCreatePlayer(session, guildId) {
245
166
  if (session.players.has(guildId)) return session.players.get(guildId);
246
167
 
247
168
  const player = new GuildPlayer(guildId, this);
248
169
 
249
- // Eventos do player mensagens WS para o client
250
- player.on("trackStart", (track) => {
251
- this._send(session.ws, {
252
- op: Op.EVENT,
253
- type: TrackEvent.START,
254
- guildId,
255
- track,
256
- });
257
- });
258
-
259
- player.on("trackEnd", (track, reason) => {
260
- this._send(session.ws, {
261
- op: Op.EVENT,
262
- type: TrackEvent.END,
263
- guildId,
264
- track,
265
- reason,
266
- });
267
- });
170
+ player.on("trackStart", (track) => this._send(session.ws, { op: Op.EVENT, type: TrackEvent.START, guildId, track }));
171
+ player.on("trackEnd", (track, reason) => this._send(session.ws, { op: Op.EVENT, type: TrackEvent.END, guildId, track, reason }));
172
+ player.on("trackException", (track, error) => this._send(session.ws, { op: Op.EVENT, type: TrackEvent.EXCEPTION, guildId, track, exception: { message: error } }));
173
+ player.on("playerUpdate", ({ position }) => this._send(session.ws, { op: Op.PLAYER_UPDATE, guildId, state: { time: Date.now(), position, connected: true } }));
268
174
 
269
- player.on("trackException", (track, error) => {
270
- this._send(session.ws, {
271
- op: Op.EVENT,
272
- type: TrackEvent.EXCEPTION,
273
- guildId,
274
- track,
275
- exception: { message: error, severity: "common" },
276
- });
277
- });
278
-
279
- player.on("playerUpdate", ({ position }) => {
280
- this._send(session.ws, {
281
- op: Op.PLAYER_UPDATE,
282
- guildId,
283
- state: { time: Date.now(), position, connected: true },
284
- });
285
- });
286
-
287
- // Stream de áudio PCM → enviado via WS para o bot tocar com @discordjs/voice
175
+ // ── Protocolo binário: [4 bytes guildId len] [guildId UTF-8] [Ogg/Opus data] ──
288
176
  player.on("audioData", (chunk) => {
289
- if (session.ws.readyState === WebSocket.OPEN) {
290
- session.ws.send(chunk); // Buffer binário — bot identifica pelo guildId atual
291
- }
177
+ if (session.ws.readyState !== WebSocket.OPEN) return;
178
+ const guildBuf = Buffer.from(guildId, "utf8");
179
+ const header = Buffer.allocUnsafe(4);
180
+ header.writeUInt32BE(guildBuf.length, 0);
181
+ session.ws.send(Buffer.concat([header, guildBuf, chunk]));
292
182
  });
293
183
 
294
184
  session.players.set(guildId, player);
295
185
  return player;
296
186
  }
297
187
 
298
- // ── Stats ──────────────────────────────────────────────────────────────────
299
-
300
188
  _startStatsInterval() {
301
- const interval = setInterval(() => {
189
+ const iv = setInterval(() => {
302
190
  const stats = this._getStats();
303
- for (const { ws } of this.sessions.values()) {
304
- this._send(ws, { op: Op.STATS, ...stats });
305
- }
191
+ for (const { ws } of this.sessions.values()) this._send(ws, { op: Op.STATS, ...stats });
306
192
  }, Defaults.STATS_INTERVAL_MS);
307
- if (interval.unref) interval.unref();
193
+ if (iv.unref) iv.unref();
308
194
  }
309
195
 
310
196
  _getStats() {
311
197
  const mem = process.memoryUsage();
312
198
  let players = 0, playingPlayers = 0;
313
- for (const session of this.sessions.values()) {
314
- players += session.players.size;
315
- for (const p of session.players.values()) {
316
- if (p.state === PlayerState.PLAYING) playingPlayers++;
317
- }
199
+ for (const s of this.sessions.values()) {
200
+ players += s.players.size;
201
+ for (const p of s.players.values()) if (p.state === PlayerState.PLAYING) playingPlayers++;
318
202
  }
319
203
  return {
320
204
  players, playingPlayers,
321
- memory: {
322
- free: Math.round((require("os").freemem()) / 1024 / 1024),
323
- used: Math.round(mem.heapUsed / 1024 / 1024),
324
- allocated: Math.round(mem.heapTotal / 1024 / 1024),
325
- reservable: Math.round(require("os").totalmem() / 1024 / 1024),
326
- },
205
+ memory: { used: Math.round(mem.heapUsed/1024/1024), allocated: Math.round(mem.heapTotal/1024/1024) },
327
206
  cpu: { cores: require("os").cpus().length, systemLoad: require("os").loadavg()[0] },
328
207
  uptime: Math.floor(process.uptime()),
329
208
  };
330
209
  }
331
210
 
332
- // ── Utils ──────────────────────────────────────────────────────────────────
333
-
334
211
  _send(ws, payload) {
335
- if (ws && ws.readyState === WebSocket.OPEN) {
336
- ws.send(JSON.stringify(payload));
337
- }
212
+ if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify(payload));
338
213
  }
339
214
 
340
- /** Retorna estatísticas do servidor para uso externo */
341
215
  getStats() { return this._getStats(); }
342
216
  }
343
217