ziplayer 0.3.7 → 0.3.8
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 +1 -8
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +52 -106
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.d.ts +7 -24
- package/dist/structures/FilterManager.d.ts.map +1 -1
- package/dist/structures/FilterManager.js +123 -99
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/Player.d.ts +1 -0
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +99 -91
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PreloadManager.d.ts +1 -0
- package/dist/structures/PreloadManager.d.ts.map +1 -1
- package/dist/structures/PreloadManager.js +26 -6
- package/dist/structures/PreloadManager.js.map +1 -1
- package/dist/structures/StreamManager.d.ts +1 -0
- package/dist/structures/StreamManager.d.ts.map +1 -1
- package/dist/structures/StreamManager.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/index.ts +63 -119
- package/src/structures/FilterManager.ts +439 -380
- package/src/structures/Player.ts +120 -97
- package/src/structures/PreloadManager.ts +293 -274
- package/src/structures/StreamManager.ts +2 -0
|
@@ -1,380 +1,439 @@
|
|
|
1
|
-
import type { AudioFilter } from "../types";
|
|
2
|
-
import { PREDEFINED_FILTERS } from "../types";
|
|
3
|
-
import type { Player } from "./Player";
|
|
4
|
-
import type { PlayerManager } from "./PlayerManager";
|
|
5
|
-
import prism, { FFmpeg } from "prism-media";
|
|
6
|
-
import type { Readable } from "stream";
|
|
7
|
-
import { spawn, type ChildProcess } from "child_process";
|
|
8
|
-
import ffmpegPath from "ffmpeg-static";
|
|
9
|
-
|
|
10
|
-
type DebugFn = (message?: any, ...optionalParams: any[]) => void;
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
*
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
* player
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
this.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
this.
|
|
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
|
-
this.StreamType === "
|
|
343
|
-
:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
this.ffmpegProcess
|
|
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
|
-
}
|
|
1
|
+
import type { AudioFilter } from "../types";
|
|
2
|
+
import { PREDEFINED_FILTERS } from "../types";
|
|
3
|
+
import type { Player } from "./Player";
|
|
4
|
+
import type { PlayerManager } from "./PlayerManager";
|
|
5
|
+
import prism, { FFmpeg } from "prism-media";
|
|
6
|
+
import type { Readable } from "stream";
|
|
7
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
8
|
+
import ffmpegPath from "ffmpeg-static";
|
|
9
|
+
|
|
10
|
+
type DebugFn = (message?: any, ...optionalParams: any[]) => void;
|
|
11
|
+
|
|
12
|
+
export type FilterManagerStreamType = "webm/opus" | "ogg/opus" | "arbitrary" | "mp3";
|
|
13
|
+
|
|
14
|
+
export class FilterManager {
|
|
15
|
+
private activeFilters: AudioFilter[] = [];
|
|
16
|
+
private debug: DebugFn;
|
|
17
|
+
private player: Player;
|
|
18
|
+
private ffmpeg: FFmpeg | null = null;
|
|
19
|
+
private currentInputStream: Readable | null = null;
|
|
20
|
+
public StreamType: FilterManagerStreamType = "arbitrary";
|
|
21
|
+
private ffmpegProcess: ChildProcess | null = null;
|
|
22
|
+
private ffmpegAbortController: AbortController | null = null;
|
|
23
|
+
private ffmpegGeneration = 0;
|
|
24
|
+
private pendingFFmpegProcess: ChildProcess | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(player: Player, manager: PlayerManager) {
|
|
27
|
+
this.player = player as Player;
|
|
28
|
+
this.debug = (message?: any, ...optionalParams: any[]) => {
|
|
29
|
+
if (manager.debugEnabled) {
|
|
30
|
+
manager.emit("debug", `[FilterManager] ${message}`, ...optionalParams);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public setSourceStreamType(type: string): void {
|
|
36
|
+
if (type === "webm/opus" || type === "ogg/opus" || type === "mp3") {
|
|
37
|
+
this.StreamType = type as FilterManagerStreamType;
|
|
38
|
+
} else {
|
|
39
|
+
this.StreamType = "arbitrary";
|
|
40
|
+
}
|
|
41
|
+
this.debug(`[FilterManager] Source stream type set to: ${this.StreamType}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
destroy(): void {
|
|
45
|
+
this.activeFilters = [];
|
|
46
|
+
this.teardownFFmpeg();
|
|
47
|
+
this.currentInputStream = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private teardownFFmpeg(): void {
|
|
51
|
+
// Abort any pending spawn first
|
|
52
|
+
if (this.ffmpegAbortController) {
|
|
53
|
+
this.ffmpegAbortController.abort();
|
|
54
|
+
this.ffmpegAbortController = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.ffmpeg) {
|
|
58
|
+
try {
|
|
59
|
+
this.ffmpeg.destroy();
|
|
60
|
+
} catch {
|
|
61
|
+
/* ignore */
|
|
62
|
+
}
|
|
63
|
+
this.ffmpeg = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (this.ffmpegProcess) {
|
|
67
|
+
try {
|
|
68
|
+
// Detach stdin so source stream doesn't get EPIPE when we kill
|
|
69
|
+
if (this.ffmpegProcess.stdin && !this.ffmpegProcess.stdin.destroyed) {
|
|
70
|
+
this.ffmpegProcess.stdin.destroy();
|
|
71
|
+
}
|
|
72
|
+
this.ffmpegProcess.kill("SIGKILL");
|
|
73
|
+
} catch {
|
|
74
|
+
/* ignore */
|
|
75
|
+
}
|
|
76
|
+
this.ffmpegProcess = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public getFilterString(): string {
|
|
81
|
+
if (this.activeFilters.length === 0) return "";
|
|
82
|
+
return this.activeFilters.map((f) => f.ffmpegFilter).join(",");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all currently applied filters
|
|
87
|
+
*
|
|
88
|
+
* @returns {AudioFilter[]} Array of active filters
|
|
89
|
+
* @example
|
|
90
|
+
* const filters = player.getActiveFilters();
|
|
91
|
+
* console.log(`Active filters: ${filters.map(f => f.name).join(', ')}`);
|
|
92
|
+
*/
|
|
93
|
+
public getActiveFilters(): AudioFilter[] {
|
|
94
|
+
return [...this.activeFilters];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a specific filter is currently applied
|
|
99
|
+
*
|
|
100
|
+
* @param {string} filterName - Name of the filter to check
|
|
101
|
+
* @returns {boolean} True if filter is applied
|
|
102
|
+
* @example
|
|
103
|
+
* const hasBassBoost = player.hasFilter("bassboost");
|
|
104
|
+
* console.log(`Has bass boost: ${hasBassBoost}`);
|
|
105
|
+
*/
|
|
106
|
+
public hasFilter(filterName: string): boolean {
|
|
107
|
+
return this.activeFilters.some((f) => f.name === filterName);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get available predefined filters
|
|
112
|
+
*
|
|
113
|
+
* @returns {AudioFilter[]} Array of all predefined filters
|
|
114
|
+
* @example
|
|
115
|
+
* const availableFilters = player.getAvailableFilters();
|
|
116
|
+
* console.log(`Available filters: ${availableFilters.length}`);
|
|
117
|
+
*/
|
|
118
|
+
public getAvailableFilters(): AudioFilter[] {
|
|
119
|
+
return Object.values(PREDEFINED_FILTERS);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get filters by category
|
|
124
|
+
*
|
|
125
|
+
* @param {string} category - Category to filter by
|
|
126
|
+
* @returns {AudioFilter[]} Array of filters in the category
|
|
127
|
+
* @example
|
|
128
|
+
* const eqFilters = player.getFiltersByCategory("eq");
|
|
129
|
+
* console.log(`EQ filters: ${eqFilters.map(f => f.name).join(', ')}`);
|
|
130
|
+
*/
|
|
131
|
+
public getFiltersByCategory(category: string): AudioFilter[] {
|
|
132
|
+
return Object.values(PREDEFINED_FILTERS).filter((f) => f.category === category);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Apply an audio filter to the player
|
|
137
|
+
*
|
|
138
|
+
* @param {string | AudioFilter} filter - Filter name or AudioFilter object
|
|
139
|
+
* @returns {Promise<boolean>} True if filter was applied successfully
|
|
140
|
+
* @example
|
|
141
|
+
* // Apply predefined filter to current track
|
|
142
|
+
* await player.applyFilter("bassboost");
|
|
143
|
+
*
|
|
144
|
+
* // Apply custom filter to current track
|
|
145
|
+
* await player.applyFilter({
|
|
146
|
+
* name: "custom",
|
|
147
|
+
* ffmpegFilter: "volume=1.5,treble=g=5",
|
|
148
|
+
* description: "Tăng âm lượng và âm cao"
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* // Apply filter without affecting current track
|
|
152
|
+
* await player.applyFilter("bassboost", false);
|
|
153
|
+
*/
|
|
154
|
+
public async applyFilter(filter?: string | AudioFilter): Promise<boolean> {
|
|
155
|
+
if (!filter) return false;
|
|
156
|
+
|
|
157
|
+
let audioFilter: AudioFilter | undefined;
|
|
158
|
+
if (typeof filter === "string") {
|
|
159
|
+
const predefined = PREDEFINED_FILTERS[filter];
|
|
160
|
+
if (!predefined) {
|
|
161
|
+
this.debug(`[FilterManager] Predefined filter not found: ${filter}`);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
audioFilter = predefined;
|
|
165
|
+
} else {
|
|
166
|
+
audioFilter = filter;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (this.activeFilters.some((f) => f.name === audioFilter!.name)) {
|
|
170
|
+
this.debug(`[FilterManager] Filter already applied: ${audioFilter.name}`);
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.activeFilters.push(audioFilter);
|
|
175
|
+
this.debug(`[FilterManager] Applied filter: ${audioFilter.name} - ${audioFilter.description}`);
|
|
176
|
+
return await this.player.refreshPlayerResource();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Apply multiple filters at once
|
|
181
|
+
*
|
|
182
|
+
* @param {(string | AudioFilter)[]} filters - Array of filter names or AudioFilter objects
|
|
183
|
+
* @returns {Promise<boolean>} True if all filters were applied successfully
|
|
184
|
+
* @example
|
|
185
|
+
* // Apply multiple filters to current track
|
|
186
|
+
* await player.applyFilters(["bassboost", "trebleboost"]);
|
|
187
|
+
*
|
|
188
|
+
* // Apply filters without affecting current track
|
|
189
|
+
* await player.applyFilters(["bassboost", "trebleboost"], false);
|
|
190
|
+
*/
|
|
191
|
+
public async applyFilters(filters: (string | AudioFilter)[]): Promise<boolean> {
|
|
192
|
+
let allApplied = true;
|
|
193
|
+
for (const f of filters) {
|
|
194
|
+
const ok = await this.applyFilter(f);
|
|
195
|
+
if (!ok) allApplied = false;
|
|
196
|
+
}
|
|
197
|
+
return allApplied;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public async removeFilter(filterName: string): Promise<boolean> {
|
|
201
|
+
const index = this.activeFilters.findIndex((f) => f.name === filterName);
|
|
202
|
+
if (index === -1) {
|
|
203
|
+
this.debug(`[FilterManager] Filter not found: ${filterName}`);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const removed = this.activeFilters.splice(index, 1)[0];
|
|
207
|
+
this.debug(`[FilterManager] Removed filter: ${removed.name}`);
|
|
208
|
+
return await this.player.refreshPlayerResource();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Clear all audio filters from the player
|
|
213
|
+
*
|
|
214
|
+
* @returns {boolean} True if filters were cleared successfully
|
|
215
|
+
* @example
|
|
216
|
+
* player.clearFilters();
|
|
217
|
+
*/
|
|
218
|
+
public async clearAll(): Promise<boolean> {
|
|
219
|
+
const count = this.activeFilters.length;
|
|
220
|
+
this.activeFilters = [];
|
|
221
|
+
this.debug(`[FilterManager] Cleared ${count} filters`);
|
|
222
|
+
return await this.player.refreshPlayerResource();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Apply filters and seek to a stream
|
|
227
|
+
*
|
|
228
|
+
* @param {Readable} stream - The stream to apply filters and seek to
|
|
229
|
+
* @param {number} position - The position to seek to in milliseconds (default: 0)
|
|
230
|
+
* @returns {Promise<Readable>} The stream with filters and seek applied
|
|
231
|
+
*/
|
|
232
|
+
public async applyFiltersAndSeek(stream: Readable, position: number = -1): Promise<Readable> {
|
|
233
|
+
const generation = ++this.ffmpegGeneration;
|
|
234
|
+
|
|
235
|
+
const filterString = this.getFilterString();
|
|
236
|
+
|
|
237
|
+
this.debug(
|
|
238
|
+
`Applying filters and seek — filters: ${filterString || "none"}, seek: ${position}ms, srcType: ${this.StreamType}`,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// có request mới chen vào
|
|
242
|
+
if (generation !== this.ffmpegGeneration) {
|
|
243
|
+
throw new Error("FFmpeg generation outdated");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
this.currentInputStream = stream;
|
|
247
|
+
|
|
248
|
+
const abortController = new AbortController();
|
|
249
|
+
this.ffmpegAbortController = abortController;
|
|
250
|
+
|
|
251
|
+
if (position >= 0 && ffmpegPath) {
|
|
252
|
+
return this.spawnFFmpegInputSeek(stream, position, filterString, abortController.signal, generation);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const args = ["-analyzeduration", "0", "-loglevel", "0"];
|
|
256
|
+
|
|
257
|
+
if (filterString) {
|
|
258
|
+
args.push("-af", filterString);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
args.push(
|
|
262
|
+
"-f",
|
|
263
|
+
this.StreamType === "webm/opus" ? "webm/opus"
|
|
264
|
+
: this.StreamType === "ogg/opus" ? "ogg/opus"
|
|
265
|
+
: "mp3",
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
args.push("-ar", "48000", "-ac", "2");
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
this.ffmpeg = stream.pipe(new prism.FFmpeg({ args }));
|
|
272
|
+
} catch (spawnError) {
|
|
273
|
+
this.debug(`FFmpeg spawn error:`, spawnError);
|
|
274
|
+
this.currentInputStream = null;
|
|
275
|
+
this.ffmpegAbortController = null;
|
|
276
|
+
throw spawnError;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// nếu bị supersede ngay sau khi spawn
|
|
280
|
+
if (generation !== this.ffmpegGeneration) {
|
|
281
|
+
try {
|
|
282
|
+
this.ffmpeg.destroy();
|
|
283
|
+
} catch {}
|
|
284
|
+
|
|
285
|
+
throw new Error("FFmpeg process superseded");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.ffmpeg.on("close", () => {
|
|
289
|
+
this.debug(`FFmpeg processing completed`);
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
this.ffmpeg?.destroy();
|
|
293
|
+
} catch {}
|
|
294
|
+
|
|
295
|
+
if (this.ffmpeg === this.ffmpeg) {
|
|
296
|
+
this.ffmpeg = null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (this.ffmpegAbortController === abortController) {
|
|
300
|
+
this.ffmpegAbortController = null;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
this.ffmpeg.on("error", (err: Error) => {
|
|
305
|
+
this.debug(`FFmpeg error:`, err);
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
this.ffmpeg?.destroy();
|
|
309
|
+
} catch {}
|
|
310
|
+
|
|
311
|
+
if (this.ffmpeg === this.ffmpeg) {
|
|
312
|
+
this.ffmpeg = null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (this.ffmpegAbortController === abortController) {
|
|
316
|
+
this.ffmpegAbortController = null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.currentInputStream = null;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return this.ffmpeg;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private spawnFFmpegInputSeek(
|
|
326
|
+
stream: Readable,
|
|
327
|
+
position: number,
|
|
328
|
+
filterString: string,
|
|
329
|
+
signal: AbortSignal,
|
|
330
|
+
generation: number,
|
|
331
|
+
): Readable {
|
|
332
|
+
const seekSeconds = (position / 1000).toFixed(3);
|
|
333
|
+
|
|
334
|
+
const args: string[] = ["-i", "pipe:0", "-ss", seekSeconds, "-analyzeduration", "0", "-loglevel", "0"];
|
|
335
|
+
|
|
336
|
+
if (filterString) {
|
|
337
|
+
args.push("-af", filterString);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const outFormat =
|
|
341
|
+
this.StreamType === "webm/opus" ? "webm"
|
|
342
|
+
: this.StreamType === "ogg/opus" ? "ogg"
|
|
343
|
+
: "mp3";
|
|
344
|
+
|
|
345
|
+
args.push("-f", outFormat, "-ar", "48000", "-ac", "2", "pipe:1");
|
|
346
|
+
|
|
347
|
+
const proc = spawn(ffmpegPath!, args, {
|
|
348
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const oldProcess = this.ffmpegProcess;
|
|
352
|
+
|
|
353
|
+
this.pendingFFmpegProcess = proc;
|
|
354
|
+
|
|
355
|
+
if (generation !== this.ffmpegGeneration) {
|
|
356
|
+
try {
|
|
357
|
+
proc.kill("SIGKILL");
|
|
358
|
+
} catch {}
|
|
359
|
+
|
|
360
|
+
throw new Error("FFmpeg process superseded");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const onAbort = () => {
|
|
364
|
+
signal.removeEventListener("abort", onAbort);
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
stream.unpipe(proc.stdin!);
|
|
368
|
+
} catch {}
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
if (proc.stdin && !proc.stdin.destroyed) {
|
|
372
|
+
proc.stdin.destroy();
|
|
373
|
+
}
|
|
374
|
+
} catch {}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
proc.kill("SIGKILL");
|
|
378
|
+
} catch {}
|
|
379
|
+
|
|
380
|
+
this.debug(`[FilterManager] FFmpeg process aborted (seek pos: ${position}ms)`);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
if (signal.aborted) {
|
|
384
|
+
// Already aborted before we even spawned
|
|
385
|
+
onAbort();
|
|
386
|
+
} else {
|
|
387
|
+
signal.addEventListener("abort", onAbort);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Pipe source → ffmpeg stdin
|
|
391
|
+
stream.pipe(proc.stdin!);
|
|
392
|
+
|
|
393
|
+
// Suppress EPIPE on stdin when the process exits early
|
|
394
|
+
proc.stdin!.on("error", (err: Error) => {
|
|
395
|
+
if ((err as any).code !== "EPIPE") {
|
|
396
|
+
this.debug(`FFmpeg stdin error: ${err.message}`);
|
|
397
|
+
}
|
|
398
|
+
// EPIPE is expected when proc is killed — silence it
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
proc.stdout!.on("error", (err: Error) => {
|
|
402
|
+
this.debug(`FFmpeg stdout error: ${err.message}`);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
proc.on("close", (code) => {
|
|
406
|
+
signal.removeEventListener("abort", onAbort);
|
|
407
|
+
this.debug(`FFmpeg process exited (code: ${code})`);
|
|
408
|
+
if (this.ffmpegProcess === proc) {
|
|
409
|
+
this.ffmpegProcess = null;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
proc.on("error", (err: Error) => {
|
|
414
|
+
signal.removeEventListener("abort", onAbort);
|
|
415
|
+
this.debug(`FFmpeg process error: ${err.message}`);
|
|
416
|
+
if (this.ffmpegProcess === proc) {
|
|
417
|
+
this.ffmpegProcess = null;
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
this.ffmpegProcess = proc;
|
|
422
|
+
this.pendingFFmpegProcess = null;
|
|
423
|
+
|
|
424
|
+
// kill old AFTER new ready
|
|
425
|
+
if (oldProcess && oldProcess !== proc) {
|
|
426
|
+
try {
|
|
427
|
+
if (oldProcess.stdin && !oldProcess.stdin.destroyed) {
|
|
428
|
+
oldProcess.stdin.destroy();
|
|
429
|
+
}
|
|
430
|
+
} catch {}
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
oldProcess.kill("SIGKILL");
|
|
434
|
+
} catch {}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return proc.stdout as Readable;
|
|
438
|
+
}
|
|
439
|
+
}
|