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,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var TinyDebugger = require('./TinyDebugger.cjs');
|
|
4
4
|
var mediaContent = require('../basics/mediaContent.cjs');
|
|
5
5
|
var utils = require('./utils.cjs');
|
|
6
6
|
var index = require('./TinyMediaPlayer/index.cjs');
|
|
@@ -34,6 +34,9 @@ const checkDestroy = utils.createCheckDestroyed('TinyMediaPlayer');
|
|
|
34
34
|
* @property {boolean} [repeatCurrentOnPrev=false] - If true, clicking 'previous' repeats the current track on the first click.
|
|
35
35
|
* @property {boolean} [smoothPlayPauseVolume=false] - If true, volume fades smoothly during play/pause transitions.
|
|
36
36
|
* @property {boolean} [smoothStopVolume=false] - If true, volume fades smoothly to zero when stopping.
|
|
37
|
+
* @property {boolean} [debugMode=false] - Whether to enable internal debug logging.
|
|
38
|
+
* @property {boolean} [useLogColors=false] - Whether to enable log color support.
|
|
39
|
+
* @property {Console} [logger=console] - A custom logger object (must implement console methods).
|
|
37
40
|
*/
|
|
38
41
|
|
|
39
42
|
/**
|
|
@@ -46,7 +49,7 @@ const checkDestroy = utils.createCheckDestroyed('TinyMediaPlayer');
|
|
|
46
49
|
/**
|
|
47
50
|
* A universal media player manager capable of orchestrating multiple API adapters.
|
|
48
51
|
*/
|
|
49
|
-
class TinyMediaPlayer extends
|
|
52
|
+
class TinyMediaPlayer extends TinyDebugger {
|
|
50
53
|
static BaseMediaAdapter = index.BaseMediaAdapter;
|
|
51
54
|
|
|
52
55
|
/**
|
|
@@ -127,6 +130,9 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
127
130
|
/** @type {boolean} */
|
|
128
131
|
#isPlaying = false;
|
|
129
132
|
|
|
133
|
+
/** @type {boolean} */
|
|
134
|
+
#isMuted = false;
|
|
135
|
+
|
|
130
136
|
/** @type {number} */
|
|
131
137
|
#volume = 1.0;
|
|
132
138
|
|
|
@@ -263,11 +269,18 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
263
269
|
* @throws {RangeError} If numeric options are out of valid ranges.
|
|
264
270
|
*/
|
|
265
271
|
constructor(options = {}) {
|
|
266
|
-
super();
|
|
267
272
|
if (!objChecker.isValidObj(options)) {
|
|
268
273
|
throw new TypeError('Options must be a non-null object.');
|
|
269
274
|
}
|
|
270
275
|
|
|
276
|
+
// Debug Configuration
|
|
277
|
+
super({
|
|
278
|
+
id: '[_blue_TinyMediaPlayer_reset_] :debug:',
|
|
279
|
+
logger: options.logger ?? console,
|
|
280
|
+
debugMode: options.debugMode ?? false,
|
|
281
|
+
useLogColors: options.useLogColors ?? false,
|
|
282
|
+
});
|
|
283
|
+
|
|
271
284
|
// Volume configuration
|
|
272
285
|
if (options.persistVolume !== undefined) {
|
|
273
286
|
if (typeof options.persistVolume !== 'boolean') {
|
|
@@ -315,6 +328,8 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
315
328
|
}
|
|
316
329
|
|
|
317
330
|
if (this.#persistVolume) this.#loadVolumeFromStorage();
|
|
331
|
+
|
|
332
|
+
this.log('info', 'Instance initialized.', { options });
|
|
318
333
|
}
|
|
319
334
|
|
|
320
335
|
// ==========================================
|
|
@@ -332,6 +347,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
332
347
|
const parsedVolume = parseFloat(storedValue);
|
|
333
348
|
if (!isNaN(parsedVolume) && parsedVolume >= 0 && parsedVolume <= 1) {
|
|
334
349
|
this.#volume = parsedVolume;
|
|
350
|
+
this.log('info', `Volume loaded from storage: ${this.#volume}`);
|
|
335
351
|
}
|
|
336
352
|
}
|
|
337
353
|
}
|
|
@@ -347,6 +363,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
347
363
|
try {
|
|
348
364
|
if (this.#persistVolume && typeof window !== 'undefined' && window.localStorage) {
|
|
349
365
|
window.localStorage.setItem(this.#volumeStorageKey, this.#volume.toString());
|
|
366
|
+
this.log('info', `Volume saved to storage: ${this.#volume}`);
|
|
350
367
|
}
|
|
351
368
|
} catch (error) {
|
|
352
369
|
console.warn('[TinyMediaPlayer] Failed to save volume to localStorage.', error);
|
|
@@ -418,10 +435,15 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
418
435
|
const startTime = performance.now();
|
|
419
436
|
const duration = this.#fadeVolumeSpeed;
|
|
420
437
|
|
|
438
|
+
this.log('info', `Starting volume fade: ${startVolume} -> ${targetVolume}`);
|
|
439
|
+
|
|
421
440
|
return new Promise((resolve) => {
|
|
422
441
|
/** @param {number} currentTime */
|
|
423
442
|
const step = (currentTime) => {
|
|
424
|
-
if (signal.aborted)
|
|
443
|
+
if (signal.aborted) {
|
|
444
|
+
this.log('info', 'Volume fade aborted.');
|
|
445
|
+
return resolve();
|
|
446
|
+
}
|
|
425
447
|
|
|
426
448
|
const elapsed = currentTime - startTime;
|
|
427
449
|
const progress = Math.min(elapsed / duration, 1);
|
|
@@ -433,6 +455,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
433
455
|
requestAnimationFrame(step);
|
|
434
456
|
} else {
|
|
435
457
|
adapter.setVolume(targetVolume);
|
|
458
|
+
this.log('info', `Volume fade completed: ${targetVolume}`);
|
|
436
459
|
resolve();
|
|
437
460
|
}
|
|
438
461
|
};
|
|
@@ -474,6 +497,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
474
497
|
}
|
|
475
498
|
|
|
476
499
|
this.#playlist = value;
|
|
500
|
+
this.log('info', `Playlist updated. New size: ${this.#playlist.length}`);
|
|
477
501
|
this.emit('playlistUpdate', this.playlist);
|
|
478
502
|
|
|
479
503
|
// Validation to correct currentIndex ensuring it aligns with the new playlist limits
|
|
@@ -494,6 +518,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
494
518
|
if (oldAdapter) await oldAdapter.stop();
|
|
495
519
|
if (this.#currentIndex !== -1) await this.play();
|
|
496
520
|
} catch (error) {
|
|
521
|
+
this.log('error', 'Background transition error on playlist update:', error);
|
|
497
522
|
console.warn('[TinyMediaPlayer] Background transition error on playlist update:', error);
|
|
498
523
|
}
|
|
499
524
|
})();
|
|
@@ -522,6 +547,8 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
522
547
|
|
|
523
548
|
if (this.#currentIndex === value) return; // Optimization: do nothing if index is the same
|
|
524
549
|
|
|
550
|
+
this.log('info', `Index change requested: ${this.#currentIndex} -> ${value}`);
|
|
551
|
+
|
|
525
552
|
const wasPlaying = this.#isPlaying;
|
|
526
553
|
let oldAdapter = null;
|
|
527
554
|
|
|
@@ -543,6 +570,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
543
570
|
if (oldAdapter) await oldAdapter.stop();
|
|
544
571
|
if (this.#currentIndex !== -1) await this.play();
|
|
545
572
|
} catch (error) {
|
|
573
|
+
this.log('error', 'Background transition error on index update:', error);
|
|
546
574
|
console.warn('[TinyMediaPlayer] Background transition error on index update:', error);
|
|
547
575
|
}
|
|
548
576
|
})();
|
|
@@ -566,6 +594,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
566
594
|
throw new TypeError(`Loop mode must be one of: ${validModes.join(', ')}.`);
|
|
567
595
|
}
|
|
568
596
|
this.#loopMode = value;
|
|
597
|
+
this.log('info', `Loop mode changed to: ${this.#loopMode}`);
|
|
569
598
|
this.emit('loopModeChange', this.#loopMode);
|
|
570
599
|
}
|
|
571
600
|
|
|
@@ -585,6 +614,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
585
614
|
throw new TypeError('Random mode state must be a boolean.');
|
|
586
615
|
}
|
|
587
616
|
this.#isRandom = value;
|
|
617
|
+
this.log('info', `Random mode: ${this.#isRandom}`);
|
|
588
618
|
this.emit('randomModeChange', this.#isRandom);
|
|
589
619
|
}
|
|
590
620
|
|
|
@@ -614,6 +644,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
614
644
|
// Abort any ongoing fade to ensure manual control is immediate
|
|
615
645
|
if (this.#fadeController) this.#fadeController.abort();
|
|
616
646
|
|
|
647
|
+
this.log('info', `Volume changed: ${this.#volume} -> ${value}`);
|
|
617
648
|
this.#volume = value;
|
|
618
649
|
this.#saveVolumeToStorage();
|
|
619
650
|
this.emit('volumeChange', this.#volume);
|
|
@@ -645,6 +676,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
645
676
|
throw new TypeError('Persist volume parameter must be a boolean.');
|
|
646
677
|
}
|
|
647
678
|
this.#persistVolume = value;
|
|
679
|
+
this.log('info', `Persist volume: ${this.#persistVolume}`);
|
|
648
680
|
if (value) {
|
|
649
681
|
this.#saveVolumeToStorage();
|
|
650
682
|
}
|
|
@@ -683,6 +715,40 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
683
715
|
}
|
|
684
716
|
}
|
|
685
717
|
|
|
718
|
+
/** @returns {boolean} True if the player is currently muted. */
|
|
719
|
+
get isMuted() {
|
|
720
|
+
checkDestroy(this.#destroyed);
|
|
721
|
+
return this.#isMuted;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Mutes the current playback.
|
|
726
|
+
* @returns {Promise<void>}
|
|
727
|
+
*/
|
|
728
|
+
async mute() {
|
|
729
|
+
checkDestroy(this.#destroyed);
|
|
730
|
+
const adapter = this.#getActiveAdapter();
|
|
731
|
+
if (!adapter) return;
|
|
732
|
+
await adapter.mute();
|
|
733
|
+
this.#isMuted = true;
|
|
734
|
+
this.emit('muteChange', this.#isMuted);
|
|
735
|
+
this.log('info', 'Playback muted successfully.');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Unmutes the current playback.
|
|
740
|
+
* @returns {Promise<void>}
|
|
741
|
+
*/
|
|
742
|
+
async unMute() {
|
|
743
|
+
checkDestroy(this.#destroyed);
|
|
744
|
+
const adapter = this.#getActiveAdapter();
|
|
745
|
+
if (!adapter) return;
|
|
746
|
+
await adapter.unMute();
|
|
747
|
+
this.#isMuted = false;
|
|
748
|
+
this.emit('muteChange', this.#isMuted);
|
|
749
|
+
this.log('info', 'Playback unmuted successfully.');
|
|
750
|
+
}
|
|
751
|
+
|
|
686
752
|
// ==========================================
|
|
687
753
|
// ADAPTER MANAGEMENT
|
|
688
754
|
// ==========================================
|
|
@@ -737,6 +803,8 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
737
803
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
738
804
|
}
|
|
739
805
|
|
|
806
|
+
this.log('info', 'Registering new adapter:', adapter);
|
|
807
|
+
|
|
740
808
|
const events = new Map();
|
|
741
809
|
this.#adapterHandlers.set(adapter, events);
|
|
742
810
|
|
|
@@ -759,6 +827,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
759
827
|
if (!(adapter instanceof index.BaseMediaAdapter)) {
|
|
760
828
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
761
829
|
}
|
|
830
|
+
this.log('info', 'Removing adapter:', adapter);
|
|
762
831
|
this.#removeHandler(adapter);
|
|
763
832
|
return this.#adapters.delete(adapter);
|
|
764
833
|
}
|
|
@@ -773,6 +842,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
773
842
|
if (!(adapter instanceof index.BaseMediaAdapter)) {
|
|
774
843
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
775
844
|
}
|
|
845
|
+
this.log('info', 'Destroying adapter:', adapter);
|
|
776
846
|
this.#removeHandler(adapter);
|
|
777
847
|
const result = this.#adapters.delete(adapter);
|
|
778
848
|
if (result) adapter.destroy();
|
|
@@ -797,6 +867,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
797
867
|
*/
|
|
798
868
|
destroyAllAdapters() {
|
|
799
869
|
checkDestroy(this.#destroyed);
|
|
870
|
+
this.log('info', 'Destroying all adapters.');
|
|
800
871
|
this.#adapters.forEach((adapter) => {
|
|
801
872
|
this.#removeHandler(adapter);
|
|
802
873
|
adapter.destroy();
|
|
@@ -809,6 +880,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
809
880
|
*/
|
|
810
881
|
clearAdapters() {
|
|
811
882
|
checkDestroy(this.#destroyed);
|
|
883
|
+
this.log('info', 'Clearing all adapters.');
|
|
812
884
|
this.#adapters.forEach((adapter) => this.#removeHandler(adapter));
|
|
813
885
|
this.#adapters.clear();
|
|
814
886
|
}
|
|
@@ -841,7 +913,9 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
841
913
|
const currentContent = this.#playlist[this.#currentIndex];
|
|
842
914
|
const adapter = this.getMediaAdapter(currentContent);
|
|
843
915
|
if (adapter) return adapter;
|
|
844
|
-
|
|
916
|
+
const errorMsg = `No compatible adapter found for content ID: ${currentContent.id}.`;
|
|
917
|
+
this.log('error', errorMsg);
|
|
918
|
+
throw new Error(errorMsg);
|
|
845
919
|
}
|
|
846
920
|
|
|
847
921
|
// ==========================================
|
|
@@ -862,6 +936,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
862
936
|
// If the event is already in the list, do nothing to avoid duplicates
|
|
863
937
|
if (this.#adapterEventNames.has(eventName)) return;
|
|
864
938
|
|
|
939
|
+
this.log('info', `Adding adapter event listener: ${eventName}`);
|
|
865
940
|
this.#adapterEventNames.add(eventName);
|
|
866
941
|
|
|
867
942
|
// Synchronize: Add the listener to all already registered adapters
|
|
@@ -883,6 +958,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
883
958
|
|
|
884
959
|
if (!this.#adapterEventNames.has(eventName)) return;
|
|
885
960
|
|
|
961
|
+
this.log('info', `Removing adapter event listener: ${eventName}`);
|
|
886
962
|
this.#adapterEventNames.delete(eventName);
|
|
887
963
|
|
|
888
964
|
// Synchronize: Remove the listener from all registered adapters
|
|
@@ -930,6 +1006,8 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
930
1006
|
/** @type {(...args: any[]) => any} */
|
|
931
1007
|
const handler = (...args) => {
|
|
932
1008
|
if (!this.#destroyed) {
|
|
1009
|
+
if (eventName !== 'timeupdate')
|
|
1010
|
+
this.log('debug', `Adapter event triggered: ${eventName}`, ...args);
|
|
933
1011
|
this.emit(eventName, ...args);
|
|
934
1012
|
}
|
|
935
1013
|
};
|
|
@@ -975,6 +1053,8 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
975
1053
|
mediaContent.valMediaContentMetadata(newContent);
|
|
976
1054
|
|
|
977
1055
|
const newLength = this.#playlist.push(newContent);
|
|
1056
|
+
this.log('info', `Track added. New length: ${newLength}`, newContent);
|
|
1057
|
+
|
|
978
1058
|
if (this.#currentIndex === -1) {
|
|
979
1059
|
this.#currentIndex = 0;
|
|
980
1060
|
this.emit('trackChange', this.#currentIndex);
|
|
@@ -1031,6 +1111,8 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1031
1111
|
throw new RangeError(`Index ${index} is out of bounds for the current playlist.`);
|
|
1032
1112
|
}
|
|
1033
1113
|
|
|
1114
|
+
this.log('info', `Removing track at index: ${index}`);
|
|
1115
|
+
|
|
1034
1116
|
if (index === this.#currentIndex) {
|
|
1035
1117
|
// Stop the current track if it is the one being removed
|
|
1036
1118
|
await this.stop();
|
|
@@ -1067,6 +1149,8 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1067
1149
|
throw new TypeError('Search query must be a string or a boolean evaluation function.');
|
|
1068
1150
|
}
|
|
1069
1151
|
|
|
1152
|
+
this.log('info', 'Searching playlist...', { query });
|
|
1153
|
+
|
|
1070
1154
|
const results = [];
|
|
1071
1155
|
|
|
1072
1156
|
for (let i = 0; i < this.#playlist.length; i++) {
|
|
@@ -1089,6 +1173,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1089
1173
|
}
|
|
1090
1174
|
}
|
|
1091
1175
|
|
|
1176
|
+
this.log('info', `Search completed. Found ${results.length} results.`);
|
|
1092
1177
|
return results;
|
|
1093
1178
|
}
|
|
1094
1179
|
|
|
@@ -1097,6 +1182,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1097
1182
|
*/
|
|
1098
1183
|
async clearPlaylist() {
|
|
1099
1184
|
checkDestroy(this.#destroyed);
|
|
1185
|
+
this.log('info', 'Clearing entire playlist.');
|
|
1100
1186
|
await this.stop();
|
|
1101
1187
|
this.#playlist = [];
|
|
1102
1188
|
this.#currentIndex = -1;
|
|
@@ -1114,6 +1200,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1114
1200
|
*/
|
|
1115
1201
|
async play() {
|
|
1116
1202
|
checkDestroy(this.#destroyed);
|
|
1203
|
+
this.log('info', `Attempting to play track at index: ${this.#currentIndex}`);
|
|
1117
1204
|
const adapter = this.#getActiveAdapter();
|
|
1118
1205
|
if (!adapter) return;
|
|
1119
1206
|
|
|
@@ -1124,9 +1211,16 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1124
1211
|
adapter.setVolume(this.#volume);
|
|
1125
1212
|
}
|
|
1126
1213
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1214
|
+
try {
|
|
1215
|
+
await adapter.play(this.#playlist[this.#currentIndex]);
|
|
1216
|
+
if (this.#isMuted) await adapter.mute();
|
|
1217
|
+
this.#isPlaying = true;
|
|
1218
|
+
this.log('info', 'Playback started successfully.');
|
|
1219
|
+
this.emit('play', this.#currentIndex);
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
this.log('error', 'Failed to play track:', error);
|
|
1222
|
+
throw error;
|
|
1223
|
+
}
|
|
1130
1224
|
}
|
|
1131
1225
|
|
|
1132
1226
|
/**
|
|
@@ -1135,6 +1229,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1135
1229
|
*/
|
|
1136
1230
|
async pause() {
|
|
1137
1231
|
checkDestroy(this.#destroyed);
|
|
1232
|
+
this.log('info', 'Attempting to pause playback.');
|
|
1138
1233
|
const adapter = this.#getActiveAdapter();
|
|
1139
1234
|
if (!adapter) return;
|
|
1140
1235
|
|
|
@@ -1144,6 +1239,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1144
1239
|
|
|
1145
1240
|
await adapter.pause();
|
|
1146
1241
|
this.#isPlaying = false;
|
|
1242
|
+
this.log('info', 'Playback paused.');
|
|
1147
1243
|
this.emit('pause', this.#currentIndex);
|
|
1148
1244
|
}
|
|
1149
1245
|
|
|
@@ -1153,6 +1249,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1153
1249
|
*/
|
|
1154
1250
|
async stop() {
|
|
1155
1251
|
checkDestroy(this.#destroyed);
|
|
1252
|
+
this.log('info', 'Attempting to stop playback.');
|
|
1156
1253
|
const adapter = this.#getActiveAdapter();
|
|
1157
1254
|
if (!adapter) return;
|
|
1158
1255
|
|
|
@@ -1162,6 +1259,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1162
1259
|
|
|
1163
1260
|
await adapter.stop();
|
|
1164
1261
|
this.#isPlaying = false;
|
|
1262
|
+
this.log('info', 'Playback stopped.');
|
|
1165
1263
|
this.emit('stop', this.#currentIndex);
|
|
1166
1264
|
}
|
|
1167
1265
|
|
|
@@ -1173,9 +1271,11 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1173
1271
|
checkDestroy(this.#destroyed);
|
|
1174
1272
|
if (this.#playlist.length === 0) return;
|
|
1175
1273
|
|
|
1274
|
+
this.log('info', 'Next track requested.');
|
|
1176
1275
|
await this.stop();
|
|
1177
1276
|
|
|
1178
1277
|
if (this.#loopMode === 'TRACK') {
|
|
1278
|
+
this.log('info', 'Loop mode: TRACK. Repeating current track.');
|
|
1179
1279
|
// Index remains the same
|
|
1180
1280
|
await this.play();
|
|
1181
1281
|
return;
|
|
@@ -1183,13 +1283,15 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1183
1283
|
|
|
1184
1284
|
if (this.#isRandom) {
|
|
1185
1285
|
this.#currentIndex = this._getWeightedRandomIndex();
|
|
1286
|
+
this.log('info', `Random mode: Selected index ${this.#currentIndex}`);
|
|
1186
1287
|
} else {
|
|
1187
1288
|
let nextIndex = this.#currentIndex + 1;
|
|
1188
1289
|
if (nextIndex >= this.#playlist.length) {
|
|
1189
1290
|
if (this.#loopMode === 'PLAYLIST') {
|
|
1190
1291
|
nextIndex = 0;
|
|
1292
|
+
this.log('info', 'Loop mode: PLAYLIST. Returning to start.');
|
|
1191
1293
|
} else {
|
|
1192
|
-
|
|
1294
|
+
this.log('info', 'End of playlist reached. Stopping.');
|
|
1193
1295
|
return;
|
|
1194
1296
|
}
|
|
1195
1297
|
}
|
|
@@ -1210,6 +1312,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1210
1312
|
|
|
1211
1313
|
// UX: Repeat current track if "repeat on prev" is enabled and it's the first click
|
|
1212
1314
|
if (this.#repeatCurrentOnPrev && !this.#prevClickedToRepeat) {
|
|
1315
|
+
this.log('info', 'UX: Repeat current track on first "prev" click.');
|
|
1213
1316
|
this.#prevClickedToRepeat = true;
|
|
1214
1317
|
|
|
1215
1318
|
// Reset the "repeat" state after 2 seconds of inactivity to prevent glitches
|
|
@@ -1231,17 +1334,20 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1231
1334
|
}
|
|
1232
1335
|
this.#prevClickedToRepeat = false;
|
|
1233
1336
|
|
|
1337
|
+
this.log('info', 'Previous track requested.');
|
|
1234
1338
|
await this.stop();
|
|
1235
1339
|
|
|
1236
1340
|
if (this.#isRandom) {
|
|
1237
1341
|
this.#currentIndex = this._getWeightedRandomIndex();
|
|
1342
|
+
this.log('info', `Random mode: Selected index ${this.#currentIndex}`);
|
|
1238
1343
|
} else {
|
|
1239
1344
|
let prevIndex = this.#currentIndex - 1;
|
|
1240
1345
|
if (prevIndex < 0) {
|
|
1241
1346
|
if (this.#loopMode === 'PLAYLIST') {
|
|
1242
1347
|
prevIndex = this.#playlist.length - 1;
|
|
1348
|
+
this.log('info', 'Loop mode: PLAYLIST. Returning to end.');
|
|
1243
1349
|
} else {
|
|
1244
|
-
|
|
1350
|
+
this.log('info', 'Start of playlist reached. Stopping.');
|
|
1245
1351
|
return;
|
|
1246
1352
|
}
|
|
1247
1353
|
}
|
|
@@ -1252,6 +1358,29 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1252
1358
|
await this.play();
|
|
1253
1359
|
}
|
|
1254
1360
|
|
|
1361
|
+
/**
|
|
1362
|
+
* Jumps directly to a specific index in the playlist.
|
|
1363
|
+
* @param {number} index - The target index to skip to.
|
|
1364
|
+
* @returns {Promise<void>}
|
|
1365
|
+
* @throws {TypeError} If the index is not a number.
|
|
1366
|
+
* @throws {RangeError} If the index is out of the playlist bounds.
|
|
1367
|
+
*/
|
|
1368
|
+
async skipTo(index) {
|
|
1369
|
+
checkDestroy(this.#destroyed);
|
|
1370
|
+
if (typeof index !== 'number') {
|
|
1371
|
+
throw new TypeError('Index must be a number.');
|
|
1372
|
+
}
|
|
1373
|
+
if (!this.existsTrack(index)) {
|
|
1374
|
+
throw new RangeError(`Index ${index} is out of bounds for the current playlist.`);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
this.log('info', `Skipping to index: ${index}`);
|
|
1378
|
+
await this.stop();
|
|
1379
|
+
this.#currentIndex = index;
|
|
1380
|
+
this.emit('trackChange', this.#currentIndex);
|
|
1381
|
+
await this.play();
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1255
1384
|
/**
|
|
1256
1385
|
* Jumps to a specific absolute time in the timeline.
|
|
1257
1386
|
* @param {number} timeMs - The target time in milliseconds.
|
|
@@ -1268,6 +1397,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1268
1397
|
throw new RangeError('Seek time cannot be negative.');
|
|
1269
1398
|
}
|
|
1270
1399
|
|
|
1400
|
+
this.log('info', `Seeking to: ${timeMs}ms`);
|
|
1271
1401
|
const adapter = this.#getActiveAdapter();
|
|
1272
1402
|
if (adapter) {
|
|
1273
1403
|
await adapter.seek(timeMs);
|
|
@@ -1287,6 +1417,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1287
1417
|
throw new TypeError('Step amount must be a number in milliseconds.');
|
|
1288
1418
|
}
|
|
1289
1419
|
|
|
1420
|
+
this.log('info', `Stepping by: ${stepMs}ms`);
|
|
1290
1421
|
const adapter = this.#getActiveAdapter();
|
|
1291
1422
|
if (!adapter) return;
|
|
1292
1423
|
|
|
@@ -1359,6 +1490,34 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1359
1490
|
}
|
|
1360
1491
|
}
|
|
1361
1492
|
|
|
1493
|
+
/**
|
|
1494
|
+
* Checks whether the media adapter is paused.
|
|
1495
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
1496
|
+
*/
|
|
1497
|
+
get isPaused() {
|
|
1498
|
+
checkDestroy(this.#destroyed);
|
|
1499
|
+
try {
|
|
1500
|
+
const adapter = this.#getActiveAdapter();
|
|
1501
|
+
return adapter ? adapter.isPaused() : false;
|
|
1502
|
+
} catch {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Checks whether the media adapter is ended.
|
|
1509
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
1510
|
+
*/
|
|
1511
|
+
get isEnded() {
|
|
1512
|
+
checkDestroy(this.#destroyed);
|
|
1513
|
+
try {
|
|
1514
|
+
const adapter = this.#getActiveAdapter();
|
|
1515
|
+
return adapter ? adapter.isEnded() : true;
|
|
1516
|
+
} catch {
|
|
1517
|
+
return true;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1362
1521
|
/**
|
|
1363
1522
|
* Retrieves the metadata of the currently loaded content, returning a structured
|
|
1364
1523
|
* object containing details such as the content ID, title, author, and duration.
|
|
@@ -1411,12 +1570,15 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
1411
1570
|
async destroy() {
|
|
1412
1571
|
if (this.#destroyed) return;
|
|
1413
1572
|
|
|
1573
|
+
this.log('info', 'Destroying TinyMediaPlayer instance.');
|
|
1574
|
+
|
|
1414
1575
|
// 1. Stop current playback if active to halt media processes
|
|
1415
1576
|
try {
|
|
1416
1577
|
if (this.#isPlaying) {
|
|
1417
1578
|
await this.stop();
|
|
1418
1579
|
}
|
|
1419
1580
|
} catch (error) {
|
|
1581
|
+
this.log('error', 'Non-fatal error during playback termination:', error);
|
|
1420
1582
|
console.warn('[TinyMediaPlayer] Non-fatal error during playback termination:', error);
|
|
1421
1583
|
}
|
|
1422
1584
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import TinyDebugger from './TinyDebugger.mjs';
|
|
2
2
|
import { BaseMediaAdapter } from './TinyMediaPlayer/index.mjs';
|
|
3
3
|
export type PictureDataType = import('../basics/mediaContent.mjs').PictureDataType;
|
|
4
4
|
export type MediaContent = import('../basics/mediaContent.mjs').MediaContent<PictureDataType>;
|
|
@@ -32,6 +32,18 @@ export type TinyMediaPlayerOptions = {
|
|
|
32
32
|
* - If true, volume fades smoothly to zero when stopping.
|
|
33
33
|
*/
|
|
34
34
|
smoothStopVolume?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* - Whether to enable internal debug logging.
|
|
37
|
+
*/
|
|
38
|
+
debugMode?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* - Whether to enable log color support.
|
|
41
|
+
*/
|
|
42
|
+
useLogColors?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* - A custom logger object (must implement console methods).
|
|
45
|
+
*/
|
|
46
|
+
logger?: Console;
|
|
35
47
|
};
|
|
36
48
|
export type SearchResult = {
|
|
37
49
|
/**
|
|
@@ -67,6 +79,9 @@ export type SearchResult = {
|
|
|
67
79
|
* @property {boolean} [repeatCurrentOnPrev=false] - If true, clicking 'previous' repeats the current track on the first click.
|
|
68
80
|
* @property {boolean} [smoothPlayPauseVolume=false] - If true, volume fades smoothly during play/pause transitions.
|
|
69
81
|
* @property {boolean} [smoothStopVolume=false] - If true, volume fades smoothly to zero when stopping.
|
|
82
|
+
* @property {boolean} [debugMode=false] - Whether to enable internal debug logging.
|
|
83
|
+
* @property {boolean} [useLogColors=false] - Whether to enable log color support.
|
|
84
|
+
* @property {Console} [logger=console] - A custom logger object (must implement console methods).
|
|
70
85
|
*/
|
|
71
86
|
/**
|
|
72
87
|
* Represents a search match containing the media object and its exact playlist index.
|
|
@@ -77,7 +92,7 @@ export type SearchResult = {
|
|
|
77
92
|
/**
|
|
78
93
|
* A universal media player manager capable of orchestrating multiple API adapters.
|
|
79
94
|
*/
|
|
80
|
-
declare class TinyMediaPlayer extends
|
|
95
|
+
declare class TinyMediaPlayer extends TinyDebugger {
|
|
81
96
|
#private;
|
|
82
97
|
static BaseMediaAdapter: typeof BaseMediaAdapter;
|
|
83
98
|
/**
|
|
@@ -220,6 +235,18 @@ declare class TinyMediaPlayer extends EventEmitter {
|
|
|
220
235
|
* @throws {TypeError} If the value is not a string or is empty.
|
|
221
236
|
*/
|
|
222
237
|
set volumeStorageKey(value: string);
|
|
238
|
+
/** @returns {boolean} True if the player is currently muted. */
|
|
239
|
+
get isMuted(): boolean;
|
|
240
|
+
/**
|
|
241
|
+
* Mutes the current playback.
|
|
242
|
+
* @returns {Promise<void>}
|
|
243
|
+
*/
|
|
244
|
+
mute(): Promise<void>;
|
|
245
|
+
/**
|
|
246
|
+
* Unmutes the current playback.
|
|
247
|
+
* @returns {Promise<void>}
|
|
248
|
+
*/
|
|
249
|
+
unMute(): Promise<void>;
|
|
223
250
|
get adapters(): BaseMediaAdapter[];
|
|
224
251
|
get adaptersSize(): number;
|
|
225
252
|
static get defaultAdapterEventNames(): string[];
|
|
@@ -347,6 +374,14 @@ declare class TinyMediaPlayer extends EventEmitter {
|
|
|
347
374
|
* @returns {Promise<void>}
|
|
348
375
|
*/
|
|
349
376
|
prev(): Promise<void>;
|
|
377
|
+
/**
|
|
378
|
+
* Jumps directly to a specific index in the playlist.
|
|
379
|
+
* @param {number} index - The target index to skip to.
|
|
380
|
+
* @returns {Promise<void>}
|
|
381
|
+
* @throws {TypeError} If the index is not a number.
|
|
382
|
+
* @throws {RangeError} If the index is out of the playlist bounds.
|
|
383
|
+
*/
|
|
384
|
+
skipTo(index: number): Promise<void>;
|
|
350
385
|
/**
|
|
351
386
|
* Jumps to a specific absolute time in the timeline.
|
|
352
387
|
* @param {number} timeMs - The target time in milliseconds.
|
|
@@ -382,6 +417,16 @@ declare class TinyMediaPlayer extends EventEmitter {
|
|
|
382
417
|
* @returns {number} A value between 0 and 100.
|
|
383
418
|
*/
|
|
384
419
|
getPlaybackPercentage(): number;
|
|
420
|
+
/**
|
|
421
|
+
* Checks whether the media adapter is paused.
|
|
422
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
423
|
+
*/
|
|
424
|
+
get isPaused(): boolean;
|
|
425
|
+
/**
|
|
426
|
+
* Checks whether the media adapter is ended.
|
|
427
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
428
|
+
*/
|
|
429
|
+
get isEnded(): boolean;
|
|
385
430
|
/**
|
|
386
431
|
* Retrieves the metadata of the currently loaded content, returning a structured
|
|
387
432
|
* object containing details such as the content ID, title, author, and duration.
|