ziplayer 0.1.5 → 0.2.1
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/README.md +16 -0
- package/dist/extensions/index.d.ts +18 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +146 -3
- package/dist/extensions/index.js.map +1 -1
- package/dist/plugins/index.d.ts +12 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +55 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.d.ts +126 -0
- package/dist/structures/FilterManager.d.ts.map +1 -0
- package/dist/structures/FilterManager.js +247 -0
- package/dist/structures/FilterManager.js.map +1 -0
- package/dist/structures/Player.d.ts +144 -115
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +683 -743
- package/dist/structures/Player.js.map +1 -1
- package/dist/types/index.d.ts +71 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +223 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +3 -7
- package/src/extensions/index.ts +165 -3
- package/src/plugins/index.ts +70 -0
- package/src/structures/FilterManager.ts +262 -0
- package/src/structures/Player.ts +759 -803
- package/src/types/index.ts +291 -0
package/src/extensions/index.ts
CHANGED
|
@@ -1,17 +1,69 @@
|
|
|
1
|
+
import type { Player } from "../structures/Player";
|
|
2
|
+
import type { PlayerManager } from "../structures/PlayerManager";
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionSearchRequest,
|
|
5
|
+
SearchResult,
|
|
6
|
+
StreamInfo,
|
|
7
|
+
Track,
|
|
8
|
+
ExtensionContext,
|
|
9
|
+
ExtensionPlayRequest,
|
|
10
|
+
ExtensionPlayResponse,
|
|
11
|
+
ExtensionAfterPlayPayload,
|
|
12
|
+
ExtensionStreamRequest,
|
|
13
|
+
} from "../types";
|
|
14
|
+
|
|
1
15
|
import { BaseExtension } from "./BaseExtension";
|
|
2
16
|
|
|
3
17
|
export { BaseExtension } from "./BaseExtension";
|
|
4
18
|
|
|
19
|
+
type DebugFn = (message?: any, ...optionalParams: any[]) => void;
|
|
20
|
+
|
|
5
21
|
// Extension factory
|
|
6
22
|
export class ExtensionManager {
|
|
7
|
-
private
|
|
23
|
+
private debug: DebugFn;
|
|
24
|
+
private extensions: Map<string, BaseExtension>;
|
|
25
|
+
private player: Player;
|
|
26
|
+
private manager: PlayerManager;
|
|
27
|
+
private extensionContext: ExtensionContext;
|
|
28
|
+
|
|
29
|
+
constructor(player: Player, manager: PlayerManager) {
|
|
30
|
+
this.player = player;
|
|
31
|
+
this.manager = manager;
|
|
32
|
+
this.extensions = new Map();
|
|
33
|
+
this.extensionContext = Object.freeze({ player, manager });
|
|
34
|
+
|
|
35
|
+
this.debug = (message?: any, ...optionalParams: any[]) => {
|
|
36
|
+
if (manager.debugEnabled) {
|
|
37
|
+
manager.emit("debug", `[ExtensionManager] ${message}`, ...optionalParams);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
8
41
|
|
|
9
42
|
register(extension: BaseExtension): void {
|
|
43
|
+
if (this.extensions.has(extension.name)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (!extension.player) {
|
|
47
|
+
extension.player = this.player;
|
|
48
|
+
}
|
|
10
49
|
this.extensions.set(extension.name, extension);
|
|
11
50
|
}
|
|
12
51
|
|
|
13
|
-
unregister(
|
|
14
|
-
|
|
52
|
+
unregister(extension: BaseExtension): boolean {
|
|
53
|
+
const name = extension.name;
|
|
54
|
+
const result = this.extensions.delete(name);
|
|
55
|
+
if (result) {
|
|
56
|
+
this.invokeExtensionLifecycle(extension, "onDestroy");
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
destroy(): void {
|
|
62
|
+
this.debug(`[ExtensionManager] destroying all extensions`);
|
|
63
|
+
for (const extension of this.extensions.values()) {
|
|
64
|
+
this.unregister(extension);
|
|
65
|
+
}
|
|
66
|
+
this.extensions.clear();
|
|
15
67
|
}
|
|
16
68
|
|
|
17
69
|
get(name: string): BaseExtension | undefined {
|
|
@@ -29,4 +81,114 @@ export class ExtensionManager {
|
|
|
29
81
|
clear(): void {
|
|
30
82
|
this.extensions.clear();
|
|
31
83
|
}
|
|
84
|
+
|
|
85
|
+
private invokeExtensionLifecycle(extension: BaseExtension | undefined, hook: "onRegister" | "onDestroy"): void {
|
|
86
|
+
if (!extension) return;
|
|
87
|
+
const fn = (extension as any)[hook];
|
|
88
|
+
if (typeof fn !== "function") return;
|
|
89
|
+
try {
|
|
90
|
+
const result = fn.call(extension, this.extensionContext);
|
|
91
|
+
if (result && typeof (result as Promise<unknown>).then === "function") {
|
|
92
|
+
(result as Promise<unknown>).catch((err) => this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err));
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async provideSearch(query: string, requestedBy: string): Promise<SearchResult | null> {
|
|
100
|
+
const request: ExtensionSearchRequest = { query, requestedBy };
|
|
101
|
+
for (const extension of this.getAll()) {
|
|
102
|
+
const hook = (extension as any).provideSearch;
|
|
103
|
+
if (typeof hook !== "function") continue;
|
|
104
|
+
try {
|
|
105
|
+
const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
|
|
106
|
+
if (result && Array.isArray(result.tracks) && result.tracks.length > 0) {
|
|
107
|
+
this.debug(`[Player] Extension ${extension.name} handled search for query: ${query}`);
|
|
108
|
+
return result as SearchResult;
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
this.debug(`[Player] Extension ${extension.name} provideSearch error:`, err);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async provideStream(track: Track): Promise<StreamInfo | null> {
|
|
118
|
+
const request: ExtensionStreamRequest = { track };
|
|
119
|
+
for (const extension of this.getAll()) {
|
|
120
|
+
const hook = (extension as any).provideStream;
|
|
121
|
+
if (typeof hook !== "function") continue;
|
|
122
|
+
try {
|
|
123
|
+
const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
|
|
124
|
+
if (result && (result as StreamInfo).stream) {
|
|
125
|
+
this.debug(`[Player] Extension ${extension.name} provided stream for track: ${track.title}`);
|
|
126
|
+
return result as StreamInfo;
|
|
127
|
+
}
|
|
128
|
+
} catch (err) {
|
|
129
|
+
this.debug(`[Player] Extension ${extension.name} provideStream error:`, err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async BeforePlayHooks(
|
|
136
|
+
initial: ExtensionPlayRequest,
|
|
137
|
+
): Promise<{ request: ExtensionPlayRequest; response: ExtensionPlayResponse }> {
|
|
138
|
+
const request: ExtensionPlayRequest = { ...initial };
|
|
139
|
+
const response: ExtensionPlayResponse = {};
|
|
140
|
+
for (const extension of this.getAll()) {
|
|
141
|
+
const hook = (extension as any).beforePlay;
|
|
142
|
+
if (typeof hook !== "function") continue;
|
|
143
|
+
try {
|
|
144
|
+
const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
|
|
145
|
+
if (!result) continue;
|
|
146
|
+
if (result.query !== undefined) {
|
|
147
|
+
request.query = result.query;
|
|
148
|
+
response.query = result.query;
|
|
149
|
+
}
|
|
150
|
+
if (result.requestedBy !== undefined) {
|
|
151
|
+
request.requestedBy = result.requestedBy;
|
|
152
|
+
response.requestedBy = result.requestedBy;
|
|
153
|
+
}
|
|
154
|
+
if (Array.isArray(result.tracks)) {
|
|
155
|
+
response.tracks = result.tracks;
|
|
156
|
+
}
|
|
157
|
+
if (typeof result.isPlaylist === "boolean") {
|
|
158
|
+
response.isPlaylist = result.isPlaylist;
|
|
159
|
+
}
|
|
160
|
+
if (typeof result.success === "boolean") {
|
|
161
|
+
response.success = result.success;
|
|
162
|
+
}
|
|
163
|
+
if (result.error instanceof Error) {
|
|
164
|
+
response.error = result.error;
|
|
165
|
+
}
|
|
166
|
+
if (typeof result.handled === "boolean") {
|
|
167
|
+
response.handled = result.handled;
|
|
168
|
+
if (result.handled) break;
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
this.debug(`[Player] Extension ${extension.name} beforePlay error:`, err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { request, response };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async AfterPlayHooks(payload: ExtensionAfterPlayPayload): Promise<void> {
|
|
178
|
+
if (this.getAll().length === 0) return;
|
|
179
|
+
const safeTracks = payload.tracks ? [...payload.tracks] : undefined;
|
|
180
|
+
if (safeTracks) {
|
|
181
|
+
Object.freeze(safeTracks);
|
|
182
|
+
}
|
|
183
|
+
const immutablePayload = Object.freeze({ ...payload, tracks: safeTracks });
|
|
184
|
+
for (const extension of this.getAll()) {
|
|
185
|
+
const hook = (extension as any).afterPlay;
|
|
186
|
+
if (typeof hook !== "function") continue;
|
|
187
|
+
try {
|
|
188
|
+
await Promise.resolve(hook.call(extension, this.extensionContext, immutablePayload));
|
|
189
|
+
} catch (err) {
|
|
190
|
+
this.debug(`[Player] Extension ${extension.name} afterPlay error:`, err);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
32
194
|
}
|
package/src/plugins/index.ts
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import { BasePlugin } from "./BasePlugin";
|
|
2
|
+
import { withTimeout } from "../utils/timeout";
|
|
3
|
+
import type { Track, StreamInfo } from "../types";
|
|
4
|
+
import type { PlayerManager } from "../structures/PlayerManager";
|
|
5
|
+
import type { Player } from "../structures/Player";
|
|
6
|
+
type DebugFn = (message?: any, ...optionalParams: any[]) => void;
|
|
7
|
+
|
|
8
|
+
type PluginManagerOptions = {
|
|
9
|
+
extractorTimeout: number | undefined;
|
|
10
|
+
};
|
|
2
11
|
|
|
3
12
|
export { BasePlugin } from "./BasePlugin";
|
|
4
13
|
|
|
5
14
|
// Plugin factory
|
|
6
15
|
export class PluginManager {
|
|
16
|
+
private debug: DebugFn;
|
|
17
|
+
private options: PluginManagerOptions;
|
|
18
|
+
private player: Player;
|
|
19
|
+
private manager: PlayerManager;
|
|
7
20
|
private plugins: Map<string, BasePlugin> = new Map();
|
|
8
21
|
|
|
22
|
+
constructor(player: Player, manager: PlayerManager, options: PluginManagerOptions) {
|
|
23
|
+
this.player = player;
|
|
24
|
+
this.manager = manager;
|
|
25
|
+
this.options = options;
|
|
26
|
+
this.debug = (message?: any, ...optionalParams: any[]) => {
|
|
27
|
+
if (manager.debugEnabled) {
|
|
28
|
+
manager.emit("debug", `[ExtensionManager] ${message}`, ...optionalParams);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
9
33
|
register(plugin: BasePlugin): void {
|
|
10
34
|
this.plugins.set(plugin.name, plugin);
|
|
11
35
|
}
|
|
@@ -29,4 +53,50 @@ export class PluginManager {
|
|
|
29
53
|
clear(): void {
|
|
30
54
|
this.plugins.clear();
|
|
31
55
|
}
|
|
56
|
+
|
|
57
|
+
async getStream(track: Track): Promise<StreamInfo | null> {
|
|
58
|
+
let streamInfo: StreamInfo | null = null;
|
|
59
|
+
const plugin = this.get(track.source) || this.findPlugin(track.url);
|
|
60
|
+
|
|
61
|
+
if (!plugin) {
|
|
62
|
+
this.debug(`[Player] No plugin found for track: ${track.title}`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.debug(`[Player] Getting stream for track: ${track.title}`);
|
|
67
|
+
this.debug(`[Player] Using plugin: ${plugin.name}`);
|
|
68
|
+
this.debug(`[Track] Track Info:`, track);
|
|
69
|
+
const timeoutMs = this.options.extractorTimeout ?? 50000;
|
|
70
|
+
try {
|
|
71
|
+
streamInfo = await withTimeout(plugin.getStream(track), timeoutMs, "getStream timed out");
|
|
72
|
+
if (!(streamInfo as any)?.stream) {
|
|
73
|
+
throw new Error(`No stream returned from ${plugin.name}`);
|
|
74
|
+
}
|
|
75
|
+
} catch (streamError) {
|
|
76
|
+
this.debug(`[Player] getStream failed, trying getFallback:`, streamError);
|
|
77
|
+
const allplugs = this.getAll();
|
|
78
|
+
for (const p of allplugs) {
|
|
79
|
+
if (typeof (p as any).getFallback !== "function" && typeof (p as any).getStream !== "function") {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
streamInfo = await withTimeout((p as any).getStream(track), timeoutMs, `getStream timed out for plugin ${p.name}`);
|
|
84
|
+
if ((streamInfo as any)?.stream) {
|
|
85
|
+
this.debug(`[Player] getStream succeeded with plugin ${p.name} for track: ${track.title}`);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
streamInfo = await withTimeout((p as any).getFallback(track), timeoutMs, `getFallback timed out for plugin ${p.name}`);
|
|
89
|
+
if (!(streamInfo as any)?.stream) continue;
|
|
90
|
+
break;
|
|
91
|
+
} catch (fallbackError) {
|
|
92
|
+
this.debug(`[Player] getFallback failed with plugin ${p.name}:`, fallbackError);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!(streamInfo as any)?.stream) {
|
|
96
|
+
throw new Error(`All getFallback attempts failed for track: ${track.title}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return streamInfo as StreamInfo;
|
|
101
|
+
}
|
|
32
102
|
}
|
|
@@ -0,0 +1,262 @@
|
|
|
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
|
+
|
|
8
|
+
type DebugFn = (message?: any, ...optionalParams: any[]) => void;
|
|
9
|
+
|
|
10
|
+
export class FilterManager {
|
|
11
|
+
private activeFilters: AudioFilter[] = [];
|
|
12
|
+
private debug: DebugFn;
|
|
13
|
+
private player: Player;
|
|
14
|
+
private ffmpeg: FFmpeg | null = null;
|
|
15
|
+
public StreamType: "webm/opus" | "ogg/opus" | "mp3" | "arbitrary" = "mp3";
|
|
16
|
+
|
|
17
|
+
constructor(player: Player, manager: PlayerManager) {
|
|
18
|
+
this.player = player as Player;
|
|
19
|
+
|
|
20
|
+
this.debug = (message?: any, ...optionalParams: any[]) => {
|
|
21
|
+
if (manager.debugEnabled) {
|
|
22
|
+
manager.emit("debug", `[FilterManager] ${message}`, ...optionalParams);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Destroy the filter manager
|
|
29
|
+
*
|
|
30
|
+
* @returns {void}
|
|
31
|
+
* @example
|
|
32
|
+
* player.filter.destroy();
|
|
33
|
+
*/
|
|
34
|
+
destroy(): void {
|
|
35
|
+
this.activeFilters = [];
|
|
36
|
+
if (this.ffmpeg) {
|
|
37
|
+
try {
|
|
38
|
+
this.ffmpeg.destroy();
|
|
39
|
+
} catch {}
|
|
40
|
+
this.ffmpeg = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the combined FFmpeg filter string for all active filters
|
|
46
|
+
*
|
|
47
|
+
* @returns {string} Combined FFmpeg filter string
|
|
48
|
+
* @example
|
|
49
|
+
* const filterString = player.getFilterString();
|
|
50
|
+
* console.log(`Filter string: ${filterString}`);
|
|
51
|
+
*/
|
|
52
|
+
public getFilterString(): string {
|
|
53
|
+
if (this.activeFilters.length === 0) return "";
|
|
54
|
+
return this.activeFilters.map((f) => f.ffmpegFilter).join(",");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get all currently applied filters
|
|
59
|
+
*
|
|
60
|
+
* @returns {AudioFilter[]} Array of active filters
|
|
61
|
+
* @example
|
|
62
|
+
* const filters = player.getActiveFilters();
|
|
63
|
+
* console.log(`Active filters: ${filters.map(f => f.name).join(', ')}`);
|
|
64
|
+
*/
|
|
65
|
+
public getActiveFilters(): AudioFilter[] {
|
|
66
|
+
return [...this.activeFilters];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a specific filter is currently applied
|
|
71
|
+
*
|
|
72
|
+
* @param {string} filterName - Name of the filter to check
|
|
73
|
+
* @returns {boolean} True if filter is applied
|
|
74
|
+
* @example
|
|
75
|
+
* const hasBassBoost = player.hasFilter("bassboost");
|
|
76
|
+
* console.log(`Has bass boost: ${hasBassBoost}`);
|
|
77
|
+
*/
|
|
78
|
+
public hasFilter(filterName: string): boolean {
|
|
79
|
+
return this.activeFilters.some((f) => f.name === filterName);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get available predefined filters
|
|
84
|
+
*
|
|
85
|
+
* @returns {AudioFilter[]} Array of all predefined filters
|
|
86
|
+
* @example
|
|
87
|
+
* const availableFilters = player.getAvailableFilters();
|
|
88
|
+
* console.log(`Available filters: ${availableFilters.length}`);
|
|
89
|
+
*/
|
|
90
|
+
public getAvailableFilters(): AudioFilter[] {
|
|
91
|
+
return Object.values(PREDEFINED_FILTERS);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get filters by category
|
|
96
|
+
*
|
|
97
|
+
* @param {string} category - Category to filter by
|
|
98
|
+
* @returns {AudioFilter[]} Array of filters in the category
|
|
99
|
+
* @example
|
|
100
|
+
* const eqFilters = player.getFiltersByCategory("eq");
|
|
101
|
+
* console.log(`EQ filters: ${eqFilters.map(f => f.name).join(', ')}`);
|
|
102
|
+
*/
|
|
103
|
+
public getFiltersByCategory(category: string): AudioFilter[] {
|
|
104
|
+
return Object.values(PREDEFINED_FILTERS).filter((f) => f.category === category);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Apply an audio filter to the player
|
|
109
|
+
*
|
|
110
|
+
* @param {string | AudioFilter} filter - Filter name or AudioFilter object
|
|
111
|
+
* @returns {Promise<boolean>} True if filter was applied successfully
|
|
112
|
+
* @example
|
|
113
|
+
* // Apply predefined filter to current track
|
|
114
|
+
* await player.applyFilter("bassboost");
|
|
115
|
+
*
|
|
116
|
+
* // Apply custom filter to current track
|
|
117
|
+
* await player.applyFilter({
|
|
118
|
+
* name: "custom",
|
|
119
|
+
* ffmpegFilter: "volume=1.5,treble=g=5",
|
|
120
|
+
* description: "Tăng âm lượng và âm cao"
|
|
121
|
+
* });
|
|
122
|
+
*
|
|
123
|
+
* // Apply filter without affecting current track
|
|
124
|
+
* await player.applyFilter("bassboost", false);
|
|
125
|
+
*/
|
|
126
|
+
public async applyFilter(filter?: string | AudioFilter): Promise<boolean> {
|
|
127
|
+
if (!filter) return false;
|
|
128
|
+
|
|
129
|
+
let audioFilter: AudioFilter | undefined;
|
|
130
|
+
if (typeof filter === "string") {
|
|
131
|
+
const predefined = PREDEFINED_FILTERS[filter];
|
|
132
|
+
if (!predefined) {
|
|
133
|
+
this.debug(`[FilterManager] Predefined filter not found: ${filter}`);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
audioFilter = predefined;
|
|
137
|
+
} else {
|
|
138
|
+
audioFilter = filter;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.activeFilters.some((f) => f.name === audioFilter.name)) {
|
|
142
|
+
this.debug(`[FilterManager] Filter already applied: ${audioFilter.name}`);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.activeFilters.push(audioFilter);
|
|
147
|
+
this.debug(`[FilterManager] Applied filter: ${audioFilter.name} - ${audioFilter.description}`);
|
|
148
|
+
return await this.player.refeshPlayerResource();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Apply multiple filters at once
|
|
153
|
+
*
|
|
154
|
+
* @param {(string | AudioFilter)[]} filters - Array of filter names or AudioFilter objects
|
|
155
|
+
* @returns {Promise<boolean>} True if all filters were applied successfully
|
|
156
|
+
* @example
|
|
157
|
+
* // Apply multiple filters to current track
|
|
158
|
+
* await player.applyFilters(["bassboost", "trebleboost"]);
|
|
159
|
+
*
|
|
160
|
+
* // Apply filters without affecting current track
|
|
161
|
+
* await player.applyFilters(["bassboost", "trebleboost"], false);
|
|
162
|
+
*/
|
|
163
|
+
public async applyFilters(filters: (string | AudioFilter)[]): Promise<boolean> {
|
|
164
|
+
let allApplied = true;
|
|
165
|
+
for (const f of filters) {
|
|
166
|
+
const ok = await this.applyFilter(f);
|
|
167
|
+
if (!ok) allApplied = false;
|
|
168
|
+
}
|
|
169
|
+
return allApplied;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Remove an audio filter from the player
|
|
173
|
+
*
|
|
174
|
+
* @param {string} filterName - Name of the filter to remove
|
|
175
|
+
* @returns {boolean} True if filter was removed successfully
|
|
176
|
+
* @example
|
|
177
|
+
* player.removeFilter("bassboost");
|
|
178
|
+
*/
|
|
179
|
+
public async removeFilter(filterName: string): Promise<boolean> {
|
|
180
|
+
const index = this.activeFilters.findIndex((f) => f.name === filterName);
|
|
181
|
+
if (index === -1) {
|
|
182
|
+
this.debug(`[FilterManager] Filter not found: ${filterName}`);
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const removed = this.activeFilters.splice(index, 1)[0];
|
|
186
|
+
this.debug(`[FilterManager] Removed filter: ${removed.name}`);
|
|
187
|
+
return await this.player.refeshPlayerResource();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Clear all audio filters from the player
|
|
192
|
+
*
|
|
193
|
+
* @returns {boolean} True if filters were cleared successfully
|
|
194
|
+
* @example
|
|
195
|
+
* player.clearFilters();
|
|
196
|
+
*/
|
|
197
|
+
public async clearAll(): Promise<boolean> {
|
|
198
|
+
const count = this.activeFilters.length;
|
|
199
|
+
this.activeFilters = [];
|
|
200
|
+
this.debug(`[FilterManager] Cleared ${count} filters`);
|
|
201
|
+
return await this.player.refeshPlayerResource();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Apply filters and seek to a stream
|
|
206
|
+
*
|
|
207
|
+
* @param {Readable} stream - The stream to apply filters and seek to
|
|
208
|
+
* @param {number} position - The position to seek to in milliseconds (default: 0)
|
|
209
|
+
* @returns {Promise<Readable>} The stream with filters and seek applied
|
|
210
|
+
*/
|
|
211
|
+
public async applyFiltersAndSeek(stream: Readable, position: number = -1): Promise<Readable> {
|
|
212
|
+
const filterString = this.getFilterString();
|
|
213
|
+
this.debug(`[FilterManager] Applying filters and seek to stream: ${filterString || "none"}, seek: ${position}ms`);
|
|
214
|
+
try {
|
|
215
|
+
const args = ["-analyzeduration", "0", "-loglevel", "0"];
|
|
216
|
+
|
|
217
|
+
if (position > 0) {
|
|
218
|
+
const seekSeconds = Math.floor(position / 1000);
|
|
219
|
+
args.push("-ss", seekSeconds.toString());
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Add filter if any are active
|
|
223
|
+
if (filterString) {
|
|
224
|
+
args.push("-af", filterString);
|
|
225
|
+
}
|
|
226
|
+
args.push("-f", this.StreamType === "webm/opus" ? "webm/opus" : this.StreamType === "ogg/opus" ? "ogg/opus" : "mp3");
|
|
227
|
+
args.push("-ar", "48000", "-ac", "2");
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
if (this.ffmpeg) {
|
|
231
|
+
this.ffmpeg.destroy();
|
|
232
|
+
}
|
|
233
|
+
this.ffmpeg = null;
|
|
234
|
+
} catch {}
|
|
235
|
+
|
|
236
|
+
this.ffmpeg = stream.pipe(new prism.FFmpeg({ args }));
|
|
237
|
+
|
|
238
|
+
this.ffmpeg.on("close", () => {
|
|
239
|
+
this.debug(`[FilterManager] FFmpeg filter+seek processing completed`);
|
|
240
|
+
if (this.ffmpeg) {
|
|
241
|
+
this.ffmpeg.destroy();
|
|
242
|
+
this.ffmpeg = null;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
this.ffmpeg.on("error", (err: Error) => {
|
|
247
|
+
this.debug(`[FilterManager] FFmpeg filter+seek error:`, err);
|
|
248
|
+
if (this.ffmpeg) {
|
|
249
|
+
this.ffmpeg.destroy();
|
|
250
|
+
}
|
|
251
|
+
if (this.ffmpeg) {
|
|
252
|
+
this.ffmpeg = null;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
return this.ffmpeg;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
this.debug(`[FilterManager] Error creating FFmpeg instance:`, error);
|
|
258
|
+
// Fallback to original stream if FFmpeg fails
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|