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.
Files changed (54) hide show
  1. package/changelog/1/28/6.md +3 -0
  2. package/changelog/1/28/7.md +7 -0
  3. package/changelog/1/28/8.md +17 -0
  4. package/dist/v1/TinyBasicsEs.min.js +1 -1
  5. package/dist/v1/TinyDebugger.min.js +1 -0
  6. package/dist/v1/TinyEssentials.min.js +1 -1
  7. package/dist/v1/TinyMediaPlayer.min.js +1 -1
  8. package/dist/v1/TinyRadioFm.min.js +1 -1
  9. package/dist/v1/TinyUploadClicker.min.js +1 -1
  10. package/dist/v1/basics/browserDetector.cjs +228 -0
  11. package/dist/v1/basics/browserDetector.d.mts +66 -0
  12. package/dist/v1/basics/browserDetector.mjs +207 -0
  13. package/dist/v1/basics/index.cjs +6 -0
  14. package/dist/v1/basics/index.d.mts +2 -1
  15. package/dist/v1/basics/index.mjs +2 -1
  16. package/dist/v1/build/TinyDebugger.cjs +7 -0
  17. package/dist/v1/build/TinyDebugger.d.mts +3 -0
  18. package/dist/v1/build/TinyDebugger.mjs +2 -0
  19. package/dist/v1/index.cjs +8 -0
  20. package/dist/v1/index.d.mts +3 -1
  21. package/dist/v1/index.mjs +4 -2
  22. package/dist/v1/libs/TinyDebugger.cjs +380 -0
  23. package/dist/v1/libs/TinyDebugger.d.mts +132 -0
  24. package/dist/v1/libs/TinyDebugger.mjs +351 -0
  25. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.cjs +66 -3
  26. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.d.mts +25 -0
  27. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.mjs +59 -3
  28. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.cjs +66 -3
  29. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.d.mts +25 -0
  30. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.mjs +59 -3
  31. package/dist/v1/libs/TinyMediaPlayer/Mock.cjs +333 -0
  32. package/dist/v1/libs/TinyMediaPlayer/Mock.d.mts +154 -0
  33. package/dist/v1/libs/TinyMediaPlayer/Mock.mjs +296 -0
  34. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.cjs +85 -1
  35. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.d.mts +25 -0
  36. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.mjs +75 -1
  37. package/dist/v1/libs/TinyMediaPlayer/Youtube.cjs +66 -3
  38. package/dist/v1/libs/TinyMediaPlayer/Youtube.d.mts +25 -0
  39. package/dist/v1/libs/TinyMediaPlayer/Youtube.mjs +56 -3
  40. package/dist/v1/libs/TinyMediaPlayer/index.cjs +45 -0
  41. package/dist/v1/libs/TinyMediaPlayer/index.d.mts +25 -0
  42. package/dist/v1/libs/TinyMediaPlayer/index.mjs +40 -0
  43. package/dist/v1/libs/TinyMediaPlayer.cjs +172 -10
  44. package/dist/v1/libs/TinyMediaPlayer.d.mts +47 -2
  45. package/dist/v1/libs/TinyMediaPlayer.mjs +160 -10
  46. package/dist/v1/libs/TinyPromiseQueue.cjs +6 -4
  47. package/dist/v1/libs/TinyPromiseQueue.d.mts +8 -6
  48. package/dist/v1/libs/TinyPromiseQueue.mjs +6 -4
  49. package/dist/v1/libs/TinyRadioFm.cjs +271 -138
  50. package/dist/v1/libs/TinyRadioFm.d.mts +67 -3
  51. package/dist/v1/libs/TinyRadioFm.mjs +239 -123
  52. package/docs/v1/libs/TinyMediaPlayer.md +52 -54
  53. package/docs/v1/libs/TinyRadioFm.md +25 -3
  54. package/package.json +13 -1
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var events = require('events');
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 events.EventEmitter {
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) return resolve();
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
- throw new Error(`No compatible adapter found for content ID: ${currentContent.id}.`);
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
- await adapter.play(this.#playlist[this.#currentIndex]);
1128
- this.#isPlaying = true;
1129
- this.emit('play', this.#currentIndex);
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
- // End of playlist, stop playing
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
- // End of playlist, stop playing
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 { EventEmitter } from 'events';
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 EventEmitter {
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.