tiny-essentials 1.28.6 → 1.28.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/changelog/1/28/6.md +3 -0
- package/changelog/1/28/7.md +7 -0
- package/changelog/1/28/8.md +17 -0
- package/dist/v1/TinyBasicsEs.min.js +1 -1
- package/dist/v1/TinyDebugger.min.js +1 -0
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinyMediaPlayer.min.js +1 -1
- package/dist/v1/TinyRadioFm.min.js +1 -1
- package/dist/v1/TinyUploadClicker.min.js +1 -1
- package/dist/v1/basics/browserDetector.cjs +228 -0
- package/dist/v1/basics/browserDetector.d.mts +66 -0
- package/dist/v1/basics/browserDetector.mjs +207 -0
- package/dist/v1/basics/index.cjs +6 -0
- package/dist/v1/basics/index.d.mts +2 -1
- package/dist/v1/basics/index.mjs +2 -1
- package/dist/v1/build/TinyDebugger.cjs +7 -0
- package/dist/v1/build/TinyDebugger.d.mts +3 -0
- package/dist/v1/build/TinyDebugger.mjs +2 -0
- package/dist/v1/index.cjs +8 -0
- package/dist/v1/index.d.mts +3 -1
- package/dist/v1/index.mjs +4 -2
- package/dist/v1/libs/TinyDebugger.cjs +380 -0
- package/dist/v1/libs/TinyDebugger.d.mts +132 -0
- package/dist/v1/libs/TinyDebugger.mjs +351 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.cjs +66 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.d.mts +25 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.mjs +59 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.cjs +66 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.d.mts +25 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.mjs +59 -3
- package/dist/v1/libs/TinyMediaPlayer/Mock.cjs +333 -0
- package/dist/v1/libs/TinyMediaPlayer/Mock.d.mts +154 -0
- package/dist/v1/libs/TinyMediaPlayer/Mock.mjs +296 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.cjs +85 -1
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.d.mts +25 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.mjs +75 -1
- package/dist/v1/libs/TinyMediaPlayer/Youtube.cjs +66 -3
- package/dist/v1/libs/TinyMediaPlayer/Youtube.d.mts +25 -0
- package/dist/v1/libs/TinyMediaPlayer/Youtube.mjs +56 -3
- package/dist/v1/libs/TinyMediaPlayer/index.cjs +45 -0
- package/dist/v1/libs/TinyMediaPlayer/index.d.mts +25 -0
- package/dist/v1/libs/TinyMediaPlayer/index.mjs +40 -0
- package/dist/v1/libs/TinyMediaPlayer.cjs +172 -10
- package/dist/v1/libs/TinyMediaPlayer.d.mts +47 -2
- package/dist/v1/libs/TinyMediaPlayer.mjs +160 -10
- package/dist/v1/libs/TinyPromiseQueue.cjs +6 -4
- package/dist/v1/libs/TinyPromiseQueue.d.mts +8 -6
- package/dist/v1/libs/TinyPromiseQueue.mjs +6 -4
- package/dist/v1/libs/TinyRadioFm.cjs +271 -138
- package/dist/v1/libs/TinyRadioFm.d.mts +67 -3
- package/dist/v1/libs/TinyRadioFm.mjs +239 -123
- package/docs/v1/libs/TinyMediaPlayer.md +52 -54
- package/docs/v1/libs/TinyRadioFm.md +25 -3
- package/package.json +13 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import TinyDebugger from './TinyDebugger.mjs';
|
|
2
2
|
import { getMediaContentBase, getMediaContentMetadata, parseMediaMetadata, valMediaContentMetadata, } from '../basics/mediaContent.mjs';
|
|
3
3
|
import { createCheckDestroyed } from './utils.mjs';
|
|
4
4
|
import { BaseMediaAdapter } from './TinyMediaPlayer/index.mjs';
|
|
@@ -28,6 +28,9 @@ const checkDestroy = createCheckDestroyed('TinyMediaPlayer');
|
|
|
28
28
|
* @property {boolean} [repeatCurrentOnPrev=false] - If true, clicking 'previous' repeats the current track on the first click.
|
|
29
29
|
* @property {boolean} [smoothPlayPauseVolume=false] - If true, volume fades smoothly during play/pause transitions.
|
|
30
30
|
* @property {boolean} [smoothStopVolume=false] - If true, volume fades smoothly to zero when stopping.
|
|
31
|
+
* @property {boolean} [debugMode=false] - Whether to enable internal debug logging.
|
|
32
|
+
* @property {boolean} [useLogColors=false] - Whether to enable log color support.
|
|
33
|
+
* @property {Console} [logger=console] - A custom logger object (must implement console methods).
|
|
31
34
|
*/
|
|
32
35
|
/**
|
|
33
36
|
* Represents a search match containing the media object and its exact playlist index.
|
|
@@ -38,7 +41,7 @@ const checkDestroy = createCheckDestroyed('TinyMediaPlayer');
|
|
|
38
41
|
/**
|
|
39
42
|
* A universal media player manager capable of orchestrating multiple API adapters.
|
|
40
43
|
*/
|
|
41
|
-
class TinyMediaPlayer extends
|
|
44
|
+
class TinyMediaPlayer extends TinyDebugger {
|
|
42
45
|
static BaseMediaAdapter = BaseMediaAdapter;
|
|
43
46
|
/**
|
|
44
47
|
* @type {UnknownArtistGetter}
|
|
@@ -98,6 +101,8 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
98
101
|
#isRandom = false;
|
|
99
102
|
/** @type {boolean} */
|
|
100
103
|
#isPlaying = false;
|
|
104
|
+
/** @type {boolean} */
|
|
105
|
+
#isMuted = false;
|
|
101
106
|
/** @type {number} */
|
|
102
107
|
#volume = 1.0;
|
|
103
108
|
/** @type {boolean} */
|
|
@@ -216,10 +221,16 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
216
221
|
* @throws {RangeError} If numeric options are out of valid ranges.
|
|
217
222
|
*/
|
|
218
223
|
constructor(options = {}) {
|
|
219
|
-
super();
|
|
220
224
|
if (!isValidObj(options)) {
|
|
221
225
|
throw new TypeError('Options must be a non-null object.');
|
|
222
226
|
}
|
|
227
|
+
// Debug Configuration
|
|
228
|
+
super({
|
|
229
|
+
id: '[_blue_TinyMediaPlayer_reset_] :debug:',
|
|
230
|
+
logger: options.logger ?? console,
|
|
231
|
+
debugMode: options.debugMode ?? false,
|
|
232
|
+
useLogColors: options.useLogColors ?? false,
|
|
233
|
+
});
|
|
223
234
|
// Volume configuration
|
|
224
235
|
if (options.persistVolume !== undefined) {
|
|
225
236
|
if (typeof options.persistVolume !== 'boolean') {
|
|
@@ -268,6 +279,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
268
279
|
}
|
|
269
280
|
if (this.#persistVolume)
|
|
270
281
|
this.#loadVolumeFromStorage();
|
|
282
|
+
this.log('info', 'Instance initialized.', { options });
|
|
271
283
|
}
|
|
272
284
|
// ==========================================
|
|
273
285
|
// STORAGE HELPERS
|
|
@@ -283,6 +295,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
283
295
|
const parsedVolume = parseFloat(storedValue);
|
|
284
296
|
if (!isNaN(parsedVolume) && parsedVolume >= 0 && parsedVolume <= 1) {
|
|
285
297
|
this.#volume = parsedVolume;
|
|
298
|
+
this.log('info', `Volume loaded from storage: ${this.#volume}`);
|
|
286
299
|
}
|
|
287
300
|
}
|
|
288
301
|
}
|
|
@@ -298,6 +311,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
298
311
|
try {
|
|
299
312
|
if (this.#persistVolume && typeof window !== 'undefined' && window.localStorage) {
|
|
300
313
|
window.localStorage.setItem(this.#volumeStorageKey, this.#volume.toString());
|
|
314
|
+
this.log('info', `Volume saved to storage: ${this.#volume}`);
|
|
301
315
|
}
|
|
302
316
|
}
|
|
303
317
|
catch (error) {
|
|
@@ -360,11 +374,14 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
360
374
|
const startVolume = adapter.getVolume();
|
|
361
375
|
const startTime = performance.now();
|
|
362
376
|
const duration = this.#fadeVolumeSpeed;
|
|
377
|
+
this.log('info', `Starting volume fade: ${startVolume} -> ${targetVolume}`);
|
|
363
378
|
return new Promise((resolve) => {
|
|
364
379
|
/** @param {number} currentTime */
|
|
365
380
|
const step = (currentTime) => {
|
|
366
|
-
if (signal.aborted)
|
|
381
|
+
if (signal.aborted) {
|
|
382
|
+
this.log('info', 'Volume fade aborted.');
|
|
367
383
|
return resolve();
|
|
384
|
+
}
|
|
368
385
|
const elapsed = currentTime - startTime;
|
|
369
386
|
const progress = Math.min(elapsed / duration, 1);
|
|
370
387
|
const calculatedVolume = startVolume + (targetVolume - startVolume) * progress;
|
|
@@ -375,6 +392,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
375
392
|
}
|
|
376
393
|
else {
|
|
377
394
|
adapter.setVolume(targetVolume);
|
|
395
|
+
this.log('info', `Volume fade completed: ${targetVolume}`);
|
|
378
396
|
resolve();
|
|
379
397
|
}
|
|
380
398
|
};
|
|
@@ -411,6 +429,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
411
429
|
}
|
|
412
430
|
}
|
|
413
431
|
this.#playlist = value;
|
|
432
|
+
this.log('info', `Playlist updated. New size: ${this.#playlist.length}`);
|
|
414
433
|
this.emit('playlistUpdate', this.playlist);
|
|
415
434
|
// Validation to correct currentIndex ensuring it aligns with the new playlist limits
|
|
416
435
|
if (this.#playlist.length === 0) {
|
|
@@ -433,6 +452,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
433
452
|
await this.play();
|
|
434
453
|
}
|
|
435
454
|
catch (error) {
|
|
455
|
+
this.log('error', 'Background transition error on playlist update:', error);
|
|
436
456
|
console.warn('[TinyMediaPlayer] Background transition error on playlist update:', error);
|
|
437
457
|
}
|
|
438
458
|
})();
|
|
@@ -458,6 +478,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
458
478
|
}
|
|
459
479
|
if (this.#currentIndex === value)
|
|
460
480
|
return; // Optimization: do nothing if index is the same
|
|
481
|
+
this.log('info', `Index change requested: ${this.#currentIndex} -> ${value}`);
|
|
461
482
|
const wasPlaying = this.#isPlaying;
|
|
462
483
|
let oldAdapter = null;
|
|
463
484
|
if (this.#currentIndex !== -1 && this.#playlist.length > 0) {
|
|
@@ -480,6 +501,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
480
501
|
await this.play();
|
|
481
502
|
}
|
|
482
503
|
catch (error) {
|
|
504
|
+
this.log('error', 'Background transition error on index update:', error);
|
|
483
505
|
console.warn('[TinyMediaPlayer] Background transition error on index update:', error);
|
|
484
506
|
}
|
|
485
507
|
})();
|
|
@@ -501,6 +523,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
501
523
|
throw new TypeError(`Loop mode must be one of: ${validModes.join(', ')}.`);
|
|
502
524
|
}
|
|
503
525
|
this.#loopMode = value;
|
|
526
|
+
this.log('info', `Loop mode changed to: ${this.#loopMode}`);
|
|
504
527
|
this.emit('loopModeChange', this.#loopMode);
|
|
505
528
|
}
|
|
506
529
|
/** @returns {boolean} The current random mode status. */
|
|
@@ -518,6 +541,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
518
541
|
throw new TypeError('Random mode state must be a boolean.');
|
|
519
542
|
}
|
|
520
543
|
this.#isRandom = value;
|
|
544
|
+
this.log('info', `Random mode: ${this.#isRandom}`);
|
|
521
545
|
this.emit('randomModeChange', this.#isRandom);
|
|
522
546
|
}
|
|
523
547
|
/** @returns {boolean} True if media is actively playing. */
|
|
@@ -544,6 +568,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
544
568
|
// Abort any ongoing fade to ensure manual control is immediate
|
|
545
569
|
if (this.#fadeController)
|
|
546
570
|
this.#fadeController.abort();
|
|
571
|
+
this.log('info', `Volume changed: ${this.#volume} -> ${value}`);
|
|
547
572
|
this.#volume = value;
|
|
548
573
|
this.#saveVolumeToStorage();
|
|
549
574
|
this.emit('volumeChange', this.#volume);
|
|
@@ -574,6 +599,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
574
599
|
throw new TypeError('Persist volume parameter must be a boolean.');
|
|
575
600
|
}
|
|
576
601
|
this.#persistVolume = value;
|
|
602
|
+
this.log('info', `Persist volume: ${this.#persistVolume}`);
|
|
577
603
|
if (value) {
|
|
578
604
|
this.#saveVolumeToStorage();
|
|
579
605
|
}
|
|
@@ -608,6 +634,39 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
608
634
|
console.warn('[TinyMediaPlayer] Failed to migrate volume storage key.', error);
|
|
609
635
|
}
|
|
610
636
|
}
|
|
637
|
+
/** @returns {boolean} True if the player is currently muted. */
|
|
638
|
+
get isMuted() {
|
|
639
|
+
checkDestroy(this.#destroyed);
|
|
640
|
+
return this.#isMuted;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Mutes the current playback.
|
|
644
|
+
* @returns {Promise<void>}
|
|
645
|
+
*/
|
|
646
|
+
async mute() {
|
|
647
|
+
checkDestroy(this.#destroyed);
|
|
648
|
+
const adapter = this.#getActiveAdapter();
|
|
649
|
+
if (!adapter)
|
|
650
|
+
return;
|
|
651
|
+
await adapter.mute();
|
|
652
|
+
this.#isMuted = true;
|
|
653
|
+
this.emit('muteChange', this.#isMuted);
|
|
654
|
+
this.log('info', 'Playback muted successfully.');
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Unmutes the current playback.
|
|
658
|
+
* @returns {Promise<void>}
|
|
659
|
+
*/
|
|
660
|
+
async unMute() {
|
|
661
|
+
checkDestroy(this.#destroyed);
|
|
662
|
+
const adapter = this.#getActiveAdapter();
|
|
663
|
+
if (!adapter)
|
|
664
|
+
return;
|
|
665
|
+
await adapter.unMute();
|
|
666
|
+
this.#isMuted = false;
|
|
667
|
+
this.emit('muteChange', this.#isMuted);
|
|
668
|
+
this.log('info', 'Playback unmuted successfully.');
|
|
669
|
+
}
|
|
611
670
|
// ==========================================
|
|
612
671
|
// ADAPTER MANAGEMENT
|
|
613
672
|
// ==========================================
|
|
@@ -655,6 +714,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
655
714
|
if (!(adapter instanceof BaseMediaAdapter)) {
|
|
656
715
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
657
716
|
}
|
|
717
|
+
this.log('info', 'Registering new adapter:', adapter);
|
|
658
718
|
const events = new Map();
|
|
659
719
|
this.#adapterHandlers.set(adapter, events);
|
|
660
720
|
// Use the controlled helper to ensure each event is registered
|
|
@@ -674,6 +734,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
674
734
|
if (!(adapter instanceof BaseMediaAdapter)) {
|
|
675
735
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
676
736
|
}
|
|
737
|
+
this.log('info', 'Removing adapter:', adapter);
|
|
677
738
|
this.#removeHandler(adapter);
|
|
678
739
|
return this.#adapters.delete(adapter);
|
|
679
740
|
}
|
|
@@ -687,6 +748,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
687
748
|
if (!(adapter instanceof BaseMediaAdapter)) {
|
|
688
749
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
689
750
|
}
|
|
751
|
+
this.log('info', 'Destroying adapter:', adapter);
|
|
690
752
|
this.#removeHandler(adapter);
|
|
691
753
|
const result = this.#adapters.delete(adapter);
|
|
692
754
|
if (result)
|
|
@@ -710,6 +772,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
710
772
|
*/
|
|
711
773
|
destroyAllAdapters() {
|
|
712
774
|
checkDestroy(this.#destroyed);
|
|
775
|
+
this.log('info', 'Destroying all adapters.');
|
|
713
776
|
this.#adapters.forEach((adapter) => {
|
|
714
777
|
this.#removeHandler(adapter);
|
|
715
778
|
adapter.destroy();
|
|
@@ -721,6 +784,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
721
784
|
*/
|
|
722
785
|
clearAdapters() {
|
|
723
786
|
checkDestroy(this.#destroyed);
|
|
787
|
+
this.log('info', 'Clearing all adapters.');
|
|
724
788
|
this.#adapters.forEach((adapter) => this.#removeHandler(adapter));
|
|
725
789
|
this.#adapters.clear();
|
|
726
790
|
}
|
|
@@ -753,7 +817,9 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
753
817
|
const adapter = this.getMediaAdapter(currentContent);
|
|
754
818
|
if (adapter)
|
|
755
819
|
return adapter;
|
|
756
|
-
|
|
820
|
+
const errorMsg = `No compatible adapter found for content ID: ${currentContent.id}.`;
|
|
821
|
+
this.log('error', errorMsg);
|
|
822
|
+
throw new Error(errorMsg);
|
|
757
823
|
}
|
|
758
824
|
// ==========================================
|
|
759
825
|
// EVENT MANAGEMENT (CONTROLLED)
|
|
@@ -771,6 +837,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
771
837
|
// If the event is already in the list, do nothing to avoid duplicates
|
|
772
838
|
if (this.#adapterEventNames.has(eventName))
|
|
773
839
|
return;
|
|
840
|
+
this.log('info', `Adding adapter event listener: ${eventName}`);
|
|
774
841
|
this.#adapterEventNames.add(eventName);
|
|
775
842
|
// Synchronize: Add the listener to all already registered adapters
|
|
776
843
|
this.#adapters.forEach((adapter) => {
|
|
@@ -789,6 +856,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
789
856
|
}
|
|
790
857
|
if (!this.#adapterEventNames.has(eventName))
|
|
791
858
|
return;
|
|
859
|
+
this.log('info', `Removing adapter event listener: ${eventName}`);
|
|
792
860
|
this.#adapterEventNames.delete(eventName);
|
|
793
861
|
// Synchronize: Remove the listener from all registered adapters
|
|
794
862
|
this.#adapters.forEach((adapter) => {
|
|
@@ -830,6 +898,8 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
830
898
|
/** @type {(...args: any[]) => any} */
|
|
831
899
|
const handler = (...args) => {
|
|
832
900
|
if (!this.#destroyed) {
|
|
901
|
+
if (eventName !== 'timeupdate')
|
|
902
|
+
this.log('debug', `Adapter event triggered: ${eventName}`, ...args);
|
|
833
903
|
this.emit(eventName, ...args);
|
|
834
904
|
}
|
|
835
905
|
};
|
|
@@ -869,6 +939,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
869
939
|
const newContent = { ...getMediaContentBase(), ...getMediaContentMetadata(), ...content };
|
|
870
940
|
valMediaContentMetadata(newContent);
|
|
871
941
|
const newLength = this.#playlist.push(newContent);
|
|
942
|
+
this.log('info', `Track added. New length: ${newLength}`, newContent);
|
|
872
943
|
if (this.#currentIndex === -1) {
|
|
873
944
|
this.#currentIndex = 0;
|
|
874
945
|
this.emit('trackChange', this.#currentIndex);
|
|
@@ -921,6 +992,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
921
992
|
if (!this.existsTrack(index)) {
|
|
922
993
|
throw new RangeError(`Index ${index} is out of bounds for the current playlist.`);
|
|
923
994
|
}
|
|
995
|
+
this.log('info', `Removing track at index: ${index}`);
|
|
924
996
|
if (index === this.#currentIndex) {
|
|
925
997
|
// Stop the current track if it is the one being removed
|
|
926
998
|
await this.stop();
|
|
@@ -956,6 +1028,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
956
1028
|
if (typeof query !== 'string' && typeof query !== 'function') {
|
|
957
1029
|
throw new TypeError('Search query must be a string or a boolean evaluation function.');
|
|
958
1030
|
}
|
|
1031
|
+
this.log('info', 'Searching playlist...', { query });
|
|
959
1032
|
const results = [];
|
|
960
1033
|
for (let i = 0; i < this.#playlist.length; i++) {
|
|
961
1034
|
const track = this.#playlist[i];
|
|
@@ -974,6 +1047,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
974
1047
|
results.push({ track, index: i });
|
|
975
1048
|
}
|
|
976
1049
|
}
|
|
1050
|
+
this.log('info', `Search completed. Found ${results.length} results.`);
|
|
977
1051
|
return results;
|
|
978
1052
|
}
|
|
979
1053
|
/**
|
|
@@ -981,6 +1055,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
981
1055
|
*/
|
|
982
1056
|
async clearPlaylist() {
|
|
983
1057
|
checkDestroy(this.#destroyed);
|
|
1058
|
+
this.log('info', 'Clearing entire playlist.');
|
|
984
1059
|
await this.stop();
|
|
985
1060
|
this.#playlist = [];
|
|
986
1061
|
this.#currentIndex = -1;
|
|
@@ -996,6 +1071,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
996
1071
|
*/
|
|
997
1072
|
async play() {
|
|
998
1073
|
checkDestroy(this.#destroyed);
|
|
1074
|
+
this.log('info', `Attempting to play track at index: ${this.#currentIndex}`);
|
|
999
1075
|
const adapter = this.#getActiveAdapter();
|
|
1000
1076
|
if (!adapter)
|
|
1001
1077
|
return;
|
|
@@ -1006,9 +1082,18 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1006
1082
|
else {
|
|
1007
1083
|
adapter.setVolume(this.#volume);
|
|
1008
1084
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1085
|
+
try {
|
|
1086
|
+
await adapter.play(this.#playlist[this.#currentIndex]);
|
|
1087
|
+
if (this.#isMuted)
|
|
1088
|
+
await adapter.mute();
|
|
1089
|
+
this.#isPlaying = true;
|
|
1090
|
+
this.log('info', 'Playback started successfully.');
|
|
1091
|
+
this.emit('play', this.#currentIndex);
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
this.log('error', 'Failed to play track:', error);
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1012
1097
|
}
|
|
1013
1098
|
/**
|
|
1014
1099
|
* Pauses the current track.
|
|
@@ -1016,6 +1101,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1016
1101
|
*/
|
|
1017
1102
|
async pause() {
|
|
1018
1103
|
checkDestroy(this.#destroyed);
|
|
1104
|
+
this.log('info', 'Attempting to pause playback.');
|
|
1019
1105
|
const adapter = this.#getActiveAdapter();
|
|
1020
1106
|
if (!adapter)
|
|
1021
1107
|
return;
|
|
@@ -1024,6 +1110,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1024
1110
|
}
|
|
1025
1111
|
await adapter.pause();
|
|
1026
1112
|
this.#isPlaying = false;
|
|
1113
|
+
this.log('info', 'Playback paused.');
|
|
1027
1114
|
this.emit('pause', this.#currentIndex);
|
|
1028
1115
|
}
|
|
1029
1116
|
/**
|
|
@@ -1032,6 +1119,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1032
1119
|
*/
|
|
1033
1120
|
async stop() {
|
|
1034
1121
|
checkDestroy(this.#destroyed);
|
|
1122
|
+
this.log('info', 'Attempting to stop playback.');
|
|
1035
1123
|
const adapter = this.#getActiveAdapter();
|
|
1036
1124
|
if (!adapter)
|
|
1037
1125
|
return;
|
|
@@ -1040,6 +1128,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1040
1128
|
}
|
|
1041
1129
|
await adapter.stop();
|
|
1042
1130
|
this.#isPlaying = false;
|
|
1131
|
+
this.log('info', 'Playback stopped.');
|
|
1043
1132
|
this.emit('stop', this.#currentIndex);
|
|
1044
1133
|
}
|
|
1045
1134
|
/**
|
|
@@ -1050,23 +1139,27 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1050
1139
|
checkDestroy(this.#destroyed);
|
|
1051
1140
|
if (this.#playlist.length === 0)
|
|
1052
1141
|
return;
|
|
1142
|
+
this.log('info', 'Next track requested.');
|
|
1053
1143
|
await this.stop();
|
|
1054
1144
|
if (this.#loopMode === 'TRACK') {
|
|
1145
|
+
this.log('info', 'Loop mode: TRACK. Repeating current track.');
|
|
1055
1146
|
// Index remains the same
|
|
1056
1147
|
await this.play();
|
|
1057
1148
|
return;
|
|
1058
1149
|
}
|
|
1059
1150
|
if (this.#isRandom) {
|
|
1060
1151
|
this.#currentIndex = this._getWeightedRandomIndex();
|
|
1152
|
+
this.log('info', `Random mode: Selected index ${this.#currentIndex}`);
|
|
1061
1153
|
}
|
|
1062
1154
|
else {
|
|
1063
1155
|
let nextIndex = this.#currentIndex + 1;
|
|
1064
1156
|
if (nextIndex >= this.#playlist.length) {
|
|
1065
1157
|
if (this.#loopMode === 'PLAYLIST') {
|
|
1066
1158
|
nextIndex = 0;
|
|
1159
|
+
this.log('info', 'Loop mode: PLAYLIST. Returning to start.');
|
|
1067
1160
|
}
|
|
1068
1161
|
else {
|
|
1069
|
-
|
|
1162
|
+
this.log('info', 'End of playlist reached. Stopping.');
|
|
1070
1163
|
return;
|
|
1071
1164
|
}
|
|
1072
1165
|
}
|
|
@@ -1085,6 +1178,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1085
1178
|
return;
|
|
1086
1179
|
// UX: Repeat current track if "repeat on prev" is enabled and it's the first click
|
|
1087
1180
|
if (this.#repeatCurrentOnPrev && !this.#prevClickedToRepeat) {
|
|
1181
|
+
this.log('info', 'UX: Repeat current track on first "prev" click.');
|
|
1088
1182
|
this.#prevClickedToRepeat = true;
|
|
1089
1183
|
// Reset the "repeat" state after 2 seconds of inactivity to prevent glitches
|
|
1090
1184
|
if (this.#prevClickTimer)
|
|
@@ -1103,18 +1197,21 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1103
1197
|
this.#prevClickTimer = null;
|
|
1104
1198
|
}
|
|
1105
1199
|
this.#prevClickedToRepeat = false;
|
|
1200
|
+
this.log('info', 'Previous track requested.');
|
|
1106
1201
|
await this.stop();
|
|
1107
1202
|
if (this.#isRandom) {
|
|
1108
1203
|
this.#currentIndex = this._getWeightedRandomIndex();
|
|
1204
|
+
this.log('info', `Random mode: Selected index ${this.#currentIndex}`);
|
|
1109
1205
|
}
|
|
1110
1206
|
else {
|
|
1111
1207
|
let prevIndex = this.#currentIndex - 1;
|
|
1112
1208
|
if (prevIndex < 0) {
|
|
1113
1209
|
if (this.#loopMode === 'PLAYLIST') {
|
|
1114
1210
|
prevIndex = this.#playlist.length - 1;
|
|
1211
|
+
this.log('info', 'Loop mode: PLAYLIST. Returning to end.');
|
|
1115
1212
|
}
|
|
1116
1213
|
else {
|
|
1117
|
-
|
|
1214
|
+
this.log('info', 'Start of playlist reached. Stopping.');
|
|
1118
1215
|
return;
|
|
1119
1216
|
}
|
|
1120
1217
|
}
|
|
@@ -1123,6 +1220,27 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1123
1220
|
this.emit('trackChange', this.#currentIndex);
|
|
1124
1221
|
await this.play();
|
|
1125
1222
|
}
|
|
1223
|
+
/**
|
|
1224
|
+
* Jumps directly to a specific index in the playlist.
|
|
1225
|
+
* @param {number} index - The target index to skip to.
|
|
1226
|
+
* @returns {Promise<void>}
|
|
1227
|
+
* @throws {TypeError} If the index is not a number.
|
|
1228
|
+
* @throws {RangeError} If the index is out of the playlist bounds.
|
|
1229
|
+
*/
|
|
1230
|
+
async skipTo(index) {
|
|
1231
|
+
checkDestroy(this.#destroyed);
|
|
1232
|
+
if (typeof index !== 'number') {
|
|
1233
|
+
throw new TypeError('Index must be a number.');
|
|
1234
|
+
}
|
|
1235
|
+
if (!this.existsTrack(index)) {
|
|
1236
|
+
throw new RangeError(`Index ${index} is out of bounds for the current playlist.`);
|
|
1237
|
+
}
|
|
1238
|
+
this.log('info', `Skipping to index: ${index}`);
|
|
1239
|
+
await this.stop();
|
|
1240
|
+
this.#currentIndex = index;
|
|
1241
|
+
this.emit('trackChange', this.#currentIndex);
|
|
1242
|
+
await this.play();
|
|
1243
|
+
}
|
|
1126
1244
|
/**
|
|
1127
1245
|
* Jumps to a specific absolute time in the timeline.
|
|
1128
1246
|
* @param {number} timeMs - The target time in milliseconds.
|
|
@@ -1138,6 +1256,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1138
1256
|
if (timeMs < 0) {
|
|
1139
1257
|
throw new RangeError('Seek time cannot be negative.');
|
|
1140
1258
|
}
|
|
1259
|
+
this.log('info', `Seeking to: ${timeMs}ms`);
|
|
1141
1260
|
const adapter = this.#getActiveAdapter();
|
|
1142
1261
|
if (adapter) {
|
|
1143
1262
|
await adapter.seek(timeMs);
|
|
@@ -1155,6 +1274,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1155
1274
|
if (typeof stepMs !== 'number') {
|
|
1156
1275
|
throw new TypeError('Step amount must be a number in milliseconds.');
|
|
1157
1276
|
}
|
|
1277
|
+
this.log('info', `Stepping by: ${stepMs}ms`);
|
|
1158
1278
|
const adapter = this.#getActiveAdapter();
|
|
1159
1279
|
if (!adapter)
|
|
1160
1280
|
return;
|
|
@@ -1224,6 +1344,34 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1224
1344
|
return 0;
|
|
1225
1345
|
}
|
|
1226
1346
|
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Checks whether the media adapter is paused.
|
|
1349
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
1350
|
+
*/
|
|
1351
|
+
get isPaused() {
|
|
1352
|
+
checkDestroy(this.#destroyed);
|
|
1353
|
+
try {
|
|
1354
|
+
const adapter = this.#getActiveAdapter();
|
|
1355
|
+
return adapter ? adapter.isPaused() : false;
|
|
1356
|
+
}
|
|
1357
|
+
catch {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Checks whether the media adapter is ended.
|
|
1363
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
1364
|
+
*/
|
|
1365
|
+
get isEnded() {
|
|
1366
|
+
checkDestroy(this.#destroyed);
|
|
1367
|
+
try {
|
|
1368
|
+
const adapter = this.#getActiveAdapter();
|
|
1369
|
+
return adapter ? adapter.isEnded() : true;
|
|
1370
|
+
}
|
|
1371
|
+
catch {
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1227
1375
|
/**
|
|
1228
1376
|
* Retrieves the metadata of the currently loaded content, returning a structured
|
|
1229
1377
|
* object containing details such as the content ID, title, author, and duration.
|
|
@@ -1275,6 +1423,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1275
1423
|
async destroy() {
|
|
1276
1424
|
if (this.#destroyed)
|
|
1277
1425
|
return;
|
|
1426
|
+
this.log('info', 'Destroying TinyMediaPlayer instance.');
|
|
1278
1427
|
// 1. Stop current playback if active to halt media processes
|
|
1279
1428
|
try {
|
|
1280
1429
|
if (this.#isPlaying) {
|
|
@@ -1282,6 +1431,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
1282
1431
|
}
|
|
1283
1432
|
}
|
|
1284
1433
|
catch (error) {
|
|
1434
|
+
this.log('error', 'Non-fatal error during playback termination:', error);
|
|
1285
1435
|
console.warn('[TinyMediaPlayer] Non-fatal error during playback termination:', error);
|
|
1286
1436
|
}
|
|
1287
1437
|
if (this.#prevClickTimer)
|
|
@@ -182,9 +182,10 @@ class TinyPromiseQueue {
|
|
|
182
182
|
* Inserts a point in the queue where subsequent tasks will be grouped and executed together in a Promise.all.
|
|
183
183
|
* If the queue is currently empty, behaves like a regular promise.
|
|
184
184
|
*
|
|
185
|
-
* @
|
|
185
|
+
* @template {any} PromiseResult
|
|
186
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise.
|
|
186
187
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
187
|
-
* @returns {Promise<
|
|
188
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
188
189
|
* @throws {Error} Throws if param is invalid.
|
|
189
190
|
*/
|
|
190
191
|
async enqueuePoint(task, id) {
|
|
@@ -206,10 +207,11 @@ class TinyPromiseQueue {
|
|
|
206
207
|
* If the task is canceled before execution, it will be rejected with the message:
|
|
207
208
|
* "The function was canceled on TinyPromiseQueue."
|
|
208
209
|
*
|
|
209
|
-
* @
|
|
210
|
+
* @template {any} PromiseResult
|
|
211
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise to be executed sequentially.
|
|
210
212
|
* @param {number|null} [delay] Optional delay (in ms) before the task is executed.
|
|
211
213
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
212
|
-
* @returns {Promise<
|
|
214
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
213
215
|
* @throws {Error} Throws if param is invalid.
|
|
214
216
|
*/
|
|
215
217
|
enqueue(task, delay, id) {
|
|
@@ -76,12 +76,13 @@ declare class TinyPromiseQueue {
|
|
|
76
76
|
* Inserts a point in the queue where subsequent tasks will be grouped and executed together in a Promise.all.
|
|
77
77
|
* If the queue is currently empty, behaves like a regular promise.
|
|
78
78
|
*
|
|
79
|
-
* @
|
|
79
|
+
* @template {any} PromiseResult
|
|
80
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise.
|
|
80
81
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
81
|
-
* @returns {Promise<
|
|
82
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
82
83
|
* @throws {Error} Throws if param is invalid.
|
|
83
84
|
*/
|
|
84
|
-
enqueuePoint(task: (...args: any[]) => Promise<
|
|
85
|
+
enqueuePoint<PromiseResult extends any>(task: (...args: any[]) => Promise<PromiseResult>, id?: string): Promise<PromiseResult>;
|
|
85
86
|
/**
|
|
86
87
|
* Adds a new async task to the queue and ensures it runs in order after previous tasks.
|
|
87
88
|
* Optionally, a delay can be added before the task is executed.
|
|
@@ -89,13 +90,14 @@ declare class TinyPromiseQueue {
|
|
|
89
90
|
* If the task is canceled before execution, it will be rejected with the message:
|
|
90
91
|
* "The function was canceled on TinyPromiseQueue."
|
|
91
92
|
*
|
|
92
|
-
* @
|
|
93
|
+
* @template {any} PromiseResult
|
|
94
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise to be executed sequentially.
|
|
93
95
|
* @param {number|null} [delay] Optional delay (in ms) before the task is executed.
|
|
94
96
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
95
|
-
* @returns {Promise<
|
|
97
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
96
98
|
* @throws {Error} Throws if param is invalid.
|
|
97
99
|
*/
|
|
98
|
-
enqueue(task: (...args: any[]) => Promise<
|
|
100
|
+
enqueue<PromiseResult extends any>(task: (...args: any[]) => Promise<PromiseResult>, delay?: number | null, id?: string): Promise<PromiseResult>;
|
|
99
101
|
/**
|
|
100
102
|
* Cancels a scheduled delay and removes the task from the queue.
|
|
101
103
|
* Adds the ID to a blacklist so the task is skipped if already being processed.
|
|
@@ -162,9 +162,10 @@ class TinyPromiseQueue {
|
|
|
162
162
|
* Inserts a point in the queue where subsequent tasks will be grouped and executed together in a Promise.all.
|
|
163
163
|
* If the queue is currently empty, behaves like a regular promise.
|
|
164
164
|
*
|
|
165
|
-
* @
|
|
165
|
+
* @template {any} PromiseResult
|
|
166
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise.
|
|
166
167
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
167
|
-
* @returns {Promise<
|
|
168
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
168
169
|
* @throws {Error} Throws if param is invalid.
|
|
169
170
|
*/
|
|
170
171
|
async enqueuePoint(task, id) {
|
|
@@ -186,10 +187,11 @@ class TinyPromiseQueue {
|
|
|
186
187
|
* If the task is canceled before execution, it will be rejected with the message:
|
|
187
188
|
* "The function was canceled on TinyPromiseQueue."
|
|
188
189
|
*
|
|
189
|
-
* @
|
|
190
|
+
* @template {any} PromiseResult
|
|
191
|
+
* @param {(...args: any[]) => Promise<PromiseResult>} task A function that returns a Promise to be executed sequentially.
|
|
190
192
|
* @param {number|null} [delay] Optional delay (in ms) before the task is executed.
|
|
191
193
|
* @param {string} [id] Optional ID to identify the task in the queue.
|
|
192
|
-
* @returns {Promise<
|
|
194
|
+
* @returns {Promise<PromiseResult>} A Promise that resolves or rejects with the result of the task once it's processed.
|
|
193
195
|
* @throws {Error} Throws if param is invalid.
|
|
194
196
|
*/
|
|
195
197
|
enqueue(task, delay, id) {
|