streamify-audio 2.2.6 → 2.2.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/package.json +1 -1
- package/src/discord/Stream.js +23 -22
- package/src/filters/ffmpeg.js +13 -11
- package/src/providers/bandcamp.js +1 -0
- package/src/providers/mixcloud.js +1 -0
- package/src/providers/soundcloud.js +2 -0
- package/src/providers/spotify.js +5 -0
- package/src/providers/twitch.js +3 -0
- package/src/providers/youtube.js +40 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "streamify-audio",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.8",
|
|
4
4
|
"description": "Dual-mode audio library: HTTP streaming proxy + Discord player (Lavalink alternative). Supports YouTube, Spotify, SoundCloud with audio filters.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
package/src/discord/Stream.js
CHANGED
|
@@ -192,6 +192,18 @@ class StreamController {
|
|
|
192
192
|
this.metrics.spawn = Date.now() - spawnStart;
|
|
193
193
|
|
|
194
194
|
if (this.ytdlp) {
|
|
195
|
+
this.ffmpeg.stdin.on('error', (err) => {
|
|
196
|
+
if (err.code !== 'EPIPE' && err.code !== 'ERR_STREAM_DESTROYED') {
|
|
197
|
+
log.error('STREAM', `ffmpeg stdin error: ${err.message}`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
this.ytdlp.stdout.on('error', (err) => {
|
|
202
|
+
if (err.code !== 'EPIPE' && err.code !== 'ERR_STREAM_DESTROYED') {
|
|
203
|
+
log.error('STREAM', `yt-dlp stdout error: ${err.message}`);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
195
207
|
this.ytdlp.stdout.pipe(this.ffmpeg.stdin);
|
|
196
208
|
|
|
197
209
|
this.ytdlp.stderr.on('data', (data) => {
|
|
@@ -232,7 +244,8 @@ class StreamController {
|
|
|
232
244
|
|
|
233
245
|
this.resource = createAudioResource(this.ffmpeg.stdout, {
|
|
234
246
|
inputType: StreamType.OggOpus,
|
|
235
|
-
inlineVolume:
|
|
247
|
+
inlineVolume: true,
|
|
248
|
+
silencePaddingFrames: 10
|
|
236
249
|
});
|
|
237
250
|
|
|
238
251
|
const elapsed = Date.now() - startTimestamp;
|
|
@@ -246,7 +259,6 @@ class StreamController {
|
|
|
246
259
|
_waitForData(isLive = false) {
|
|
247
260
|
const ffmpeg = this.ffmpeg;
|
|
248
261
|
const ytdlp = this.ytdlp;
|
|
249
|
-
const MIN_BUFFER_SIZE = 32 * 1024;
|
|
250
262
|
|
|
251
263
|
return new Promise((resolve, reject) => {
|
|
252
264
|
if (!ffmpeg) return resolve();
|
|
@@ -258,36 +270,25 @@ class StreamController {
|
|
|
258
270
|
}, timeoutMs);
|
|
259
271
|
|
|
260
272
|
let resolved = false;
|
|
261
|
-
let bufferedSize = 0;
|
|
262
|
-
let firstByteRecorded = false;
|
|
263
273
|
|
|
264
|
-
const
|
|
274
|
+
const onReadable = () => {
|
|
265
275
|
if (resolved) return;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
bufferedSize = ffmpeg.stdout ? ffmpeg.stdout.readableLength : 0;
|
|
273
|
-
|
|
274
|
-
if (bufferedSize >= MIN_BUFFER_SIZE) {
|
|
275
|
-
resolved = true;
|
|
276
|
-
clearTimeout(timeout);
|
|
277
|
-
if (ffmpeg.stdout) ffmpeg.stdout.removeListener('readable', checkBuffer);
|
|
278
|
-
resolve();
|
|
279
|
-
}
|
|
276
|
+
resolved = true;
|
|
277
|
+
this.metrics.firstByte = Date.now() - (this.startTime + this.metrics.metadata + this.metrics.spawn);
|
|
278
|
+
clearTimeout(timeout);
|
|
279
|
+
if (ffmpeg.stdout) ffmpeg.stdout.removeListener('readable', onReadable);
|
|
280
|
+
resolve();
|
|
280
281
|
};
|
|
281
282
|
|
|
282
283
|
if (ffmpeg.stdout) {
|
|
283
|
-
ffmpeg.stdout.on('readable',
|
|
284
|
+
ffmpeg.stdout.on('readable', onReadable);
|
|
284
285
|
}
|
|
285
286
|
|
|
286
287
|
ffmpeg.on('close', () => {
|
|
287
288
|
if (!resolved) {
|
|
288
289
|
resolved = true;
|
|
289
290
|
clearTimeout(timeout);
|
|
290
|
-
if (ffmpeg.stdout) ffmpeg.stdout.removeListener('readable',
|
|
291
|
+
if (ffmpeg.stdout) ffmpeg.stdout.removeListener('readable', onReadable);
|
|
291
292
|
|
|
292
293
|
if (this.destroyed) {
|
|
293
294
|
return reject(new Error('Stream destroyed during initialization'));
|
|
@@ -303,7 +304,7 @@ class StreamController {
|
|
|
303
304
|
if (!resolved && code !== 0 && code !== null) {
|
|
304
305
|
resolved = true;
|
|
305
306
|
clearTimeout(timeout);
|
|
306
|
-
if (ffmpeg.stdout) ffmpeg.stdout.removeListener('readable',
|
|
307
|
+
if (ffmpeg.stdout) ffmpeg.stdout.removeListener('readable', onReadable);
|
|
307
308
|
|
|
308
309
|
if (this.destroyed) {
|
|
309
310
|
return reject(new Error('Stream destroyed during initialization'));
|
package/src/filters/ffmpeg.js
CHANGED
|
@@ -69,16 +69,12 @@ function buildEqualizer(bands) {
|
|
|
69
69
|
function buildFfmpegArgs(filters = {}, config = {}) {
|
|
70
70
|
filters = filters || {};
|
|
71
71
|
const args = [
|
|
72
|
-
'-thread_queue_size', '
|
|
73
|
-
'-probesize', '128K',
|
|
74
|
-
'-analyzeduration', '0',
|
|
75
|
-
'-fflags', '+discardcorrupt',
|
|
76
|
-
'-copyts',
|
|
77
|
-
'-start_at_zero',
|
|
72
|
+
'-thread_queue_size', '512',
|
|
78
73
|
'-i', 'pipe:0',
|
|
79
|
-
'-vn'
|
|
74
|
+
'-vn',
|
|
75
|
+
'-sn'
|
|
80
76
|
];
|
|
81
|
-
const audioFilters = [
|
|
77
|
+
const audioFilters = [];
|
|
82
78
|
|
|
83
79
|
if (filters.equalizer && Array.isArray(filters.equalizer)) {
|
|
84
80
|
const eq = buildEqualizer(filters.equalizer);
|
|
@@ -251,9 +247,15 @@ function buildFfmpegArgs(filters = {}, config = {}) {
|
|
|
251
247
|
}
|
|
252
248
|
|
|
253
249
|
const bitrate = config.audio?.bitrate || '128k';
|
|
254
|
-
const format = config.audio?.format || '
|
|
250
|
+
const format = config.audio?.format || 'raw';
|
|
255
251
|
|
|
256
|
-
if (format === '
|
|
252
|
+
if (format === 'raw' || format === 'pcm') {
|
|
253
|
+
args.push(
|
|
254
|
+
'-ar', '48000',
|
|
255
|
+
'-ac', '2',
|
|
256
|
+
'-f', 's16le'
|
|
257
|
+
);
|
|
258
|
+
} else if (format === 'opus') {
|
|
257
259
|
args.push(
|
|
258
260
|
'-acodec', 'libopus',
|
|
259
261
|
'-b:a', bitrate,
|
|
@@ -267,7 +269,7 @@ function buildFfmpegArgs(filters = {}, config = {}) {
|
|
|
267
269
|
} else if (format === 'aac') {
|
|
268
270
|
args.push('-acodec', 'aac', '-b:a', bitrate, '-f', 'adts');
|
|
269
271
|
} else {
|
|
270
|
-
args.push('-
|
|
272
|
+
args.push('-ar', '48000', '-ac', '2', '-f', 's16le');
|
|
271
273
|
}
|
|
272
274
|
|
|
273
275
|
args.push('-');
|
|
@@ -33,6 +33,7 @@ async function getInfo(url, config) {
|
|
|
33
33
|
title: data.title,
|
|
34
34
|
duration: data.duration || 0,
|
|
35
35
|
author: data.uploader || data.artist,
|
|
36
|
+
authorUrl: data.uploader_url || null,
|
|
36
37
|
thumbnail: data.thumbnail,
|
|
37
38
|
uri: data.webpage_url,
|
|
38
39
|
streamUrl: `/bandcamp/stream/${Buffer.from(url).toString('base64')}`,
|
|
@@ -33,6 +33,7 @@ async function getInfo(url, config) {
|
|
|
33
33
|
title: data.title,
|
|
34
34
|
duration: data.duration || 0,
|
|
35
35
|
author: data.uploader || data.user?.username,
|
|
36
|
+
authorUrl: data.uploader_url || null,
|
|
36
37
|
thumbnail: data.thumbnail,
|
|
37
38
|
uri: data.webpage_url,
|
|
38
39
|
streamUrl: `/mixcloud/stream/${Buffer.from(url).toString('base64')}`,
|
|
@@ -37,6 +37,7 @@ async function search(query, limit, config) {
|
|
|
37
37
|
title: entry.title,
|
|
38
38
|
duration: entry.duration,
|
|
39
39
|
author: entry.uploader,
|
|
40
|
+
authorUrl: entry.uploader_url || null,
|
|
40
41
|
thumbnail: entry.thumbnails?.[0]?.url,
|
|
41
42
|
uri: entry.url || entry.webpage_url,
|
|
42
43
|
streamUrl: `/soundcloud/stream/${entry.id}`,
|
|
@@ -82,6 +83,7 @@ async function getInfo(trackId, config) {
|
|
|
82
83
|
title: data.title,
|
|
83
84
|
duration: data.duration,
|
|
84
85
|
author: data.uploader,
|
|
86
|
+
authorUrl: data.uploader_url || null,
|
|
85
87
|
thumbnail: data.thumbnail,
|
|
86
88
|
uri: data.webpage_url,
|
|
87
89
|
streamUrl: `/soundcloud/stream/${data.id}`,
|
package/src/providers/spotify.js
CHANGED
|
@@ -62,6 +62,7 @@ async function search(query, limit, config) {
|
|
|
62
62
|
id: track.id,
|
|
63
63
|
title: track.name,
|
|
64
64
|
author: track.artists.map(a => a.name).join(', '),
|
|
65
|
+
authorUrl: track.artists[0]?.external_urls?.spotify || null,
|
|
65
66
|
album: track.album.name,
|
|
66
67
|
duration: Math.floor(track.duration_ms / 1000),
|
|
67
68
|
thumbnail: track.album.images?.[0]?.url,
|
|
@@ -83,6 +84,7 @@ async function getInfo(trackId, config) {
|
|
|
83
84
|
id: track.id,
|
|
84
85
|
title: track.name,
|
|
85
86
|
author: track.artists.map(a => a.name).join(', '),
|
|
87
|
+
authorUrl: track.artists[0]?.external_urls?.spotify || null,
|
|
86
88
|
album: track.album.name,
|
|
87
89
|
duration: Math.floor(track.duration_ms / 1000),
|
|
88
90
|
thumbnail: track.album.images?.[0]?.url,
|
|
@@ -142,6 +144,7 @@ async function getPlaylist(playlistId, config) {
|
|
|
142
144
|
id: item.track.id,
|
|
143
145
|
title: item.track.name,
|
|
144
146
|
author: item.track.artists.map(a => a.name).join(', '),
|
|
147
|
+
authorUrl: item.track.artists[0]?.external_urls?.spotify || null,
|
|
145
148
|
album: item.track.album.name,
|
|
146
149
|
duration: Math.floor(item.track.duration_ms / 1000),
|
|
147
150
|
thumbnail: item.track.album.images?.[0]?.url,
|
|
@@ -171,6 +174,7 @@ async function getAlbum(albumId, config) {
|
|
|
171
174
|
id: track.id,
|
|
172
175
|
title: track.name,
|
|
173
176
|
author: track.artists.map(a => a.name).join(', '),
|
|
177
|
+
authorUrl: track.artists[0]?.external_urls?.spotify || null,
|
|
174
178
|
album: data.name,
|
|
175
179
|
duration: Math.floor(track.duration_ms / 1000),
|
|
176
180
|
thumbnail: data.images?.[0]?.url,
|
|
@@ -200,6 +204,7 @@ async function getRecommendations(trackId, limit, config) {
|
|
|
200
204
|
id: track.id,
|
|
201
205
|
title: track.name,
|
|
202
206
|
author: track.artists.map(a => a.name).join(', '),
|
|
207
|
+
authorUrl: track.artists[0]?.external_urls?.spotify || null,
|
|
203
208
|
album: track.album.name,
|
|
204
209
|
duration: Math.floor(track.duration_ms / 1000),
|
|
205
210
|
thumbnail: track.album.images?.[0]?.url,
|
package/src/providers/twitch.js
CHANGED
|
@@ -28,11 +28,14 @@ async function getInfo(url, config) {
|
|
|
28
28
|
}
|
|
29
29
|
try {
|
|
30
30
|
const data = JSON.parse(stdout);
|
|
31
|
+
const authorUrl = data.uploader_url || data.channel_url ||
|
|
32
|
+
(data.channel ? `https://www.twitch.tv/${data.channel}` : null);
|
|
31
33
|
resolve({
|
|
32
34
|
id: data.id,
|
|
33
35
|
title: data.title,
|
|
34
36
|
duration: data.duration || 0,
|
|
35
37
|
author: data.uploader || data.channel,
|
|
38
|
+
authorUrl,
|
|
36
39
|
thumbnail: data.thumbnail,
|
|
37
40
|
uri: data.webpage_url,
|
|
38
41
|
streamUrl: `/twitch/stream/${Buffer.from(url).toString('base64')}`,
|
package/src/providers/youtube.js
CHANGED
|
@@ -64,11 +64,15 @@ async function search(query, limit, config, options = {}) {
|
|
|
64
64
|
?.filter(f => f.vcodec === 'none' && f.acodec !== 'none')
|
|
65
65
|
.sort((a, b) => (b.abr || 0) - (a.abr || 0))[0];
|
|
66
66
|
|
|
67
|
+
const authorUrl = entry.channel_url || entry.uploader_url ||
|
|
68
|
+
(entry.channel_id ? `https://www.youtube.com/channel/${entry.channel_id}` : null);
|
|
69
|
+
|
|
67
70
|
return {
|
|
68
71
|
id: entry.id,
|
|
69
72
|
title: entry.title,
|
|
70
73
|
duration: entry.duration || 0,
|
|
71
74
|
author: entry.channel || entry.uploader,
|
|
75
|
+
authorUrl,
|
|
72
76
|
thumbnail: entry.thumbnails?.[0]?.url,
|
|
73
77
|
uri: `https://www.youtube.com/watch?v=${entry.id}`,
|
|
74
78
|
streamUrl: `/youtube/stream/${entry.id}`,
|
|
@@ -126,11 +130,15 @@ async function getInfo(videoId, config) {
|
|
|
126
130
|
?.filter(f => f.vcodec === 'none' && f.acodec !== 'none')
|
|
127
131
|
.sort((a, b) => (b.abr || 0) - (a.abr || 0))[0];
|
|
128
132
|
|
|
133
|
+
const authorUrl = data.channel_url || data.uploader_url ||
|
|
134
|
+
(data.channel_id ? `https://www.youtube.com/channel/${data.channel_id}` : null);
|
|
135
|
+
|
|
129
136
|
resolve({
|
|
130
137
|
id: data.id,
|
|
131
138
|
title: data.title,
|
|
132
139
|
duration: data.duration || 0,
|
|
133
140
|
author: data.channel || data.uploader,
|
|
141
|
+
authorUrl,
|
|
134
142
|
thumbnail: data.thumbnail,
|
|
135
143
|
uri: data.webpage_url,
|
|
136
144
|
streamUrl: `/youtube/stream/${data.id}`,
|
|
@@ -279,17 +287,22 @@ async function getPlaylist(playlistId, config) {
|
|
|
279
287
|
}
|
|
280
288
|
try {
|
|
281
289
|
const data = JSON.parse(stdout);
|
|
282
|
-
const tracks = (data.entries || []).map(entry =>
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
const tracks = (data.entries || []).map(entry => {
|
|
291
|
+
const authorUrl = entry.channel_url || entry.uploader_url ||
|
|
292
|
+
(entry.channel_id ? `https://www.youtube.com/channel/${entry.channel_id}` : null);
|
|
293
|
+
return {
|
|
294
|
+
id: entry.id,
|
|
295
|
+
title: entry.title,
|
|
296
|
+
duration: entry.duration || 0,
|
|
297
|
+
author: entry.channel || entry.uploader,
|
|
298
|
+
authorUrl,
|
|
299
|
+
thumbnail: entry.thumbnails?.[0]?.url,
|
|
300
|
+
uri: `https://www.youtube.com/watch?v=${entry.id}`,
|
|
301
|
+
streamUrl: `/youtube/stream/${entry.id}`,
|
|
302
|
+
source: 'youtube',
|
|
303
|
+
isLive: entry.live_status === 'is_live' || entry.is_live === true || !entry.duration
|
|
304
|
+
};
|
|
305
|
+
});
|
|
293
306
|
log.info('YOUTUBE', `Playlist loaded: ${data.title || playlistId} (${tracks.length} tracks)`);
|
|
294
307
|
resolve({
|
|
295
308
|
id: playlistId,
|
|
@@ -342,17 +355,22 @@ async function getRelated(videoId, limit, config) {
|
|
|
342
355
|
const tracks = entries
|
|
343
356
|
.filter(entry => entry.id !== videoId)
|
|
344
357
|
.slice(0, limit)
|
|
345
|
-
.map(entry =>
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
358
|
+
.map(entry => {
|
|
359
|
+
const authorUrl = entry.channel_url || entry.uploader_url ||
|
|
360
|
+
(entry.channel_id ? `https://www.youtube.com/channel/${entry.channel_id}` : null);
|
|
361
|
+
return {
|
|
362
|
+
id: entry.id,
|
|
363
|
+
title: entry.title,
|
|
364
|
+
duration: entry.duration,
|
|
365
|
+
author: entry.channel || entry.uploader,
|
|
366
|
+
authorUrl,
|
|
367
|
+
thumbnail: entry.thumbnails?.[0]?.url,
|
|
368
|
+
uri: `https://www.youtube.com/watch?v=${entry.id}`,
|
|
369
|
+
streamUrl: `/youtube/stream/${entry.id}`,
|
|
370
|
+
source: 'youtube',
|
|
371
|
+
isAutoplay: true
|
|
372
|
+
};
|
|
373
|
+
});
|
|
356
374
|
log.info('YOUTUBE', `Found ${tracks.length} related tracks`);
|
|
357
375
|
resolve({ tracks, source: 'youtube' });
|
|
358
376
|
} catch (e) {
|