@ziplayer/plugin 0.1.52 → 0.2.1-dev-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/dist/YouTubePlugin.d.ts +5 -1
- package/dist/YouTubePlugin.d.ts.map +1 -1
- package/dist/YouTubePlugin.js +68 -107
- package/dist/YouTubePlugin.js.map +1 -1
- package/dist/index.d.ts +0 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -16
- package/dist/index.js.map +1 -1
- package/dist/utils/sabr-stream-factory.d.ts +27 -8
- package/dist/utils/sabr-stream-factory.d.ts.map +1 -1
- package/dist/utils/sabr-stream-factory.js +164 -27
- package/dist/utils/sabr-stream-factory.js.map +1 -1
- package/dist/utils/stream-converter.d.ts +1 -4
- package/dist/utils/stream-converter.d.ts.map +1 -1
- package/dist/utils/stream-converter.js +69 -37
- package/dist/utils/stream-converter.js.map +1 -1
- package/package.json +46 -45
- package/src/YouTubePlugin.ts +587 -620
- package/src/index.ts +0 -17
- package/src/utils/sabr-stream-factory.ts +282 -96
- package/src/utils/stream-converter.ts +75 -44
- package/tsconfig.json +6 -2
- package/src/YTSRPlugin.ts +0 -596
- package/src/types/googlevideo.d.ts +0 -45
package/src/index.ts
CHANGED
|
@@ -85,23 +85,6 @@ export * from "./SpotifyPlugin";
|
|
|
85
85
|
*/
|
|
86
86
|
export * from "./TTSPlugin";
|
|
87
87
|
|
|
88
|
-
/**
|
|
89
|
-
* YTSR plugin for advanced YouTube search without streaming.
|
|
90
|
-
*
|
|
91
|
-
* Provides comprehensive YouTube search functionality including:
|
|
92
|
-
* - Advanced video search with filters (duration, upload date, sort by)
|
|
93
|
-
* - Playlist and channel search
|
|
94
|
-
* - Multiple search types (video, playlist, channel, all)
|
|
95
|
-
* - Metadata extraction without streaming
|
|
96
|
-
* - Support for YouTube URLs
|
|
97
|
-
*
|
|
98
|
-
* @example
|
|
99
|
-
* const ytsrPlugin = new YTSRPlugin();
|
|
100
|
-
* const result = await ytsrPlugin.search("Never Gonna Give You Up", "user123");
|
|
101
|
-
* const playlistResult = await ytsrPlugin.searchPlaylist("chill music", "user123");
|
|
102
|
-
*/
|
|
103
|
-
export * from "./YTSRPlugin";
|
|
104
|
-
|
|
105
88
|
/**
|
|
106
89
|
* Attachments plugin for handling Discord attachment URLs and audio files.
|
|
107
90
|
*
|
|
@@ -1,96 +1,282 @@
|
|
|
1
|
-
import { createWriteStream } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import type
|
|
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
|
-
|
|
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
|
-
}
|
|
1
|
+
import { createWriteStream } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { Readable } from "stream";
|
|
5
|
+
import { Constants, YTNodes, Platform } from "youtubei.js";
|
|
6
|
+
import type Innertube from "youtubei.js";
|
|
7
|
+
|
|
8
|
+
import { SabrStream } from "googlevideo/sabr-stream";
|
|
9
|
+
import { buildSabrFormat } from "googlevideo/utils";
|
|
10
|
+
|
|
11
|
+
import { BG } from "bgutils-js";
|
|
12
|
+
import { JSDOM } from "jsdom";
|
|
13
|
+
import { webStreamToNodeStream } from "./stream-converter";
|
|
14
|
+
|
|
15
|
+
export interface OutputStream {
|
|
16
|
+
stream: NodeJS.WritableStream;
|
|
17
|
+
filePath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SabrAudioResult {
|
|
21
|
+
title: string;
|
|
22
|
+
stream: Readable;
|
|
23
|
+
format: {
|
|
24
|
+
mimeType: string;
|
|
25
|
+
itag: number;
|
|
26
|
+
contentLength: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SabrPlaybackOptions {
|
|
31
|
+
preferWebM?: boolean;
|
|
32
|
+
preferOpus?: boolean;
|
|
33
|
+
videoQuality?: string;
|
|
34
|
+
audioQuality?: string;
|
|
35
|
+
enabledTrackTypes?: any;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generates a web PoToken for YouTube authentication
|
|
39
|
+
* This is required for accessing restricted video content
|
|
40
|
+
*/
|
|
41
|
+
async function generateWebPoToken(contentBinding: string): Promise<{
|
|
42
|
+
visitorData: string;
|
|
43
|
+
placeholderPoToken: string;
|
|
44
|
+
poToken: string;
|
|
45
|
+
}> {
|
|
46
|
+
try {
|
|
47
|
+
const requestKey = "O43z0dpjhgX20SCx4KAo";
|
|
48
|
+
|
|
49
|
+
if (!contentBinding) throw new Error("Could not get visitor data");
|
|
50
|
+
|
|
51
|
+
const dom = new JSDOM();
|
|
52
|
+
|
|
53
|
+
Object.assign(globalThis, {
|
|
54
|
+
window: dom.window,
|
|
55
|
+
document: dom.window.document,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const bgConfig = {
|
|
59
|
+
fetch: (input: any, init: any) => fetch(input, init),
|
|
60
|
+
globalObj: globalThis,
|
|
61
|
+
identifier: contentBinding,
|
|
62
|
+
requestKey,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const bgChallenge = await BG.Challenge.create(bgConfig);
|
|
66
|
+
|
|
67
|
+
if (!bgChallenge) throw new Error("Could not get challenge");
|
|
68
|
+
|
|
69
|
+
const interpreterJavascript = bgChallenge.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
|
|
70
|
+
|
|
71
|
+
if (interpreterJavascript) {
|
|
72
|
+
new Function(interpreterJavascript)();
|
|
73
|
+
} else throw new Error("Could not load VM");
|
|
74
|
+
|
|
75
|
+
const poTokenResult = await BG.PoToken.generate({
|
|
76
|
+
program: bgChallenge.program,
|
|
77
|
+
globalName: bgChallenge.globalName,
|
|
78
|
+
bgConfig,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const placeholderPoToken = BG.PoToken.generatePlaceholder(contentBinding);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
visitorData: contentBinding,
|
|
85
|
+
placeholderPoToken,
|
|
86
|
+
poToken: poTokenResult.poToken,
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.warn("PoToken generation failed, continuing without it:", error);
|
|
90
|
+
return {
|
|
91
|
+
visitorData: contentBinding,
|
|
92
|
+
placeholderPoToken: "",
|
|
93
|
+
poToken: "",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Makes a proper player request to YouTube API
|
|
100
|
+
*/
|
|
101
|
+
async function makePlayerRequest(innertube: Innertube, videoId: string, reloadPlaybackContext?: any): Promise<any> {
|
|
102
|
+
const watchEndpoint = new YTNodes.NavigationEndpoint({
|
|
103
|
+
watchEndpoint: { videoId },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const extraArgs: any = {
|
|
107
|
+
playbackContext: {
|
|
108
|
+
adPlaybackContext: { pyv: true },
|
|
109
|
+
contentPlaybackContext: {
|
|
110
|
+
vis: 0,
|
|
111
|
+
splay: false,
|
|
112
|
+
lactMilliseconds: "-1",
|
|
113
|
+
signatureTimestamp: innertube.session.player?.signature_timestamp,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
contentCheckOk: true,
|
|
117
|
+
racyCheckOk: true,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (reloadPlaybackContext) {
|
|
121
|
+
extraArgs.playbackContext.reloadPlaybackContext = reloadPlaybackContext;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return watchEndpoint.call(innertube.actions, {
|
|
125
|
+
...extraArgs,
|
|
126
|
+
parse: true,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* YouTube VM shim
|
|
132
|
+
* This allows the SABR stream to execute YouTube's custom JavaScript for deciphering signatures and generating tokens
|
|
133
|
+
*/
|
|
134
|
+
Platform.shim.eval = async (data, env) => {
|
|
135
|
+
const properties = [];
|
|
136
|
+
|
|
137
|
+
if (env.n) properties.push(`n: exportedVars.nFunction("${env.n}")`);
|
|
138
|
+
if (env.sig) properties.push(`sig: exportedVars.sigFunction("${env.sig}")`);
|
|
139
|
+
|
|
140
|
+
const code = `${data.output}\nreturn { ${properties.join(", ")} }`;
|
|
141
|
+
return new Function(code)();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Creates a SABR audio stream for YouTube video download
|
|
146
|
+
* This provides better quality and more reliable streaming than standard methods
|
|
147
|
+
*/
|
|
148
|
+
export async function createSabrStream(
|
|
149
|
+
videoId: string,
|
|
150
|
+
innertube: Innertube,
|
|
151
|
+
options?: SabrPlaybackOptions,
|
|
152
|
+
): Promise<SabrAudioResult> {
|
|
153
|
+
try {
|
|
154
|
+
// Generate PoToken for authentication
|
|
155
|
+
const webPo = await generateWebPoToken(videoId);
|
|
156
|
+
|
|
157
|
+
// Make initial player request
|
|
158
|
+
const player = await makePlayerRequest(innertube, videoId);
|
|
159
|
+
|
|
160
|
+
const title = player.video_details?.title || "unknown";
|
|
161
|
+
|
|
162
|
+
const serverAbrStreamingUrl = await innertube.session.player?.decipher(player.streaming_data?.server_abr_streaming_url);
|
|
163
|
+
|
|
164
|
+
const ustreamerConfig =
|
|
165
|
+
player.player_config?.media_common_config.media_ustreamer_request_config?.video_playback_ustreamer_config;
|
|
166
|
+
|
|
167
|
+
if (!serverAbrStreamingUrl || !ustreamerConfig) {
|
|
168
|
+
throw new Error("Missing SABR streaming config");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const sabrFormats = player.streaming_data?.adaptive_formats.map((f: any) => buildSabrFormat(f)) || [];
|
|
172
|
+
|
|
173
|
+
const sabr = new SabrStream({
|
|
174
|
+
formats: sabrFormats,
|
|
175
|
+
serverAbrStreamingUrl,
|
|
176
|
+
videoPlaybackUstreamerConfig: ustreamerConfig,
|
|
177
|
+
poToken: webPo.poToken,
|
|
178
|
+
clientInfo: {
|
|
179
|
+
clientName: parseInt(
|
|
180
|
+
Constants.CLIENT_NAME_IDS[innertube.session.context.client.clientName as keyof typeof Constants.CLIENT_NAME_IDS],
|
|
181
|
+
),
|
|
182
|
+
clientVersion: innertube.session.context.client.clientVersion,
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Handle player response reload events
|
|
187
|
+
sabr.on("reloadPlayerResponse", async (ctx: any) => {
|
|
188
|
+
try {
|
|
189
|
+
const pr = await makePlayerRequest(innertube, videoId, ctx);
|
|
190
|
+
|
|
191
|
+
const url = await innertube.session.player?.decipher(pr.streaming_data?.server_abr_streaming_url);
|
|
192
|
+
|
|
193
|
+
const config = pr.player_config?.media_common_config.media_ustreamer_request_config?.video_playback_ustreamer_config;
|
|
194
|
+
|
|
195
|
+
if (url && config) {
|
|
196
|
+
sabr.setStreamingURL(url);
|
|
197
|
+
sabr.setUstreamerConfig(config);
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error("Failed to reload player response:", error);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Start the stream with audio preference
|
|
205
|
+
const { audioStream, selectedFormats } = await sabr.start({
|
|
206
|
+
// audioQuality: options?.audioQuality || "high",
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Convert Web Stream to Node.js Readable stream
|
|
210
|
+
const nodeStream = webStreamToNodeStream(audioStream);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
title,
|
|
214
|
+
stream: nodeStream,
|
|
215
|
+
format: {
|
|
216
|
+
mimeType: selectedFormats.audioFormat.mimeType || "audio/webm",
|
|
217
|
+
itag: selectedFormats.audioFormat.itag || 0,
|
|
218
|
+
contentLength: selectedFormats.audioFormat.contentLength || 0,
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw new Error(`SABR stream creation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Creates an output stream for writing downloaded content
|
|
228
|
+
*/
|
|
229
|
+
export function createOutputStream(videoTitle: string, mimeType: string): OutputStream {
|
|
230
|
+
const sanitizedTitle = videoTitle.replace(/[<>:"/\\|?*]/g, "_").substring(0, 100);
|
|
231
|
+
const extension = getExtensionFromMimeType(mimeType);
|
|
232
|
+
const fileName = `${sanitizedTitle}.${extension}`;
|
|
233
|
+
const filePath = join(tmpdir(), fileName);
|
|
234
|
+
|
|
235
|
+
const stream = createWriteStream(filePath);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
stream,
|
|
239
|
+
filePath,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Sanitizes a filename by removing invalid characters
|
|
245
|
+
*/
|
|
246
|
+
export function sanitizeFileName(name: string): string {
|
|
247
|
+
return name.replace(/[^\w\d]+/g, "_").slice(0, 128);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Converts bytes to megabytes
|
|
252
|
+
*/
|
|
253
|
+
export function bytesToMB(bytes: number): string {
|
|
254
|
+
return (bytes / 1024 / 1024).toFixed(2);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Gets file extension from MIME type
|
|
259
|
+
*/
|
|
260
|
+
function getExtensionFromMimeType(mimeType: string): string {
|
|
261
|
+
const mimeMap: { [key: string]: string } = {
|
|
262
|
+
"audio/mp4": "m4a",
|
|
263
|
+
"audio/webm": "webm",
|
|
264
|
+
"audio/ogg": "ogg",
|
|
265
|
+
"video/mp4": "mp4",
|
|
266
|
+
"video/webm": "webm",
|
|
267
|
+
"video/ogg": "ogv",
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return mimeMap[mimeType] || "bin";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Default sabr playback options
|
|
275
|
+
*/
|
|
276
|
+
export const DEFAULT_SABR_OPTIONS: SabrPlaybackOptions = {
|
|
277
|
+
preferWebM: true,
|
|
278
|
+
preferOpus: true,
|
|
279
|
+
videoQuality: "720p",
|
|
280
|
+
audioQuality: "high",
|
|
281
|
+
enabledTrackTypes: "VIDEO_AND_AUDIO",
|
|
282
|
+
};
|
|
@@ -2,78 +2,109 @@ import { Readable } from "stream";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Converts a Web ReadableStream to a Node.js Readable stream
|
|
5
|
+
* with proper cleanup handling to prevent "Controller is already closed" errors
|
|
5
6
|
*/
|
|
6
7
|
export function webStreamToNodeStream(webStream: ReadableStream): Readable {
|
|
8
|
+
let reader: ReadableStreamDefaultReader | null = null;
|
|
9
|
+
let pumpActive = true;
|
|
10
|
+
let abortController: AbortController | null = null;
|
|
11
|
+
|
|
7
12
|
const nodeStream = new Readable({
|
|
8
13
|
read() {
|
|
9
14
|
// This will be handled by the Web Stream reader
|
|
10
15
|
},
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const reader = webStream.getReader();
|
|
16
|
+
destroy(error: Error | null, callback: (error?: Error | null) => void) {
|
|
17
|
+
// Gracefully stop the pump when stream is destroyed
|
|
18
|
+
pumpActive = false;
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
nodeStream.push(Buffer.from(value));
|
|
20
|
+
// Cancel reader if active
|
|
21
|
+
if (reader) {
|
|
22
|
+
try {
|
|
23
|
+
// reader.cancel() will abort the fetch
|
|
24
|
+
reader.cancel().catch(() => {
|
|
25
|
+
// Ignore cancel errors
|
|
26
|
+
});
|
|
27
|
+
reader = null;
|
|
28
|
+
} catch {}
|
|
26
29
|
}
|
|
27
|
-
} catch (error) {
|
|
28
|
-
nodeStream.destroy(error as Error);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Start pumping data
|
|
33
|
-
pump();
|
|
34
30
|
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
// Abort any pending operations
|
|
32
|
+
if (abortController) {
|
|
33
|
+
try {
|
|
34
|
+
abortController.abort();
|
|
35
|
+
} catch {}
|
|
36
|
+
abortController = null;
|
|
37
|
+
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
* Converts a Web ReadableStream to a Node.js Readable stream with progress tracking
|
|
40
|
-
*/
|
|
41
|
-
export function webStreamToNodeStreamWithProgress(
|
|
42
|
-
webStream: ReadableStream,
|
|
43
|
-
progressCallback?: (bytesRead: number) => void,
|
|
44
|
-
): Readable {
|
|
45
|
-
const nodeStream = new Readable({
|
|
46
|
-
read() {
|
|
47
|
-
// This will be handled by the Web Stream reader
|
|
39
|
+
callback(error);
|
|
48
40
|
},
|
|
49
41
|
});
|
|
50
42
|
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
// Create abort controller for graceful shutdown
|
|
44
|
+
abortController = new AbortController();
|
|
45
|
+
|
|
46
|
+
// Create a reader from the Web Stream
|
|
47
|
+
reader = webStream.getReader();
|
|
53
48
|
|
|
49
|
+
// Read chunks and push to Node.js stream
|
|
54
50
|
const pump = async () => {
|
|
55
51
|
try {
|
|
56
|
-
while (
|
|
52
|
+
while (pumpActive && reader) {
|
|
57
53
|
const { done, value } = await reader.read();
|
|
54
|
+
|
|
55
|
+
// Check if pump was stopped during read
|
|
56
|
+
if (!pumpActive || !reader) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
|
|
58
60
|
if (done) {
|
|
59
61
|
nodeStream.push(null); // End the stream
|
|
60
62
|
break;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
bytesRead += buffer.length;
|
|
67
|
-
if (progressCallback) {
|
|
68
|
-
progressCallback(bytesRead);
|
|
65
|
+
if (value && pumpActive) {
|
|
66
|
+
nodeStream.push(Buffer.from(value));
|
|
69
67
|
}
|
|
70
68
|
}
|
|
71
69
|
} catch (error) {
|
|
72
|
-
|
|
70
|
+
// Only destroy if pump is still active and stream exists
|
|
71
|
+
if (pumpActive) {
|
|
72
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
73
|
+
|
|
74
|
+
// Ignore "Controller is already closed" and stream cancelled errors
|
|
75
|
+
if (
|
|
76
|
+
errorMsg.includes("Controller is already closed") ||
|
|
77
|
+
errorMsg.includes("already been cancelled") ||
|
|
78
|
+
errorMsg.includes("stream closed") ||
|
|
79
|
+
errorMsg.includes("aborted")
|
|
80
|
+
) {
|
|
81
|
+
// Stream was destroyed externally, just end cleanly
|
|
82
|
+
nodeStream.push(null);
|
|
83
|
+
} else {
|
|
84
|
+
// Real error, report it
|
|
85
|
+
nodeStream.destroy(error as Error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
// Cleanup reader when pump ends
|
|
90
|
+
try {
|
|
91
|
+
if (reader) {
|
|
92
|
+
await reader.cancel();
|
|
93
|
+
reader = null;
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
|
|
97
|
+
pumpActive = false;
|
|
73
98
|
}
|
|
74
99
|
};
|
|
75
100
|
|
|
76
|
-
|
|
101
|
+
// Start pumping data
|
|
102
|
+
pump().catch((error) => {
|
|
103
|
+
// Catch any unhandled promise rejection
|
|
104
|
+
if (pumpActive) {
|
|
105
|
+
console.error("[stream-converter] Pump error:", error);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
77
108
|
|
|
78
109
|
return nodeStream;
|
|
79
110
|
}
|
package/tsconfig.json
CHANGED
|
@@ -16,8 +16,12 @@
|
|
|
16
16
|
"allowSyntheticDefaultImports": true,
|
|
17
17
|
"experimentalDecorators": true,
|
|
18
18
|
"emitDecoratorMetadata": true,
|
|
19
|
-
"resolveJsonModule": true
|
|
19
|
+
"resolveJsonModule": true,
|
|
20
|
+
"paths": {
|
|
21
|
+
"googlevideo/sabr-stream": ["./node_modules/googlevideo/dist/src/exports/sabr-stream"],
|
|
22
|
+
"googlevideo/utils": ["./node_modules/googlevideo/dist/src/exports/utils"]
|
|
23
|
+
}
|
|
20
24
|
},
|
|
21
|
-
"include": ["src/*"
|
|
25
|
+
"include": ["src/*"],
|
|
22
26
|
"exclude": ["node_modules", "dist", "examples"]
|
|
23
27
|
}
|