streamify-audio 2.2.13 → 2.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/docs/plans/2026-02-22-stream-revamp-design.md +88 -0
- package/docs/plans/2026-02-22-stream-revamp-plan.md +814 -0
- package/package.json +1 -1
- package/src/config.js +14 -2
- package/src/discord/Player.js +74 -20
- package/src/discord/Stream.js +253 -87
- package/src/filters/ffmpeg.js +4 -3
- package/youtube-cookies.txt +26 -0
package/src/discord/Stream.js
CHANGED
|
@@ -11,6 +11,50 @@ try {
|
|
|
11
11
|
|
|
12
12
|
const { createAudioResource, StreamType } = voiceModule || {};
|
|
13
13
|
|
|
14
|
+
class StreamURLCache {
|
|
15
|
+
constructor(defaultTTL = 1800000) {
|
|
16
|
+
this._cache = new Map();
|
|
17
|
+
this._defaultTTL = defaultTTL;
|
|
18
|
+
this._cleanupInterval = setInterval(() => this._cleanup(), 300000);
|
|
19
|
+
if (this._cleanupInterval.unref) this._cleanupInterval.unref();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get(key) {
|
|
23
|
+
const entry = this._cache.get(key);
|
|
24
|
+
if (!entry) return null;
|
|
25
|
+
if (Date.now() > entry.expiry) {
|
|
26
|
+
this._cache.delete(key);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return { url: entry.url, headers: entry.headers };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set(key, url, headers = null, ttl = this._defaultTTL) {
|
|
33
|
+
this._cache.set(key, {
|
|
34
|
+
url,
|
|
35
|
+
headers,
|
|
36
|
+
expiry: Date.now() + ttl
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
invalidate(key) {
|
|
41
|
+
this._cache.delete(key);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get size() {
|
|
45
|
+
return this._cache.size;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_cleanup() {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
for (const [key, entry] of this._cache) {
|
|
51
|
+
if (now > entry.expiry) this._cache.delete(key);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const urlCache = new StreamURLCache();
|
|
57
|
+
|
|
14
58
|
class StreamController {
|
|
15
59
|
constructor(track, filters, config) {
|
|
16
60
|
this.track = track;
|
|
@@ -23,54 +67,120 @@ class StreamController {
|
|
|
23
67
|
this.startTime = null;
|
|
24
68
|
this.ytdlpError = '';
|
|
25
69
|
this.ffmpegError = '';
|
|
26
|
-
|
|
27
|
-
|
|
70
|
+
|
|
71
|
+
this._videoId = null;
|
|
72
|
+
this._directStreamUrl = null;
|
|
73
|
+
this._directStreamHeaders = null;
|
|
74
|
+
this._useDirectUrl = false;
|
|
75
|
+
this._prepared = false;
|
|
76
|
+
this._ffmpegSpawnTime = null;
|
|
77
|
+
|
|
28
78
|
this.metrics = {
|
|
29
79
|
metadata: 0,
|
|
80
|
+
urlExtract: 0,
|
|
30
81
|
spawn: 0,
|
|
31
82
|
firstByte: 0,
|
|
32
83
|
total: 0
|
|
33
84
|
};
|
|
34
85
|
}
|
|
35
86
|
|
|
36
|
-
async
|
|
37
|
-
if (this.destroyed)
|
|
38
|
-
throw new Error('Stream already destroyed');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (this.resource) {
|
|
42
|
-
return this.resource;
|
|
43
|
-
}
|
|
87
|
+
async prepare() {
|
|
88
|
+
if (this.destroyed) throw new Error('Stream already destroyed');
|
|
44
89
|
|
|
45
90
|
this.startTime = Date.now();
|
|
46
|
-
const startTimestamp = this.startTime;
|
|
47
91
|
const source = this.track.source || 'youtube';
|
|
48
|
-
|
|
49
92
|
let videoId = this.track._resolvedId || this.track.id;
|
|
50
93
|
|
|
51
|
-
// Skip metadata resolution if we already have it
|
|
52
94
|
if (source === 'spotify' && !this.track._resolvedId) {
|
|
53
95
|
log.info('STREAM', `Resolving Spotify track to YouTube: ${this.track.title}`);
|
|
54
96
|
try {
|
|
55
97
|
const spotify = require('../providers/spotify');
|
|
56
98
|
videoId = await spotify.resolveToYouTube(this.track.id, this.config);
|
|
57
99
|
this.track._resolvedId = videoId;
|
|
58
|
-
this.metrics.metadata = Date.now() -
|
|
100
|
+
this.metrics.metadata = Date.now() - this.startTime;
|
|
59
101
|
} catch (error) {
|
|
60
|
-
log.error('STREAM', `Spotify resolution failed: ${error.message}`);
|
|
61
102
|
throw new Error(`Failed to resolve Spotify track: ${error.message}`);
|
|
62
103
|
}
|
|
63
104
|
} else {
|
|
64
|
-
this.metrics.metadata = 0;
|
|
105
|
+
this.metrics.metadata = 0;
|
|
65
106
|
}
|
|
66
107
|
|
|
67
108
|
if (!videoId || videoId === 'undefined') {
|
|
68
|
-
throw new Error(`Invalid track ID: ${videoId} (source: ${source}
|
|
109
|
+
throw new Error(`Invalid track ID: ${videoId} (source: ${source})`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this._videoId = videoId;
|
|
113
|
+
const isYouTube = source === 'youtube' || source === 'spotify';
|
|
114
|
+
const isLive = this.track.isLive === true || this.track.duration === 0;
|
|
115
|
+
const isLocal = source === 'local';
|
|
116
|
+
|
|
117
|
+
if (isLocal || isLive || !isYouTube) {
|
|
118
|
+
this._useDirectUrl = false;
|
|
119
|
+
this._prepared = true;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const cacheKey = `${videoId}:${this.config.ytdlp.format}`;
|
|
124
|
+
const cached = urlCache.get(cacheKey);
|
|
125
|
+
|
|
126
|
+
if (cached) {
|
|
127
|
+
log.info('STREAM', `URL cache hit for ${videoId}`);
|
|
128
|
+
this._directStreamUrl = cached.url;
|
|
129
|
+
this._directStreamHeaders = cached.headers;
|
|
130
|
+
this._useDirectUrl = true;
|
|
131
|
+
this._prepared = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const extractStart = Date.now();
|
|
136
|
+
const maxRetries = this.config.stream?.maxRetries || 2;
|
|
137
|
+
|
|
138
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
139
|
+
try {
|
|
140
|
+
const extractedUrl = await this._extractUrl(videoId, this.config, isYouTube);
|
|
141
|
+
this.metrics.urlExtract = Date.now() - extractStart;
|
|
142
|
+
|
|
143
|
+
const ttl = (this.config.stream?.urlCacheTTL || 1800) * 1000;
|
|
144
|
+
urlCache.set(cacheKey, extractedUrl, this.track._headers, ttl);
|
|
145
|
+
|
|
146
|
+
this._directStreamUrl = extractedUrl;
|
|
147
|
+
this._directStreamHeaders = this.track._headers;
|
|
148
|
+
this._useDirectUrl = true;
|
|
149
|
+
this._prepared = true;
|
|
150
|
+
|
|
151
|
+
log.info('STREAM', `URL extracted for ${videoId} (${this.metrics.urlExtract}ms, attempt ${attempt})`);
|
|
152
|
+
return;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
log.warn('STREAM', `URL extraction attempt ${attempt}/${maxRetries} failed: ${error.message}`);
|
|
155
|
+
if (attempt === maxRetries) {
|
|
156
|
+
log.warn('STREAM', `All extraction attempts failed for ${videoId}, falling back to pipe mode`);
|
|
157
|
+
this._useDirectUrl = false;
|
|
158
|
+
this._prepared = true;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async start(seekPosition = 0) {
|
|
166
|
+
if (this.destroyed) throw new Error('Stream already destroyed');
|
|
167
|
+
if (this.resource) return this.resource;
|
|
168
|
+
|
|
169
|
+
if (!this._prepared) {
|
|
170
|
+
await this.prepare();
|
|
69
171
|
}
|
|
70
172
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
173
|
+
if (!this.startTime) this.startTime = Date.now();
|
|
174
|
+
const startTimestamp = this.startTime;
|
|
175
|
+
|
|
176
|
+
const videoId = this._videoId || this.track._resolvedId || this.track.id;
|
|
177
|
+
const source = this.track.source || 'youtube';
|
|
178
|
+
const isLive = this.track.isLive === true || this.track.duration === 0;
|
|
179
|
+
const isLocal = source === 'local';
|
|
180
|
+
const isYouTube = source === 'youtube' || source === 'spotify';
|
|
181
|
+
|
|
182
|
+
log.info('STREAM', `Creating stream for ${videoId} (${source}, mode: ${this._useDirectUrl ? 'direct' : isLocal ? 'local' : 'pipe'})`);
|
|
183
|
+
|
|
74
184
|
const filterNames = Object.keys(this.filters).filter(k => k !== 'start' && k !== '_trigger' && k !== 'volume');
|
|
75
185
|
if (filterNames.length > 0) {
|
|
76
186
|
const chain = filterNames.map(name => {
|
|
@@ -78,30 +188,14 @@ class StreamController {
|
|
|
78
188
|
let displayVal = typeof val === 'object' ? JSON.stringify(val) : val;
|
|
79
189
|
if (displayVal === true || displayVal === 'true') displayVal = 'ON';
|
|
80
190
|
return `[${name.toUpperCase()} (${displayVal})]`;
|
|
81
|
-
}).join('
|
|
191
|
+
}).join(' > ');
|
|
82
192
|
log.info('STREAM', `Filter Chain: ${chain}`);
|
|
83
193
|
}
|
|
84
194
|
|
|
85
|
-
const isYouTube = source === 'youtube' || source === 'spotify';
|
|
86
|
-
const isLive = this.track.isLive === true || this.track.duration === 0;
|
|
87
|
-
const isLocal = source === 'local';
|
|
88
|
-
|
|
89
|
-
let ytdlp;
|
|
90
|
-
let ffmpegIn;
|
|
91
|
-
|
|
92
195
|
const env = { ...process.env, PATH: '/usr/local/bin:/root/.deno/bin:' + process.env.PATH };
|
|
93
|
-
const
|
|
196
|
+
const bufferSize = this.config.stream?.bufferSize || '1M';
|
|
94
197
|
|
|
95
|
-
if (isLocal) {
|
|
96
|
-
// Skip yt-dlp for local files
|
|
97
|
-
ffmpegIn = 'pipe:0'; // We'll just pass the file path to ffmpeg -i
|
|
98
|
-
this.metrics.spawn = Date.now() - spawnStart;
|
|
99
|
-
} else if (this.track._directUrl && !isLive && seekPosition === 0) {
|
|
100
|
-
// OPTIMIZATION: Bypass yt-dlp if we already have a direct stream URL
|
|
101
|
-
ffmpegIn = this.track._directUrl;
|
|
102
|
-
log.info('STREAM', `Bypassing yt-dlp, using direct URL for ${videoId}`);
|
|
103
|
-
this.metrics.spawn = 0;
|
|
104
|
-
} else {
|
|
198
|
+
if (!this._useDirectUrl && !isLocal) {
|
|
105
199
|
let url;
|
|
106
200
|
if (source === 'soundcloud') {
|
|
107
201
|
url = this.track.uri || `https://api.soundcloud.com/tracks/${videoId}/stream`;
|
|
@@ -111,13 +205,7 @@ class StreamController {
|
|
|
111
205
|
url = `https://www.youtube.com/watch?v=${videoId}`;
|
|
112
206
|
}
|
|
113
207
|
|
|
114
|
-
|
|
115
|
-
if (isLive) {
|
|
116
|
-
formatString = 'bestaudio*/best';
|
|
117
|
-
} else {
|
|
118
|
-
formatString = this.config.ytdlp.format;
|
|
119
|
-
}
|
|
120
|
-
|
|
208
|
+
const formatString = isLive ? 'bestaudio*/best' : this.config.ytdlp.format;
|
|
121
209
|
const ytdlpArgs = [
|
|
122
210
|
'-f', formatString,
|
|
123
211
|
'--no-playlist',
|
|
@@ -125,7 +213,7 @@ class StreamController {
|
|
|
125
213
|
'--no-warnings',
|
|
126
214
|
'--no-cache-dir',
|
|
127
215
|
'--no-mtime',
|
|
128
|
-
'--buffer-size',
|
|
216
|
+
'--buffer-size', bufferSize,
|
|
129
217
|
'--quiet',
|
|
130
218
|
'--retries', '3',
|
|
131
219
|
'--fragment-retries', '3',
|
|
@@ -156,13 +244,11 @@ class StreamController {
|
|
|
156
244
|
}
|
|
157
245
|
|
|
158
246
|
this.ytdlp = spawn(this.config.ytdlpPath, ytdlpArgs, { env });
|
|
159
|
-
ffmpegIn = 'pipe:0';
|
|
160
247
|
}
|
|
161
248
|
|
|
162
249
|
const ffmpegFilters = { ...this.filters };
|
|
163
250
|
const ffmpegArgs = buildFfmpegArgs(ffmpegFilters, this.config);
|
|
164
|
-
|
|
165
|
-
// Input injection - only override if NOT using pipe:0 (buildFfmpegArgs already includes -i pipe:0)
|
|
251
|
+
|
|
166
252
|
if (isLocal) {
|
|
167
253
|
const filePath = this.track.absolutePath || videoId.replace('file://', '');
|
|
168
254
|
const pipeIndex = ffmpegArgs.indexOf('pipe:0');
|
|
@@ -173,23 +259,29 @@ class StreamController {
|
|
|
173
259
|
ffmpegArgs.splice(pipeIndex - 1, 0, '-ss', seekSeconds);
|
|
174
260
|
}
|
|
175
261
|
}
|
|
176
|
-
} else if (
|
|
177
|
-
// Direct URL - replace the pipe:0 input with the direct URL
|
|
262
|
+
} else if (this._useDirectUrl) {
|
|
178
263
|
const pipeIndex = ffmpegArgs.indexOf('pipe:0');
|
|
179
264
|
if (pipeIndex > 0) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
265
|
+
if (seekPosition > 0) {
|
|
266
|
+
const seekSeconds = (seekPosition / 1000).toFixed(3);
|
|
267
|
+
ffmpegArgs.splice(pipeIndex - 1, 0, '-ss', seekSeconds);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const newPipeIndex = ffmpegArgs.indexOf('pipe:0');
|
|
271
|
+
ffmpegArgs[newPipeIndex] = this._directStreamUrl;
|
|
272
|
+
|
|
273
|
+
if (this._directStreamHeaders) {
|
|
274
|
+
const headers = Object.entries(this._directStreamHeaders)
|
|
183
275
|
.map(([k, v]) => `${k}: ${v}`)
|
|
184
276
|
.join('\r\n');
|
|
185
|
-
ffmpegArgs.splice(
|
|
277
|
+
ffmpegArgs.splice(newPipeIndex - 1, 0, '-headers', headers);
|
|
186
278
|
}
|
|
187
279
|
}
|
|
188
280
|
}
|
|
189
|
-
// else: pipe:0 - buildFfmpegArgs already has -i pipe:0, nothing to change
|
|
190
281
|
|
|
282
|
+
this._ffmpegSpawnTime = Date.now();
|
|
191
283
|
this.ffmpeg = spawn(this.config.ffmpegPath, ffmpegArgs, { env });
|
|
192
|
-
this.metrics.spawn = Date.now() -
|
|
284
|
+
this.metrics.spawn = Date.now() - this._ffmpegSpawnTime;
|
|
193
285
|
|
|
194
286
|
if (this.ytdlp) {
|
|
195
287
|
this.ffmpeg.stdin.on('error', (err) => {
|
|
@@ -212,8 +304,6 @@ class StreamController {
|
|
|
212
304
|
this.ytdlpError += msg;
|
|
213
305
|
if (msg.includes('ERROR:') && !msg.includes('Retrying') && !msg.includes('Broken pipe')) {
|
|
214
306
|
log.error('YTDLP', msg.trim());
|
|
215
|
-
} else if (!msg.includes('[download]') && !msg.includes('ETA') && !msg.includes('[youtube]') && !msg.includes('Retrying fragment') && !msg.includes('Got error')) {
|
|
216
|
-
log.debug('YTDLP', msg.trim());
|
|
217
307
|
}
|
|
218
308
|
});
|
|
219
309
|
|
|
@@ -236,7 +326,17 @@ class StreamController {
|
|
|
236
326
|
}
|
|
237
327
|
});
|
|
238
328
|
|
|
239
|
-
|
|
329
|
+
try {
|
|
330
|
+
await this._waitForData();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
if (this._useDirectUrl && (error.message.includes('timed out') || error.message.includes('403'))) {
|
|
333
|
+
const cacheKey = `${videoId}:${this.config.ytdlp.format}`;
|
|
334
|
+
urlCache.invalidate(cacheKey);
|
|
335
|
+
log.warn('STREAM', `Invalidated cached URL for ${videoId} after error`);
|
|
336
|
+
}
|
|
337
|
+
this.destroy();
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
240
340
|
|
|
241
341
|
if (this.destroyed || !this.ffmpeg) {
|
|
242
342
|
throw new Error('Stream destroyed during initialization');
|
|
@@ -244,37 +344,104 @@ class StreamController {
|
|
|
244
344
|
|
|
245
345
|
this.resource = createAudioResource(this.ffmpeg.stdout, {
|
|
246
346
|
inputType: StreamType.OggOpus,
|
|
247
|
-
inlineVolume:
|
|
248
|
-
silencePaddingFrames:
|
|
347
|
+
inlineVolume: false,
|
|
348
|
+
silencePaddingFrames: 5
|
|
249
349
|
});
|
|
250
350
|
|
|
251
351
|
const elapsed = Date.now() - startTimestamp;
|
|
252
352
|
this.metrics.total = elapsed;
|
|
253
|
-
|
|
254
|
-
log.info('STREAM', `Ready ${elapsed}ms | Metrics: [Metadata: ${this.metrics.metadata}ms | Spawn: ${this.metrics.spawn}ms | FirstByte: ${this.metrics.firstByte}ms]`);
|
|
353
|
+
|
|
354
|
+
log.info('STREAM', `Ready ${elapsed}ms | Metrics: [Metadata: ${this.metrics.metadata}ms | URLExtract: ${this.metrics.urlExtract}ms | Spawn: ${this.metrics.spawn}ms | FirstByte: ${this.metrics.firstByte}ms]`);
|
|
255
355
|
|
|
256
356
|
return this.resource;
|
|
257
357
|
}
|
|
258
358
|
|
|
259
|
-
|
|
359
|
+
async create(seekPosition = 0) {
|
|
360
|
+
return this.start(seekPosition);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_extractUrl(videoId, config, isYouTube) {
|
|
364
|
+
return new Promise((resolve, reject) => {
|
|
365
|
+
const url = `https://www.youtube.com/watch?v=${videoId}`;
|
|
366
|
+
const args = [
|
|
367
|
+
'--get-url',
|
|
368
|
+
'-f', config.ytdlp.format,
|
|
369
|
+
'--no-playlist',
|
|
370
|
+
'--no-check-certificates',
|
|
371
|
+
'--no-warnings',
|
|
372
|
+
'--no-cache-dir',
|
|
373
|
+
url
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
if (isYouTube) {
|
|
377
|
+
args.push('--extractor-args', 'youtube:player_client=web_creator');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (config.cookiesPath) {
|
|
381
|
+
args.unshift('--cookies', config.cookiesPath);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (config.sponsorblock?.enabled !== false && isYouTube) {
|
|
385
|
+
const categories = config.sponsorblock?.categories || ['sponsor', 'selfpromo'];
|
|
386
|
+
args.push('--sponsorblock-remove', categories.join(','));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const env = { ...process.env, PATH: '/usr/local/bin:/root/.deno/bin:' + process.env.PATH };
|
|
390
|
+
const proc = spawn(config.ytdlpPath, args, { env });
|
|
391
|
+
|
|
392
|
+
let stdout = '';
|
|
393
|
+
let stderr = '';
|
|
394
|
+
const timeout = setTimeout(() => {
|
|
395
|
+
proc.kill('SIGKILL');
|
|
396
|
+
reject(new Error('URL extraction timed out'));
|
|
397
|
+
}, 15000);
|
|
398
|
+
|
|
399
|
+
proc.stdout.on('data', (data) => { stdout += data; });
|
|
400
|
+
proc.stderr.on('data', (data) => { stderr += data; });
|
|
401
|
+
|
|
402
|
+
proc.on('close', (code) => {
|
|
403
|
+
clearTimeout(timeout);
|
|
404
|
+
if (code !== 0) {
|
|
405
|
+
return reject(new Error(`yt-dlp --get-url failed (code ${code}): ${stderr.slice(-200)}`));
|
|
406
|
+
}
|
|
407
|
+
const urls = stdout.trim().split('\n').filter(Boolean);
|
|
408
|
+
if (urls.length === 0) {
|
|
409
|
+
return reject(new Error('yt-dlp returned no URLs'));
|
|
410
|
+
}
|
|
411
|
+
resolve(urls[0]);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
proc.on('error', (err) => {
|
|
415
|
+
clearTimeout(timeout);
|
|
416
|
+
reject(new Error(`yt-dlp spawn failed: ${err.message}`));
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_waitForData() {
|
|
260
422
|
const ffmpeg = this.ffmpeg;
|
|
261
423
|
const ytdlp = this.ytdlp;
|
|
424
|
+
const isLive = this.track.isLive === true || this.track.duration === 0;
|
|
425
|
+
const timeoutMs = isLive
|
|
426
|
+
? (this.config.stream?.liveDataTimeout || 15000)
|
|
427
|
+
: (this.config.stream?.dataTimeout || 8000);
|
|
262
428
|
|
|
263
429
|
return new Promise((resolve, reject) => {
|
|
264
430
|
if (!ffmpeg) return resolve();
|
|
265
431
|
|
|
266
|
-
|
|
432
|
+
let resolved = false;
|
|
433
|
+
|
|
267
434
|
const timeout = setTimeout(() => {
|
|
268
|
-
|
|
269
|
-
|
|
435
|
+
if (resolved) return;
|
|
436
|
+
resolved = true;
|
|
437
|
+
if (ffmpeg.stdout) ffmpeg.stdout.removeListener('readable', onReadable);
|
|
438
|
+
reject(new Error(`Stream timed out after ${timeoutMs}ms waiting for audio data`));
|
|
270
439
|
}, timeoutMs);
|
|
271
440
|
|
|
272
|
-
let resolved = false;
|
|
273
|
-
|
|
274
441
|
const onReadable = () => {
|
|
275
442
|
if (resolved) return;
|
|
276
443
|
resolved = true;
|
|
277
|
-
this.metrics.firstByte = Date.now() -
|
|
444
|
+
this.metrics.firstByte = Date.now() - this._ffmpegSpawnTime;
|
|
278
445
|
clearTimeout(timeout);
|
|
279
446
|
if (ffmpeg.stdout) ffmpeg.stdout.removeListener('readable', onReadable);
|
|
280
447
|
resolve();
|
|
@@ -284,19 +451,20 @@ class StreamController {
|
|
|
284
451
|
ffmpeg.stdout.on('readable', onReadable);
|
|
285
452
|
}
|
|
286
453
|
|
|
287
|
-
ffmpeg.on('close', () => {
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (this.destroyed) {
|
|
294
|
-
return reject(new Error('Stream destroyed during initialization'));
|
|
295
|
-
}
|
|
454
|
+
ffmpeg.on('close', (code) => {
|
|
455
|
+
if (resolved) return;
|
|
456
|
+
resolved = true;
|
|
457
|
+
clearTimeout(timeout);
|
|
458
|
+
if (ffmpeg.stdout) ffmpeg.stdout.removeListener('readable', onReadable);
|
|
296
459
|
|
|
297
|
-
|
|
298
|
-
reject(new Error(
|
|
460
|
+
if (this.destroyed) {
|
|
461
|
+
return reject(new Error('Stream destroyed during initialization'));
|
|
299
462
|
}
|
|
463
|
+
|
|
464
|
+
const sourceErr = ytdlp
|
|
465
|
+
? `yt-dlp stderr: ${this.ytdlpError.slice(-200) || 'none'}`
|
|
466
|
+
: `ffmpeg stderr: ${this.ffmpegError.slice(-200) || 'none'}`;
|
|
467
|
+
reject(new Error(`ffmpeg closed before producing data. ${sourceErr}`));
|
|
300
468
|
});
|
|
301
469
|
|
|
302
470
|
if (ytdlp) {
|
|
@@ -305,12 +473,10 @@ class StreamController {
|
|
|
305
473
|
resolved = true;
|
|
306
474
|
clearTimeout(timeout);
|
|
307
475
|
if (ffmpeg.stdout) ffmpeg.stdout.removeListener('readable', onReadable);
|
|
308
|
-
|
|
309
476
|
if (this.destroyed) {
|
|
310
477
|
return reject(new Error('Stream destroyed during initialization'));
|
|
311
478
|
}
|
|
312
|
-
|
|
313
|
-
reject(new Error(`yt-dlp failed with code ${code}`));
|
|
479
|
+
reject(new Error(`yt-dlp failed with code ${code}: ${this.ytdlpError.slice(-200)}`));
|
|
314
480
|
}
|
|
315
481
|
});
|
|
316
482
|
}
|
|
@@ -355,4 +521,4 @@ function createStream(track, filters, config) {
|
|
|
355
521
|
return new StreamController(track, filters, config);
|
|
356
522
|
}
|
|
357
523
|
|
|
358
|
-
module.exports = { createStream, StreamController };
|
|
524
|
+
module.exports = { createStream, StreamController, urlCache };
|
package/src/filters/ffmpeg.js
CHANGED
|
@@ -69,7 +69,7 @@ function buildEqualizer(bands) {
|
|
|
69
69
|
function buildFfmpegArgs(filters = {}, config = {}) {
|
|
70
70
|
filters = filters || {};
|
|
71
71
|
const args = [
|
|
72
|
-
'-thread_queue_size', '
|
|
72
|
+
'-thread_queue_size', '4096',
|
|
73
73
|
'-i', 'pipe:0',
|
|
74
74
|
'-vn',
|
|
75
75
|
'-sn'
|
|
@@ -260,8 +260,9 @@ function buildFfmpegArgs(filters = {}, config = {}) {
|
|
|
260
260
|
'-acodec', 'libopus',
|
|
261
261
|
'-b:a', bitrate,
|
|
262
262
|
'-vbr', config.audio?.vbr !== false ? 'on' : 'off',
|
|
263
|
-
'-compression_level', (config.audio?.compressionLevel ??
|
|
264
|
-
'-
|
|
263
|
+
'-compression_level', (config.audio?.compressionLevel ?? 5).toString(),
|
|
264
|
+
'-frame_duration', '20',
|
|
265
|
+
'-application', config.audio?.application || 'lowdelay',
|
|
265
266
|
'-f', 'ogg'
|
|
266
267
|
);
|
|
267
268
|
} else if (format === 'mp3') {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Netscape HTTP Cookie File
|
|
2
|
+
# This file is generated by yt-dlp. Do not edit.
|
|
3
|
+
|
|
4
|
+
.youtube.com TRUE / TRUE 1800398983 __Secure-YENID 12.YTE=UfQjguVvlx3WLiJJHSjg4nPS-N-5O3Xj8RZj-wlLmRdjAvaF3TRdMzSmyUmo82Z4P-tggpd89U5d3hMDN886ZuUZUzi5XdEFEmHjL0YrTPBwPhD6pZx5CJ-3bNb3Eb_7eZuq_YlqQb0OX6Hvu2yYtWfWZPq7dZUjnJarOqJB0UaWu4jlsn27LkAmA9ecZK7Tj0NXQsIiDMR_y8pHe-kOfQ9LhLBeMnUWvR7GXsSOo_NqyfHrNnu4W8YnBd45ZUQIhwQ91X7lhuTkbBj3hYNJsWv0G7yda1NfB8j-nh8ZkordnGPIyv5QlCTd745j2DiGeS1GSa2jP2bClx7SoQxTjQ
|
|
5
|
+
.youtube.com TRUE / FALSE 0 PREF f6=40000000&tz=UTC&hl=en
|
|
6
|
+
.youtube.com TRUE / TRUE 1782082203 NID 527=aqYd85605VNHKMN7IAs-QcA6YfpJm6LGDAjKm929dxg81_ly4EHuc_A0jCgct9sjP-yDkNobeQPSES7fdZYHdzmlnHO_gGQ-oVCIbS3GQvSB89DCDEPB-toK2K62rr6iUVoTg3lcUG5xBhVXwoPPOD6VWWqegOp-IOhJhTb7Ijp_899mbKpj-4Elx8vNwUL0X-lJAe5Th-4Mxv9lDynXDH8axGjFhMHboeuTOHiIXRg10-4BoqyhAGFDhKNohiMwDc66EraGHim03-ZM
|
|
7
|
+
.youtube.com TRUE / TRUE 1800826170 __Secure-1PSIDTS sidts-CjUB7I_69MoGgU9hYcmJtLk5dHx1d0LjE7gLaoN86V6V3M8eyIyUH6LOPPFj2llBVQAVsulbqRAA
|
|
8
|
+
.youtube.com TRUE / TRUE 1800826170 __Secure-3PSIDTS sidts-CjUB7I_69MoGgU9hYcmJtLk5dHx1d0LjE7gLaoN86V6V3M8eyIyUH6LOPPFj2llBVQAVsulbqRAA
|
|
9
|
+
.youtube.com TRUE / FALSE 1803850170 HSID Ab4swI6vjvKUnUkhu
|
|
10
|
+
.youtube.com TRUE / TRUE 1803850170 SSID AaxSz8h17yLe-BmmX
|
|
11
|
+
.youtube.com TRUE / FALSE 1803850170 APISID pQgk6uQUwAqRtThp/Ape45AAtgBDxnzVco
|
|
12
|
+
.youtube.com TRUE / TRUE 1803850170 SAPISID y_RgVkrBXATC2XrL/Avlu8CWS6dwZ7LTEg
|
|
13
|
+
.youtube.com TRUE / TRUE 1803850170 __Secure-1PAPISID y_RgVkrBXATC2XrL/Avlu8CWS6dwZ7LTEg
|
|
14
|
+
.youtube.com TRUE / TRUE 1803850170 __Secure-3PAPISID y_RgVkrBXATC2XrL/Avlu8CWS6dwZ7LTEg
|
|
15
|
+
.youtube.com TRUE / FALSE 1803850170 SID g.a0006AhyolP4RoeyrQXcHsFnRGkuVLVIZ9QQsgpwFaXGd822GTtb98Ev6KQIph7SQPbMD4aekgACgYKAaMSARMSFQHGX2MijLBjWKdOhQm5VmUkh9wlRhoVAUF8yKqE_WWJQO8PgiZpXVV1MiB30076
|
|
16
|
+
.youtube.com TRUE / TRUE 1803850170 __Secure-1PSID g.a0006AhyolP4RoeyrQXcHsFnRGkuVLVIZ9QQsgpwFaXGd822GTtb9khH10B1YSjuMht-R8LNjAACgYKAQ8SARMSFQHGX2MiWX5D8_L5MAZK1TLabPQMghoVAUF8yKoFUiQLoi8zHLuskryhcx1P0076
|
|
17
|
+
.youtube.com TRUE / TRUE 1803850170 __Secure-3PSID g.a0006AhyolP4RoeyrQXcHsFnRGkuVLVIZ9QQsgpwFaXGd822GTtbc5VtVrqvDy9fWzO4vguctgACgYKAUkSARMSFQHGX2MinKYgh3CtvTW5E5T8SwXiERoVAUF8yKqzIJTS4ajNX5uO8nh3r0j10076
|
|
18
|
+
.youtube.com TRUE / TRUE 1803850170 LOGIN_INFO AFmmF2swRQIgH6IrdLYFOhW8xu4opIUHTGxAf33LzI5EXw6pdoqsh_gCIQCxYHQL4VZEyb3nE-rhXM6VU2ua99G4AhzKl4JJlzZBEQ:QUQ3MjNmeFpJOS1rVDgtM2I4cm50Z3M4NUplLTlSbXV4OFl4MTM5eUtHX3FEcVNmT2ltVFlSWmpQdlR2QTVqR29mZlUtTVh4OEhkOGw4Rkd5d2tQZm52NXJmQzdrMFJPVk5uYTFLNFFMZWlGelJhRVk1TkVaMm9Uby0xWlcwR3ozYkwwY2lWT21pV2dpU0lsWW5saFhMektISWxwUkRUcXl3
|
|
19
|
+
.youtube.com TRUE / FALSE 1803305074 SIDCC AKEyXzW6XAqY9tNhDWlFzlv-TcDtmpxQxQjOpTwkeLq7ddP20Ecx5ttbRGa7K7WKJVXYBIsbGTE
|
|
20
|
+
.youtube.com TRUE / TRUE 1803305074 __Secure-1PSIDCC AKEyXzVdeiA5hPJOoWk_iVZXhBC3-3D4ZAlHjwkIdtnQ-y_iHxOeAdYucag2Dv-EMebAtINo-Q
|
|
21
|
+
.youtube.com TRUE / TRUE 1803305074 __Secure-3PSIDCC AKEyXzX9LV_1LP1Z1T6SCow5CKbPxteZg-XPIOt3o2Uu6OlsIn6Z2S3gRZBgUTHpaiRcVbPYbg
|
|
22
|
+
.youtube.com TRUE / TRUE 1787290794 __Secure-YNID 16.YT=uhIB3j65_j5F-nN_AIVu93J6J4tqe4fviGdBQuGk0X5EW7VRESfm4qMadhbgM04D7x1YyYRXKI0UIHJjiRMWqsP5PmT_qrHoTUSn4MOLA-FPeX09ELODYdpGvps9F6B3EFC2jC4MXYiWVY6KGZRiEoYBh_KWkh4Suwgn38LgfqN0E4Xv3YAzCB390qeKtLVkaqlQDUIvhnqGBPrObW-DzawPn00gHQph8VM2Zrklq_BzRYVaZHKHs2WkKlHqLljxwOiNXcsLrIkLqGsCgKR_onVOnlWN2gd0zojNv0VyuwYgfB9GBOLzweTnTWBZmp9IPWRA39DH4zMFerdTND-Yfg
|
|
23
|
+
.youtube.com TRUE / TRUE 0 YSC -LHUdUOZvC4
|
|
24
|
+
.youtube.com TRUE / TRUE 1787321074 VISITOR_INFO1_LIVE AuEVGsoJz2Y
|
|
25
|
+
.youtube.com TRUE / TRUE 1787321074 VISITOR_PRIVACY_METADATA CgJVUxIEGgAgVQ%3D%3D
|
|
26
|
+
.youtube.com TRUE / TRUE 1787290794 __Secure-ROLLOUT_TOKEN CIbdoKq8kOipXhC__cjij6WSAxiZi8-rseySAw%3D%3D
|