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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "streamify-audio",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
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/config.js
CHANGED
|
@@ -26,8 +26,8 @@ const defaults = {
|
|
|
26
26
|
bitrate: '128k',
|
|
27
27
|
format: 'opus',
|
|
28
28
|
vbr: true,
|
|
29
|
-
compressionLevel:
|
|
30
|
-
application: '
|
|
29
|
+
compressionLevel: 5,
|
|
30
|
+
application: 'lowdelay'
|
|
31
31
|
},
|
|
32
32
|
ytdlp: {
|
|
33
33
|
format: 'bestaudio/bestaudio[ext=webm]/bestaudio[ext=mp4]/18/22/best',
|
|
@@ -38,6 +38,13 @@ const defaults = {
|
|
|
38
38
|
searchTTL: 300,
|
|
39
39
|
infoTTL: 3600,
|
|
40
40
|
redis: null
|
|
41
|
+
},
|
|
42
|
+
stream: {
|
|
43
|
+
dataTimeout: 8000,
|
|
44
|
+
liveDataTimeout: 15000,
|
|
45
|
+
urlCacheTTL: 1800,
|
|
46
|
+
bufferSize: '1M',
|
|
47
|
+
maxRetries: 2
|
|
41
48
|
}
|
|
42
49
|
};
|
|
43
50
|
|
|
@@ -107,6 +114,11 @@ function load(options = {}) {
|
|
|
107
114
|
...defaults.cache,
|
|
108
115
|
...fileConfig.cache,
|
|
109
116
|
...options.cache
|
|
117
|
+
},
|
|
118
|
+
stream: {
|
|
119
|
+
...defaults.stream,
|
|
120
|
+
...fileConfig.stream,
|
|
121
|
+
...options.stream
|
|
110
122
|
}
|
|
111
123
|
};
|
|
112
124
|
|
package/src/discord/Player.js
CHANGED
|
@@ -48,6 +48,7 @@ class Player extends EventEmitter {
|
|
|
48
48
|
this._prefetching = false;
|
|
49
49
|
this._changingStream = false;
|
|
50
50
|
this._manualSkip = false;
|
|
51
|
+
this._pauseStreamTimeout = null;
|
|
51
52
|
|
|
52
53
|
this.autoLeave = {
|
|
53
54
|
enabled: options.autoLeave?.enabled ?? true,
|
|
@@ -316,22 +317,23 @@ class Player extends EventEmitter {
|
|
|
316
317
|
this.emit('trackStart', track);
|
|
317
318
|
|
|
318
319
|
let newStream = null;
|
|
320
|
+
let resource = null;
|
|
321
|
+
|
|
319
322
|
try {
|
|
320
323
|
const filtersWithVolume = { ...this._filters, volume: this._volume };
|
|
321
324
|
|
|
322
|
-
if (startPosition === 0 && this._prefetchedTrack?.id === track.id && this._prefetchedStream) {
|
|
323
|
-
log.
|
|
325
|
+
if (startPosition === 0 && this._prefetchedTrack?.id === track.id && this._prefetchedStream?.resource) {
|
|
326
|
+
log.info('PLAYER', `Using prefetched stream for ${track.id}`);
|
|
324
327
|
newStream = this._prefetchedStream;
|
|
328
|
+
resource = newStream.resource;
|
|
325
329
|
this._prefetchedStream = null;
|
|
326
330
|
this._prefetchedTrack = null;
|
|
327
331
|
} else {
|
|
328
|
-
|
|
332
|
+
this._clearPrefetch();
|
|
329
333
|
newStream = createStream(track, filtersWithVolume, this.config);
|
|
334
|
+
resource = await newStream.start(startPosition);
|
|
330
335
|
}
|
|
331
336
|
|
|
332
|
-
const resource = await newStream.create(startPosition);
|
|
333
|
-
|
|
334
|
-
// SEAMLESS SWAP: Only destroy old stream AFTER the new one is ready
|
|
335
337
|
const oldStream = this.stream;
|
|
336
338
|
this.stream = newStream;
|
|
337
339
|
this.audioPlayer.play(resource);
|
|
@@ -361,7 +363,7 @@ class Player extends EventEmitter {
|
|
|
361
363
|
|
|
362
364
|
log.error('PLAYER', `Failed to play track: ${error.message}`);
|
|
363
365
|
this.emit('trackError', track, error);
|
|
364
|
-
|
|
366
|
+
|
|
365
367
|
const next = this.queue.shift();
|
|
366
368
|
if (next) {
|
|
367
369
|
setImmediate(() => this._playTrack(next));
|
|
@@ -383,19 +385,35 @@ class Player extends EventEmitter {
|
|
|
383
385
|
if (this._prefetching || this.queue.tracks.length === 0) return;
|
|
384
386
|
|
|
385
387
|
const nextTrack = this.queue.tracks[0];
|
|
386
|
-
if (!nextTrack
|
|
388
|
+
if (!nextTrack) return;
|
|
389
|
+
|
|
390
|
+
if (this._prefetchedTrack?.id === nextTrack.id && this._prefetchedStream) return;
|
|
387
391
|
|
|
392
|
+
this._clearPrefetch();
|
|
388
393
|
this._prefetching = true;
|
|
389
|
-
log.debug('PLAYER', `Prefetching
|
|
394
|
+
log.debug('PLAYER', `Prefetching stream: ${nextTrack.title}`);
|
|
390
395
|
|
|
391
396
|
try {
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
397
|
+
const filtersWithVolume = { ...this._filters, volume: this._volume };
|
|
398
|
+
const stream = createStream(nextTrack, filtersWithVolume, this.config);
|
|
399
|
+
|
|
400
|
+
await stream.prepare();
|
|
401
|
+
|
|
402
|
+
if (this._destroyed || this.queue.tracks[0]?.id !== nextTrack.id) {
|
|
403
|
+
stream.destroy();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await stream.start(0);
|
|
408
|
+
|
|
409
|
+
if (this._destroyed || this.queue.tracks[0]?.id !== nextTrack.id) {
|
|
410
|
+
stream.destroy();
|
|
411
|
+
return;
|
|
398
412
|
}
|
|
413
|
+
|
|
414
|
+
this._prefetchedStream = stream;
|
|
415
|
+
this._prefetchedTrack = nextTrack;
|
|
416
|
+
log.info('PLAYER', `Prefetch ready: ${nextTrack.title} (${stream.metrics.total}ms)`);
|
|
399
417
|
} catch (error) {
|
|
400
418
|
log.debug('PLAYER', `Prefetch failed: ${error.message}`);
|
|
401
419
|
} finally {
|
|
@@ -492,7 +510,7 @@ class Player extends EventEmitter {
|
|
|
492
510
|
}
|
|
493
511
|
}
|
|
494
512
|
|
|
495
|
-
pause(destroyStream =
|
|
513
|
+
pause(destroyStream = false) {
|
|
496
514
|
if (!this._playing || this._paused) return false;
|
|
497
515
|
|
|
498
516
|
this._positionMs = this.position;
|
|
@@ -502,10 +520,23 @@ class Player extends EventEmitter {
|
|
|
502
520
|
this._clearPrefetch();
|
|
503
521
|
this.stream.destroy();
|
|
504
522
|
this.stream = null;
|
|
523
|
+
this.audioPlayer.stop(true);
|
|
524
|
+
} else {
|
|
525
|
+
this.audioPlayer.pause();
|
|
505
526
|
}
|
|
506
527
|
|
|
507
|
-
this.
|
|
508
|
-
|
|
528
|
+
if (!destroyStream && this.stream) {
|
|
529
|
+
this._pauseStreamTimeout = setTimeout(() => {
|
|
530
|
+
if (this._paused && this.stream) {
|
|
531
|
+
log.info('PLAYER', 'Destroying paused stream after 5m idle');
|
|
532
|
+
this._clearPrefetch();
|
|
533
|
+
this.stream.destroy();
|
|
534
|
+
this.stream = null;
|
|
535
|
+
}
|
|
536
|
+
}, 300000);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
log.info('PLAYER', `Paused playback at ${Math.floor(this._positionMs / 1000)}s (stream kept: ${!destroyStream})`);
|
|
509
540
|
this._clearVoiceChannelStatus();
|
|
510
541
|
|
|
511
542
|
return true;
|
|
@@ -514,8 +545,13 @@ class Player extends EventEmitter {
|
|
|
514
545
|
async resume() {
|
|
515
546
|
if (!this._playing || !this._paused) return false;
|
|
516
547
|
|
|
548
|
+
if (this._pauseStreamTimeout) {
|
|
549
|
+
clearTimeout(this._pauseStreamTimeout);
|
|
550
|
+
this._pauseStreamTimeout = null;
|
|
551
|
+
}
|
|
552
|
+
|
|
517
553
|
if (!this.stream && this.queue.current) {
|
|
518
|
-
log.info('PLAYER', `Resuming playback from ${Math.floor(this._positionMs / 1000)}s`);
|
|
554
|
+
log.info('PLAYER', `Resuming playback from ${Math.floor(this._positionMs / 1000)}s (recreating stream)`);
|
|
519
555
|
|
|
520
556
|
this._changingStream = true;
|
|
521
557
|
const track = this.queue.current;
|
|
@@ -547,6 +583,7 @@ class Player extends EventEmitter {
|
|
|
547
583
|
this.audioPlayer.unpause();
|
|
548
584
|
this._paused = false;
|
|
549
585
|
this._positionTimestamp = Date.now();
|
|
586
|
+
log.info('PLAYER', `Resumed playback instantly at ${Math.floor(this._positionMs / 1000)}s`);
|
|
550
587
|
this._updateVoiceChannelStatus(this.queue.current, true);
|
|
551
588
|
}
|
|
552
589
|
|
|
@@ -630,10 +667,21 @@ class Player extends EventEmitter {
|
|
|
630
667
|
return true;
|
|
631
668
|
}
|
|
632
669
|
|
|
633
|
-
setVolume(volume) {
|
|
670
|
+
async setVolume(volume) {
|
|
671
|
+
this._clearPrefetch();
|
|
634
672
|
const oldVolume = this._volume;
|
|
635
673
|
this._volume = Math.max(0, Math.min(200, volume));
|
|
636
674
|
log.info('PLAYER', `Volume adjusted: ${oldVolume}% -> ${this._volume}%`);
|
|
675
|
+
|
|
676
|
+
if (this._playing && this.queue.current && !this._paused) {
|
|
677
|
+
this._changingStream = true;
|
|
678
|
+
try {
|
|
679
|
+
await this._playTrack(this.queue.current, this.position);
|
|
680
|
+
} finally {
|
|
681
|
+
this._changingStream = false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
637
685
|
return this._volume;
|
|
638
686
|
}
|
|
639
687
|
|
|
@@ -659,6 +707,7 @@ class Player extends EventEmitter {
|
|
|
659
707
|
}
|
|
660
708
|
|
|
661
709
|
async setFilter(name, value) {
|
|
710
|
+
this._clearPrefetch();
|
|
662
711
|
this._filters[name] = value;
|
|
663
712
|
|
|
664
713
|
if (this._playing && this.queue.current) {
|
|
@@ -757,6 +806,7 @@ class Player extends EventEmitter {
|
|
|
757
806
|
}
|
|
758
807
|
}
|
|
759
808
|
|
|
809
|
+
this._clearPrefetch();
|
|
760
810
|
log.info('PLAYER', `Active effects: ${this._effectPresets.map(p => p.name).join(' + ') || 'NONE'}`);
|
|
761
811
|
|
|
762
812
|
if (this._playing && this.queue.current) {
|
|
@@ -801,6 +851,10 @@ class Player extends EventEmitter {
|
|
|
801
851
|
clearTimeout(this._inactivityTimeout);
|
|
802
852
|
this._inactivityTimeout = null;
|
|
803
853
|
}
|
|
854
|
+
if (this._pauseStreamTimeout) {
|
|
855
|
+
clearTimeout(this._pauseStreamTimeout);
|
|
856
|
+
this._pauseStreamTimeout = null;
|
|
857
|
+
}
|
|
804
858
|
|
|
805
859
|
this._clearPrefetch();
|
|
806
860
|
|