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 +2 -2
- package/src/server/AudioServer.js +49 -175
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enerthya.dev-audio-server",
|
|
3
|
-
"version": "1.0
|
|
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
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
30
|
-
this.logger
|
|
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
|
|
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
|
|
43
|
+
this.logger(`[AudioServer] Conexão — userId: ${userId} session: ${sessionId}`);
|
|
74
44
|
|
|
75
|
-
|
|
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]
|
|
93
|
-
|
|
94
|
-
for (const player of session.players.values()) {
|
|
95
|
-
player.destroy();
|
|
96
|
-
}
|
|
57
|
+
this.logger(`[AudioServer] Encerrado — session: ${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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
250
|
-
player.on("
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
|
290
|
-
|
|
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
|
|
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 (
|
|
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
|
|
314
|
-
players +=
|
|
315
|
-
for (const p of
|
|
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
|
|
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
|
|