ziplayer 0.3.5 → 0.3.7
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/dist/plugins/index.d.ts +6 -15
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +214 -219
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.d.ts +2 -0
- package/dist/structures/FilterManager.d.ts.map +1 -1
- package/dist/structures/FilterManager.js +123 -58
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/Player.d.ts +7 -1
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +172 -80
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +4 -0
- package/dist/structures/Queue.js.map +1 -1
- package/dist/structures/StreamManager.d.ts +7 -0
- package/dist/structures/StreamManager.d.ts.map +1 -1
- package/dist/structures/StreamManager.js +23 -0
- package/dist/structures/StreamManager.js.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/index.ts +252 -262
- package/src/structures/FilterManager.ts +139 -62
- package/src/structures/Player.ts +192 -87
- package/src/structures/Queue.ts +5 -0
- package/src/structures/StreamManager.ts +583 -563
- package/src/types/index.ts +2 -0
|
@@ -1,563 +1,583 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
private
|
|
41
|
-
private
|
|
42
|
-
private
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
stream.on("
|
|
119
|
-
stream.on("
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
this.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
this.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
this.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
this.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
stream.removeListener("
|
|
283
|
-
stream.removeListener("
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
this.suppressPrematureCloseErrors.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
1
|
+
import { Readable } from "stream";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import type { Track } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface ManagedStream {
|
|
6
|
+
id: string;
|
|
7
|
+
stream: Readable;
|
|
8
|
+
track: Track;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
lastAccessed: number;
|
|
11
|
+
metadata: {
|
|
12
|
+
source: string;
|
|
13
|
+
isPreload: boolean;
|
|
14
|
+
isRemote: boolean;
|
|
15
|
+
priority: number;
|
|
16
|
+
};
|
|
17
|
+
listeners: {
|
|
18
|
+
error: (err: Error) => void;
|
|
19
|
+
close: () => void;
|
|
20
|
+
end: () => void;
|
|
21
|
+
drain?: () => void;
|
|
22
|
+
pause?: () => void;
|
|
23
|
+
resume?: () => void;
|
|
24
|
+
};
|
|
25
|
+
status: "active" | "paused" | "ended" | "error" | "destroyed";
|
|
26
|
+
byteCount: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StreamManagerOptions {
|
|
30
|
+
maxConcurrentStreams?: number;
|
|
31
|
+
streamTimeout?: number;
|
|
32
|
+
maxListenersPerStream?: number;
|
|
33
|
+
cleanupInterval?: number;
|
|
34
|
+
enableMetrics?: boolean;
|
|
35
|
+
autoDestroy?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class StreamManager extends EventEmitter {
|
|
39
|
+
private streams = new Map<string, ManagedStream>();
|
|
40
|
+
private suppressPrematureCloseErrors = new Set<string>();
|
|
41
|
+
private options: Required<StreamManagerOptions>;
|
|
42
|
+
private cleanupTimer: NodeJS.Timeout | null = null;
|
|
43
|
+
private metrics = {
|
|
44
|
+
totalStreamsCreated: 0,
|
|
45
|
+
totalStreamsDestroyed: 0,
|
|
46
|
+
activeStreams: 0,
|
|
47
|
+
totalErrors: 0,
|
|
48
|
+
totalBytesProcessed: 0,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
constructor(options: StreamManagerOptions = {}) {
|
|
52
|
+
super();
|
|
53
|
+
this.setMaxListeners(50);
|
|
54
|
+
|
|
55
|
+
this.options = {
|
|
56
|
+
maxConcurrentStreams: 20,
|
|
57
|
+
streamTimeout: 5 * 60 * 1000, // 5 minutes
|
|
58
|
+
maxListenersPerStream: 15,
|
|
59
|
+
cleanupInterval: 60000, // 1 minute
|
|
60
|
+
enableMetrics: true,
|
|
61
|
+
autoDestroy: true,
|
|
62
|
+
...options,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (this.options.cleanupInterval > 0) {
|
|
66
|
+
this.startCleanupInterval();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.debug("StreamManager initialized");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Register a new stream
|
|
74
|
+
*/
|
|
75
|
+
registerStream(stream: Readable, track: Track, metadata: Partial<ManagedStream["metadata"]> = {}): string {
|
|
76
|
+
for (const existing of this.streams.values()) {
|
|
77
|
+
if (existing.stream === stream) {
|
|
78
|
+
if (stream.destroyed || (stream as any).readable === false) {
|
|
79
|
+
this.debug(`Stream object is dead, removing stale entry: ${existing.id}`);
|
|
80
|
+
this.streams.delete(existing.id);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
existing.lastAccessed = Date.now();
|
|
84
|
+
existing.track = track;
|
|
85
|
+
existing.metadata = {
|
|
86
|
+
...existing.metadata,
|
|
87
|
+
source: track.source || existing.metadata.source || "unknown",
|
|
88
|
+
...metadata,
|
|
89
|
+
};
|
|
90
|
+
this.debug(`Stream already managed, reusing ID: ${existing.id}`);
|
|
91
|
+
return existing.id;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const streamId = this.generateStreamId(track);
|
|
96
|
+
|
|
97
|
+
// Check if stream already exists
|
|
98
|
+
if (this.streams.has(streamId)) {
|
|
99
|
+
this.debug(`Stream already exists for track: ${track.title}, destroying old one`);
|
|
100
|
+
this.unregisterStream(streamId, true);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check concurrent limit
|
|
104
|
+
while (this.streams.size >= this.options.maxConcurrentStreams) {
|
|
105
|
+
const evicted = this.evictOldestStream();
|
|
106
|
+
if (!evicted) break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Configure stream
|
|
110
|
+
if (stream.setMaxListeners) {
|
|
111
|
+
stream.setMaxListeners(this.options.maxListenersPerStream);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Create listeners
|
|
115
|
+
const listeners = this.createStreamListeners(streamId);
|
|
116
|
+
|
|
117
|
+
// Apply listeners
|
|
118
|
+
stream.on("error", listeners.error);
|
|
119
|
+
stream.on("close", listeners.close);
|
|
120
|
+
stream.on("end", listeners.end);
|
|
121
|
+
stream.on("pause", listeners.pause!);
|
|
122
|
+
stream.on("resume", listeners.resume!);
|
|
123
|
+
stream.on("drain", listeners.drain!);
|
|
124
|
+
|
|
125
|
+
// Create managed stream
|
|
126
|
+
const managedStream: ManagedStream = {
|
|
127
|
+
id: streamId,
|
|
128
|
+
stream,
|
|
129
|
+
track,
|
|
130
|
+
createdAt: Date.now(),
|
|
131
|
+
lastAccessed: Date.now(),
|
|
132
|
+
metadata: {
|
|
133
|
+
source: track.source || "unknown",
|
|
134
|
+
isPreload: metadata.isPreload || false,
|
|
135
|
+
priority: metadata.priority || 0,
|
|
136
|
+
isRemote: metadata.isRemote || false,
|
|
137
|
+
...metadata,
|
|
138
|
+
},
|
|
139
|
+
listeners,
|
|
140
|
+
status: "active",
|
|
141
|
+
byteCount: 0,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
this.streams.set(streamId, managedStream);
|
|
145
|
+
|
|
146
|
+
if (this.options.enableMetrics) {
|
|
147
|
+
this.metrics.totalStreamsCreated++;
|
|
148
|
+
this.metrics.activeStreams = this.streams.size;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Setup data counter
|
|
152
|
+
this.setupDataCounter(managedStream);
|
|
153
|
+
|
|
154
|
+
this.debug(`Stream registered: ${track.title} (ID: ${streamId}), Total: ${this.streams.size}`);
|
|
155
|
+
this.emit("streamRegistered", { streamId, track, metadata: managedStream.metadata });
|
|
156
|
+
|
|
157
|
+
return streamId;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create stream listeners
|
|
162
|
+
*/
|
|
163
|
+
private createStreamListeners(streamId: string): ManagedStream["listeners"] {
|
|
164
|
+
return {
|
|
165
|
+
error: (err: Error) => {
|
|
166
|
+
const isPrematureClose = err?.message?.toLowerCase().includes("premature close");
|
|
167
|
+
if (isPrematureClose && this.suppressPrematureCloseErrors.has(streamId)) {
|
|
168
|
+
this.debug(`Ignored expected premature close [${streamId}] during controlled destroy`);
|
|
169
|
+
this.suppressPrematureCloseErrors.delete(streamId);
|
|
170
|
+
this.unregisterStream(streamId, false);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.debug(`Stream error [${streamId}]:`, err);
|
|
175
|
+
if (this.options.enableMetrics) {
|
|
176
|
+
this.metrics.totalErrors++;
|
|
177
|
+
}
|
|
178
|
+
this.emit("streamError", { streamId, error: err });
|
|
179
|
+
this.unregisterStream(streamId, true);
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
close: () => {
|
|
183
|
+
this.debug(`Stream closed [${streamId}]`);
|
|
184
|
+
const managed = this.streams.get(streamId);
|
|
185
|
+
if (managed) {
|
|
186
|
+
managed.status = "ended";
|
|
187
|
+
}
|
|
188
|
+
this.emit("streamClose", { streamId });
|
|
189
|
+
this.unregisterStream(streamId, false);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
end: () => {
|
|
193
|
+
this.debug(`Stream ended [${streamId}]`);
|
|
194
|
+
const managed = this.streams.get(streamId);
|
|
195
|
+
if (managed) {
|
|
196
|
+
managed.status = "ended";
|
|
197
|
+
}
|
|
198
|
+
this.emit("streamEnd", { streamId });
|
|
199
|
+
this.unregisterStream(streamId, false);
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
pause: () => {
|
|
203
|
+
const managed = this.streams.get(streamId);
|
|
204
|
+
if (managed) {
|
|
205
|
+
managed.status = "paused";
|
|
206
|
+
this.emit("streamPaused", { streamId, track: managed.track });
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
resume: () => {
|
|
211
|
+
const managed = this.streams.get(streamId);
|
|
212
|
+
if (managed) {
|
|
213
|
+
managed.status = "active";
|
|
214
|
+
managed.lastAccessed = Date.now();
|
|
215
|
+
this.emit("streamResumed", { streamId, track: managed.track });
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
drain: () => {
|
|
220
|
+
const managed = this.streams.get(streamId);
|
|
221
|
+
if (managed) {
|
|
222
|
+
this.emit("streamDrained", { streamId, track: managed.track });
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Setup data counter for stream
|
|
230
|
+
*/
|
|
231
|
+
private setupDataCounter(managed: ManagedStream): void {
|
|
232
|
+
let dataListener: (chunk: Buffer) => void;
|
|
233
|
+
|
|
234
|
+
if (managed.stream.readable) {
|
|
235
|
+
dataListener = (chunk: Buffer) => {
|
|
236
|
+
managed.byteCount += chunk.length;
|
|
237
|
+
if (this.options.enableMetrics) {
|
|
238
|
+
this.metrics.totalBytesProcessed += chunk.length;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Emit progress every ~1MB
|
|
242
|
+
if (managed.byteCount % (1024 * 1024) < chunk.length) {
|
|
243
|
+
this.emit("streamProgress", {
|
|
244
|
+
streamId: managed.id,
|
|
245
|
+
track: managed.track,
|
|
246
|
+
bytes: managed.byteCount,
|
|
247
|
+
megabytes: Math.floor(managed.byteCount / (1024 * 1024)),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
managed.stream.on("data", dataListener);
|
|
253
|
+
|
|
254
|
+
// Store data listener for cleanup
|
|
255
|
+
(managed as any).dataListener = dataListener;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Unregister a stream
|
|
261
|
+
*/
|
|
262
|
+
unregisterStream(streamId: string, forceDestroy: boolean = true): boolean {
|
|
263
|
+
const managed = this.streams.get(streamId);
|
|
264
|
+
if (!managed) {
|
|
265
|
+
this.suppressPrematureCloseErrors.delete(streamId);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.debug(`Unregistering stream: ${managed.track.title} (${streamId})`);
|
|
270
|
+
|
|
271
|
+
// Remove data listener
|
|
272
|
+
const dataListener = (managed as any).dataListener;
|
|
273
|
+
if (dataListener && managed.stream) {
|
|
274
|
+
managed.stream.removeListener("data", dataListener);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Remove all listeners
|
|
278
|
+
const { listeners } = managed;
|
|
279
|
+
const stream = managed.stream;
|
|
280
|
+
|
|
281
|
+
if (stream) {
|
|
282
|
+
stream.removeListener("error", listeners.error);
|
|
283
|
+
stream.removeListener("close", listeners.close);
|
|
284
|
+
stream.removeListener("end", listeners.end);
|
|
285
|
+
stream.removeListener("pause", listeners.pause!);
|
|
286
|
+
stream.removeListener("resume", listeners.resume!);
|
|
287
|
+
stream.removeListener("drain", listeners.drain!);
|
|
288
|
+
|
|
289
|
+
// Force destroy if needed
|
|
290
|
+
if (forceDestroy && !stream.destroyed && typeof stream.destroy === "function") {
|
|
291
|
+
try {
|
|
292
|
+
this.suppressPrematureCloseErrors.add(streamId);
|
|
293
|
+
stream.destroy();
|
|
294
|
+
managed.status = "destroyed";
|
|
295
|
+
} catch (err) {
|
|
296
|
+
this.suppressPrematureCloseErrors.delete(streamId);
|
|
297
|
+
this.debug(`Error destroying stream:`, err);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
this.streams.delete(streamId);
|
|
303
|
+
|
|
304
|
+
if (this.options.enableMetrics) {
|
|
305
|
+
this.metrics.totalStreamsDestroyed++;
|
|
306
|
+
this.metrics.activeStreams = this.streams.size;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.emit("streamUnregistered", { streamId, track: managed.track, reason: forceDestroy ? "destroyed" : "natural" });
|
|
310
|
+
this.suppressPrematureCloseErrors.delete(streamId);
|
|
311
|
+
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get a stream by ID
|
|
317
|
+
*/
|
|
318
|
+
getStream(streamId: string): Readable | null {
|
|
319
|
+
const managed = this.streams.get(streamId);
|
|
320
|
+
if (managed && managed.status === "active") {
|
|
321
|
+
managed.lastAccessed = Date.now();
|
|
322
|
+
return managed.stream;
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Like getStream() but accepts "paused" streams too.
|
|
329
|
+
* Used by refreshPlayerResource to reuse a source stream during seek.
|
|
330
|
+
* discordjs/voice pauses source streams on NoSubscriberBehavior which would
|
|
331
|
+
* make getStream() return null and force an unnecessary network fetch.
|
|
332
|
+
*/
|
|
333
|
+
getRawStream(streamId: string): Readable | null {
|
|
334
|
+
const managed = this.streams.get(streamId);
|
|
335
|
+
if (!managed) return null;
|
|
336
|
+
// Only reject truly terminal states.
|
|
337
|
+
if (managed.status === "destroyed" || managed.status === "ended" || managed.status === "error") return null;
|
|
338
|
+
if (managed.stream.destroyed) return null;
|
|
339
|
+
managed.lastAccessed = Date.now();
|
|
340
|
+
return managed.stream;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Update stream metadata
|
|
345
|
+
*/
|
|
346
|
+
updateMetadata(streamId: string, metadata: Partial<ManagedStream["metadata"]>): boolean {
|
|
347
|
+
const managed = this.streams.get(streamId);
|
|
348
|
+
if (managed) {
|
|
349
|
+
managed.metadata = { ...managed.metadata, ...metadata };
|
|
350
|
+
managed.lastAccessed = Date.now();
|
|
351
|
+
this.emit("streamMetadataUpdated", { streamId, metadata });
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Pause a stream
|
|
359
|
+
*/
|
|
360
|
+
pauseStream(streamId: string): boolean {
|
|
361
|
+
const managed = this.streams.get(streamId);
|
|
362
|
+
if (managed && managed.status === "active" && !managed.stream.isPaused()) {
|
|
363
|
+
managed.stream.pause();
|
|
364
|
+
managed.status = "paused";
|
|
365
|
+
this.emit("streamPaused", { streamId, track: managed.track });
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Resume a stream
|
|
373
|
+
*/
|
|
374
|
+
resumeStream(streamId: string): boolean {
|
|
375
|
+
const managed = this.streams.get(streamId);
|
|
376
|
+
if (managed && managed.status === "paused") {
|
|
377
|
+
managed.stream.resume();
|
|
378
|
+
managed.status = "active";
|
|
379
|
+
managed.lastAccessed = Date.now();
|
|
380
|
+
this.emit("streamResumed", { streamId, track: managed.track });
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Evict oldest stream when limit reached
|
|
388
|
+
*/
|
|
389
|
+
private evictOldestStream(): boolean {
|
|
390
|
+
// Evict lowest priority streams first
|
|
391
|
+
const sorted = Array.from(this.streams.values()).sort((a, b) => a.metadata.priority - b.metadata.priority);
|
|
392
|
+
|
|
393
|
+
for (const managed of sorted) {
|
|
394
|
+
if (managed.metadata.isPreload && managed.metadata.priority < 5) {
|
|
395
|
+
this.debug(`Evicting low priority preload stream: ${managed.track.title}`);
|
|
396
|
+
this.unregisterStream(managed.id, true);
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (sorted.length > 0) {
|
|
402
|
+
const fallback = sorted[0];
|
|
403
|
+
this.debug(`Evicting fallback stream to enforce limit: ${fallback.track.title}`);
|
|
404
|
+
this.unregisterStream(fallback.id, true);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Cleanup expired streams
|
|
413
|
+
*/
|
|
414
|
+
private cleanupExpiredStreams(): void {
|
|
415
|
+
const now = Date.now();
|
|
416
|
+
let cleaned = 0;
|
|
417
|
+
|
|
418
|
+
for (const [streamId, managed] of this.streams) {
|
|
419
|
+
const age = now - managed.lastAccessed;
|
|
420
|
+
|
|
421
|
+
if (age > this.options.streamTimeout) {
|
|
422
|
+
this.debug(`Cleaning up expired stream: ${managed.track.title} (age: ${age}ms)`);
|
|
423
|
+
this.unregisterStream(streamId, this.options.autoDestroy);
|
|
424
|
+
cleaned++;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (cleaned > 0) {
|
|
429
|
+
this.emit("cleanupCompleted", { cleaned, remaining: this.streams.size });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Start automatic cleanup interval
|
|
435
|
+
*/
|
|
436
|
+
private startCleanupInterval(): void {
|
|
437
|
+
if (this.cleanupTimer) {
|
|
438
|
+
clearInterval(this.cleanupTimer);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this.cleanupTimer = setInterval(() => {
|
|
442
|
+
this.cleanupExpiredStreams();
|
|
443
|
+
}, this.options.cleanupInterval);
|
|
444
|
+
|
|
445
|
+
this.cleanupTimer.unref(); // Don't keep process alive
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Stop cleanup interval
|
|
450
|
+
*/
|
|
451
|
+
stopCleanupInterval(): void {
|
|
452
|
+
if (this.cleanupTimer) {
|
|
453
|
+
clearInterval(this.cleanupTimer);
|
|
454
|
+
this.cleanupTimer = null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get all active streams
|
|
460
|
+
*/
|
|
461
|
+
getAllStreams(): ManagedStream[] {
|
|
462
|
+
return Array.from(this.streams.values());
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Get streams by status
|
|
467
|
+
*/
|
|
468
|
+
getStreamsByStatus(status: ManagedStream["status"]): ManagedStream[] {
|
|
469
|
+
return Array.from(this.streams.values()).filter((s) => s.status === status);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get stream by track ID (using track.id, track.url, or track.title as identifier)
|
|
474
|
+
*/
|
|
475
|
+
getStreamByTrack(trackId: string): Readable | null {
|
|
476
|
+
for (const managed of this.streams.values()) {
|
|
477
|
+
const managedTrackId = managed.track.id || managed.track.url || managed.track.title;
|
|
478
|
+
if (managedTrackId === trackId && managed.status === "active") {
|
|
479
|
+
managed.lastAccessed = Date.now();
|
|
480
|
+
return managed.stream;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Check if a stream exists for a given track ID
|
|
488
|
+
*/
|
|
489
|
+
hasStream(trackId: string): boolean {
|
|
490
|
+
for (const managed of this.streams.values()) {
|
|
491
|
+
const managedTrackId = managed.track.id || managed.track.url || managed.track.title;
|
|
492
|
+
if (managedTrackId === trackId && managed.status === "active") {
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Get stream count
|
|
500
|
+
*/
|
|
501
|
+
getStreamCount(): number {
|
|
502
|
+
return this.streams.size;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Get metrics
|
|
507
|
+
*/
|
|
508
|
+
getMetrics(): typeof this.metrics {
|
|
509
|
+
if (!this.options.enableMetrics) {
|
|
510
|
+
return {
|
|
511
|
+
totalStreamsCreated: 0,
|
|
512
|
+
totalStreamsDestroyed: 0,
|
|
513
|
+
activeStreams: 0,
|
|
514
|
+
totalErrors: 0,
|
|
515
|
+
totalBytesProcessed: 0,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return { ...this.metrics };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Get statistics
|
|
523
|
+
*/
|
|
524
|
+
getStats(): {
|
|
525
|
+
active: number;
|
|
526
|
+
paused: number;
|
|
527
|
+
ended: number;
|
|
528
|
+
error: number;
|
|
529
|
+
destroyed: number;
|
|
530
|
+
total: number;
|
|
531
|
+
bySource: Record<string, number>;
|
|
532
|
+
} {
|
|
533
|
+
const stats = {
|
|
534
|
+
active: 0,
|
|
535
|
+
paused: 0,
|
|
536
|
+
ended: 0,
|
|
537
|
+
error: 0,
|
|
538
|
+
destroyed: 0,
|
|
539
|
+
total: 0,
|
|
540
|
+
bySource: {} as Record<string, number>,
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
for (const managed of this.streams.values()) {
|
|
544
|
+
stats[managed.status]++;
|
|
545
|
+
stats.total++;
|
|
546
|
+
|
|
547
|
+
const source = managed.metadata.source;
|
|
548
|
+
stats.bySource[source] = (stats.bySource[source] || 0) + 1;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return stats;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Destroy all streams
|
|
556
|
+
*/
|
|
557
|
+
destroyAll(force: boolean = true): void {
|
|
558
|
+
this.debug(`Destroying all streams (${this.streams.size})`);
|
|
559
|
+
|
|
560
|
+
for (const streamId of Array.from(this.streams.keys())) {
|
|
561
|
+
this.unregisterStream(streamId, force);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
this.stopCleanupInterval();
|
|
565
|
+
this.emit("destroyed", { totalDestroyed: this.metrics.totalStreamsDestroyed });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Generate unique stream ID
|
|
570
|
+
*/
|
|
571
|
+
private generateStreamId(track: Track): string {
|
|
572
|
+
return `${track.source || "unknown"}:${track.id || track.url || track.title}:${Date.now()}:${Math.random().toString(36).substr(2, 9)}`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Debug logging
|
|
577
|
+
*/
|
|
578
|
+
private debug(message: string, ...args: any[]): void {
|
|
579
|
+
if (this.listenerCount("debug") > 0) {
|
|
580
|
+
this.emit("debug", `[StreamManager] ${message}`, ...args);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|