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