streamify-audio 2.2.6 → 2.2.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamify-audio",
3
- "version": "2.2.6",
3
+ "version": "2.2.7",
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",
@@ -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: false
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 checkBuffer = () => {
274
+ const onReadable = () => {
265
275
  if (resolved) return;
266
-
267
- if (!firstByteRecorded && ffmpeg.stdout && ffmpeg.stdout.readableLength > 0) {
268
- firstByteRecorded = true;
269
- this.metrics.firstByte = Date.now() - (this.startTime + this.metrics.metadata + this.metrics.spawn);
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', checkBuffer);
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', checkBuffer);
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', checkBuffer);
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'));
@@ -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', '4096',
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 = ['aresample=async=1:first_pts=0'];
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 || 'opus';
250
+ const format = config.audio?.format || 'raw';
255
251
 
256
- if (format === 'opus') {
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('-acodec', 'libopus', '-b:a', bitrate, '-f', 'ogg');
272
+ args.push('-ar', '48000', '-ac', '2', '-f', 's16le');
271
273
  }
272
274
 
273
275
  args.push('-');
@@ -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
- id: entry.id,
284
- title: entry.title,
285
- duration: entry.duration || 0,
286
- author: entry.channel || entry.uploader,
287
- thumbnail: entry.thumbnails?.[0]?.url,
288
- uri: `https://www.youtube.com/watch?v=${entry.id}`,
289
- streamUrl: `/youtube/stream/${entry.id}`,
290
- source: 'youtube',
291
- isLive: entry.live_status === 'is_live' || entry.is_live === true || !entry.duration
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
- id: entry.id,
347
- title: entry.title,
348
- duration: entry.duration,
349
- author: entry.channel || entry.uploader,
350
- thumbnail: entry.thumbnails?.[0]?.url,
351
- uri: `https://www.youtube.com/watch?v=${entry.id}`,
352
- streamUrl: `/youtube/stream/${entry.id}`,
353
- source: 'youtube',
354
- isAutoplay: true
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) {