tiny-essentials 1.28.4 → 1.28.6
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/README.md +18 -1
- package/changelog/1/28/5.md +31 -0
- package/changelog/1/28/6.md +11 -0
- package/dist/v1/TinyAdvancedRaffle.min.js +1 -1
- package/dist/v1/TinyBasicsEs.min.js +1 -1
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinyIframeEvents.min.js +1 -1
- package/dist/v1/TinyLocalStorage.min.js +1 -1
- package/dist/v1/TinyMediaPlayer.min.js +1 -1
- package/dist/v1/TinyNewWinEvents.min.js +1 -1
- package/dist/v1/TinyRadioFm.min.js +1 -1
- package/dist/v1/TinySmartScroller.min.js +1 -1
- package/dist/v1/TinyTimeout.min.js +1 -1
- package/dist/v1/TinyUploadClicker.min.js +1 -1
- package/dist/v1/basics/index.cjs +4 -0
- package/dist/v1/basics/index.d.mts +3 -2
- package/dist/v1/basics/index.mjs +3 -2
- package/dist/v1/basics/mediaContent.cjs +1 -0
- package/dist/v1/basics/mediaContent.d.mts +4 -0
- package/dist/v1/basics/mediaContent.mjs +1 -0
- package/dist/v1/basics/objChecker.cjs +16 -0
- package/dist/v1/basics/objChecker.d.mts +12 -0
- package/dist/v1/basics/objChecker.mjs +14 -0
- package/dist/v1/basics/objFilter.cjs +0 -2
- package/dist/v1/basics/objFilter.d.mts +0 -2
- package/dist/v1/basics/objFilter.mjs +1 -2
- package/dist/v1/basics/promiseUtils.cjs +101 -0
- package/dist/v1/basics/promiseUtils.d.mts +31 -0
- package/dist/v1/basics/promiseUtils.mjs +89 -0
- package/dist/v1/css/TinyLoadingScreen.min.css +1 -1
- package/dist/v1/index.cjs +4 -0
- package/dist/v1/index.d.mts +3 -2
- package/dist/v1/index.mjs +3 -2
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.cjs +318 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.d.mts +143 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.mjs +283 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.cjs +315 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.d.mts +143 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.mjs +280 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.cjs +792 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.d.mts +300 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.mjs +697 -0
- package/dist/v1/libs/TinyMediaPlayer/Youtube.cjs +783 -0
- package/dist/v1/libs/TinyMediaPlayer/Youtube.d.mts +355 -0
- package/dist/v1/libs/TinyMediaPlayer/Youtube.mjs +694 -0
- package/dist/v1/libs/TinyMediaPlayer/docs/SoundCloudWidget.cjs +381 -0
- package/dist/v1/libs/TinyMediaPlayer/docs/SoundCloudWidget.d.mts +704 -0
- package/dist/v1/libs/TinyMediaPlayer/docs/SoundCloudWidget.mjs +343 -0
- package/dist/v1/libs/TinyMediaPlayer/docs/YouTubePlayer.cjs +546 -0
- package/dist/v1/libs/TinyMediaPlayer/docs/YouTubePlayer.d.mts +657 -0
- package/dist/v1/libs/TinyMediaPlayer/docs/YouTubePlayer.mjs +480 -0
- package/dist/v1/libs/TinyMediaPlayer/index.cjs +216 -0
- package/dist/v1/libs/TinyMediaPlayer/index.d.mts +162 -0
- package/dist/v1/libs/TinyMediaPlayer/index.mjs +190 -0
- package/dist/v1/libs/TinyMediaPlayer.cjs +627 -103
- package/dist/v1/libs/TinyMediaPlayer.d.mts +163 -65
- package/dist/v1/libs/TinyMediaPlayer.mjs +573 -93
- package/dist/v1/libs/TinyTimeout.cjs +4 -14
- package/dist/v1/libs/TinyTimeout.d.mts +1 -0
- package/dist/v1/libs/TinyTimeout.mjs +4 -13
- package/docs/v1/basics/mediaContent.md +1 -1
- package/docs/v1/basics/objChecker.md +13 -0
- package/docs/v1/basics/promiseUtils.md +32 -0
- package/docs/v1/libs/TinyMediaPlayer.md +268 -39
- package/docs/v1/libs/TinyTimeout.md +1 -14
- package/package.json +71 -10
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import { getMediaContentBase, getMediaContentMetadata, parseMediaMetadata, valMediaContentMetadata, } from '../basics/mediaContent.mjs';
|
|
3
3
|
import { createCheckDestroyed } from './utils.mjs';
|
|
4
|
+
import { BaseMediaAdapter } from './TinyMediaPlayer/index.mjs';
|
|
5
|
+
import { isValidObj } from '../basics/objChecker.mjs';
|
|
4
6
|
const checkDestroy = createCheckDestroyed('TinyMediaPlayer');
|
|
5
7
|
/**
|
|
6
8
|
* @typedef {import('../basics/mediaContent.mjs').PictureDataType} PictureDataType
|
|
@@ -11,82 +13,22 @@ const checkDestroy = createCheckDestroyed('TinyMediaPlayer');
|
|
|
11
13
|
* @typedef {import('../basics/mediaContent.mjs').LoadingMediaProgress} LoadingMediaProgress
|
|
12
14
|
* @typedef {import('../basics/mediaContent.mjs').MediaLoadingErrorData} MediaLoadingErrorData
|
|
13
15
|
* @typedef {import('../basics/mediaContent.mjs').UnknownArtistGetter} UnknownArtistGetter
|
|
16
|
+
* @typedef {import('./TinyMediaPlayer/index.mjs').ContentTimeData} ContentTimeData
|
|
17
|
+
* @typedef {import('./TinyMediaPlayer/index.mjs').ContentData} ContentData
|
|
14
18
|
*/
|
|
15
19
|
/**
|
|
16
20
|
* The loop configuration modes.
|
|
17
21
|
* @typedef {'NONE' | 'TRACK' | 'PLAYLIST'} LoopModeType
|
|
18
22
|
*/
|
|
19
23
|
/**
|
|
20
|
-
* Configuration options for initializing the
|
|
24
|
+
* Configuration options for initializing the TinyMediaPlayer.
|
|
21
25
|
* @typedef {Object} TinyMediaPlayerOptions
|
|
22
26
|
* @property {boolean} [persistVolume=false] - Whether to automatically save the volume in localStorage.
|
|
23
|
-
* @property {string} [volumeStorageKey='
|
|
27
|
+
* @property {string} [volumeStorageKey='tiny_media_player_volume'] - The specific key name used for localStorage cache.
|
|
28
|
+
* @property {boolean} [repeatCurrentOnPrev=false] - If true, clicking 'previous' repeats the current track on the first click.
|
|
29
|
+
* @property {boolean} [smoothPlayPauseVolume=false] - If true, volume fades smoothly during play/pause transitions.
|
|
30
|
+
* @property {boolean} [smoothStopVolume=false] - If true, volume fades smoothly to zero when stopping.
|
|
24
31
|
*/
|
|
25
|
-
/**
|
|
26
|
-
* Interface definition for a Media Provider Adapter.
|
|
27
|
-
* All specific API wrappers must extend and implement this class.
|
|
28
|
-
* @abstract
|
|
29
|
-
*/
|
|
30
|
-
class BaseMediaAdapter {
|
|
31
|
-
constructor() {
|
|
32
|
-
if (new.target === BaseMediaAdapter) {
|
|
33
|
-
throw new Error('BaseMediaAdapter is an abstract class and cannot be instantiated directly.');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Determines if this adapter can play the provided content.
|
|
38
|
-
* @param {MediaContent} content - The media content to evaluate.
|
|
39
|
-
* @returns {boolean} True if the adapter can handle the content, false otherwise.
|
|
40
|
-
*/
|
|
41
|
-
canHandle(content) {
|
|
42
|
-
throw new Error('Method "canHandle" must be implemented by the subclass.');
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Starts or resumes playback of the provided content.
|
|
46
|
-
* @param {MediaContent} content - The media content to play.
|
|
47
|
-
* @returns {Promise<void>}
|
|
48
|
-
*/
|
|
49
|
-
async play(content) {
|
|
50
|
-
throw new Error('Method "play" must be implemented by the subclass.');
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Pauses the current playback.
|
|
54
|
-
* @returns {Promise<void>}
|
|
55
|
-
*/
|
|
56
|
-
async pause() {
|
|
57
|
-
throw new Error('Method "pause" must be implemented by the subclass.');
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Stops the playback completely and resets the internal platform state.
|
|
61
|
-
* @returns {Promise<void>}
|
|
62
|
-
*/
|
|
63
|
-
async stop() {
|
|
64
|
-
throw new Error('Method "stop" must be implemented by the subclass.');
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Seeks to a specific time in the media timeline.
|
|
68
|
-
* @param {number} timeMs - The target time in milliseconds.
|
|
69
|
-
* @returns {Promise<void>}
|
|
70
|
-
*/
|
|
71
|
-
async seek(timeMs) {
|
|
72
|
-
throw new Error('Method "seek" must be implemented by the subclass.');
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Retrieves the current playback time from the underlying API.
|
|
76
|
-
* @returns {number} The current time in milliseconds.
|
|
77
|
-
*/
|
|
78
|
-
getCurrentTime() {
|
|
79
|
-
throw new Error('Method "getCurrentTime" must be implemented by the subclass.');
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Sets the playback volume for the underlying API.
|
|
83
|
-
* @param {number} volume - The volume level from 0.0 to 1.0.
|
|
84
|
-
* @returns {void}
|
|
85
|
-
*/
|
|
86
|
-
setVolume(volume) {
|
|
87
|
-
throw new Error('Method "setVolume" must be implemented by the subclass.');
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
32
|
/**
|
|
91
33
|
* Represents a search match containing the media object and its exact playlist index.
|
|
92
34
|
* @typedef {Object} SearchResult
|
|
@@ -117,7 +59,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
117
59
|
*/
|
|
118
60
|
static set unknownArtist(value) {
|
|
119
61
|
if (typeof value !== 'string' && typeof value !== 'function')
|
|
120
|
-
throw new TypeError('unknownArtist must
|
|
62
|
+
throw new TypeError('unknownArtist must be a string or a function.');
|
|
121
63
|
TinyMediaPlayer.#unknownArtist = value;
|
|
122
64
|
}
|
|
123
65
|
/**
|
|
@@ -142,8 +84,10 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
142
84
|
get destroyed() {
|
|
143
85
|
return this.#destroyed;
|
|
144
86
|
}
|
|
145
|
-
/** @type {
|
|
146
|
-
#adapters = new
|
|
87
|
+
/** @type {Set<BaseMediaAdapter>} */
|
|
88
|
+
#adapters = new Set();
|
|
89
|
+
/** @type {WeakMap<BaseMediaAdapter, Map<string, (...args: any[]) => any>>} */
|
|
90
|
+
#adapterHandlers = new WeakMap();
|
|
147
91
|
/** @type {MediaContent[]} */
|
|
148
92
|
#playlist = [];
|
|
149
93
|
/** @type {number} */
|
|
@@ -160,17 +104,168 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
160
104
|
#persistVolume;
|
|
161
105
|
/** @type {string} */
|
|
162
106
|
#volumeStorageKey;
|
|
107
|
+
/** @type {boolean} */
|
|
108
|
+
#repeatCurrentOnPrev;
|
|
109
|
+
/** @type {boolean} */
|
|
110
|
+
#prevClickedToRepeat = false;
|
|
111
|
+
/** @type {boolean} */
|
|
112
|
+
#smoothPlayPauseVolume;
|
|
113
|
+
/** @type {boolean} */
|
|
114
|
+
#smoothStopVolume;
|
|
115
|
+
/** @type {AbortController | null} */
|
|
116
|
+
#fadeController = null;
|
|
117
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
118
|
+
#prevClickTimer = null;
|
|
119
|
+
/** @type {number} */
|
|
120
|
+
#fadeVolumeSpeed = 300;
|
|
121
|
+
/** @returns {number} The duration (ms) of the volume fade speed. */
|
|
122
|
+
get fadeVolumeSpeed() {
|
|
123
|
+
checkDestroy(this.#destroyed);
|
|
124
|
+
return this.#fadeVolumeSpeed;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* @param {number} value - The new duration in milliseconds.
|
|
128
|
+
* @throws {TypeError} If the value is not a number.
|
|
129
|
+
* @throws {RangeError} If the value is negative.
|
|
130
|
+
*/
|
|
131
|
+
set fadeVolumeSpeed(value) {
|
|
132
|
+
checkDestroy(this.#destroyed);
|
|
133
|
+
if (typeof value !== 'number')
|
|
134
|
+
throw new TypeError('fadeVolumeSpeed must be a number.');
|
|
135
|
+
if (value < 0)
|
|
136
|
+
throw new RangeError('fadeVolumeSpeed cannot be negative.');
|
|
137
|
+
this.#fadeVolumeSpeed = value;
|
|
138
|
+
this.emit('fadeVolumeSpeedChange', this.#fadeVolumeSpeed);
|
|
139
|
+
}
|
|
140
|
+
/** @type {number} */
|
|
141
|
+
#prevClickTimeoutDuration = 2000;
|
|
142
|
+
/** @returns {number} The duration (ms) before the prev click state resets. */
|
|
143
|
+
get prevClickTimeoutDuration() {
|
|
144
|
+
checkDestroy(this.#destroyed);
|
|
145
|
+
return this.#prevClickTimeoutDuration;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* @param {number} value - The new duration in milliseconds.
|
|
149
|
+
* @throws {TypeError} If the value is not a number.
|
|
150
|
+
* @throws {RangeError} If the value is negative.
|
|
151
|
+
*/
|
|
152
|
+
set prevClickTimeoutDuration(value) {
|
|
153
|
+
checkDestroy(this.#destroyed);
|
|
154
|
+
if (typeof value !== 'number')
|
|
155
|
+
throw new TypeError('prevClickTimeoutDuration must be a number.');
|
|
156
|
+
if (value < 0)
|
|
157
|
+
throw new RangeError('prevClickTimeoutDuration cannot be negative.');
|
|
158
|
+
this.#prevClickTimeoutDuration = value;
|
|
159
|
+
this.emit('prevClickTimeoutDurationChange', this.#prevClickTimeoutDuration);
|
|
160
|
+
}
|
|
161
|
+
/** @returns {boolean} True if clicking 'previous' repeats the current track on the first click. */
|
|
162
|
+
get repeatCurrentOnPrev() {
|
|
163
|
+
checkDestroy(this.#destroyed);
|
|
164
|
+
return this.#repeatCurrentOnPrev;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* @param {boolean} value - The repeat mode state.
|
|
168
|
+
* @throws {TypeError} If the value is not a boolean.
|
|
169
|
+
*/
|
|
170
|
+
set repeatCurrentOnPrev(value) {
|
|
171
|
+
checkDestroy(this.#destroyed);
|
|
172
|
+
if (typeof value !== 'boolean') {
|
|
173
|
+
throw new TypeError('repeatCurrentOnPrev must be a boolean.');
|
|
174
|
+
}
|
|
175
|
+
this.#repeatCurrentOnPrev = value;
|
|
176
|
+
this.emit('repeatCurrentOnPrevChange', this.#repeatCurrentOnPrev);
|
|
177
|
+
}
|
|
178
|
+
/** @returns {boolean} True if volume fades smoothly during play/pause transitions. */
|
|
179
|
+
get smoothPlayPauseVolume() {
|
|
180
|
+
checkDestroy(this.#destroyed);
|
|
181
|
+
return this.#smoothPlayPauseVolume;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* @param {boolean} value - The smooth play/pause volume mode.
|
|
185
|
+
* @throws {TypeError} If the value is not a boolean.
|
|
186
|
+
*/
|
|
187
|
+
set smoothPlayPauseVolume(value) {
|
|
188
|
+
checkDestroy(this.#destroyed);
|
|
189
|
+
if (typeof value !== 'boolean') {
|
|
190
|
+
throw new TypeError('smoothPlayPauseVolume must be a boolean.');
|
|
191
|
+
}
|
|
192
|
+
this.#smoothPlayPauseVolume = value;
|
|
193
|
+
this.emit('smoothPlayPauseVolumeChange', this.#smoothPlayPauseVolume);
|
|
194
|
+
}
|
|
195
|
+
/** @returns {boolean} True if volume fades smoothly to zero when stopping. */
|
|
196
|
+
get smoothStopVolume() {
|
|
197
|
+
checkDestroy(this.#destroyed);
|
|
198
|
+
return this.#smoothStopVolume;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* @param {boolean} value - The smooth stop volume mode.
|
|
202
|
+
* @throws {TypeError} If the value is not a boolean.
|
|
203
|
+
*/
|
|
204
|
+
set smoothStopVolume(value) {
|
|
205
|
+
checkDestroy(this.#destroyed);
|
|
206
|
+
if (typeof value !== 'boolean') {
|
|
207
|
+
throw new TypeError('smoothStopVolume must be a boolean.');
|
|
208
|
+
}
|
|
209
|
+
this.#smoothStopVolume = value;
|
|
210
|
+
this.emit('smoothStopVolumeChange', this.#smoothStopVolume);
|
|
211
|
+
}
|
|
163
212
|
/**
|
|
164
213
|
* @param {TinyMediaPlayerOptions} [options={}] - Configuration parameters for the player.
|
|
214
|
+
* @throws {TypeError} If options is not a valid object.
|
|
215
|
+
* @throws {TypeError} If option values do not match their expected types.
|
|
216
|
+
* @throws {RangeError} If numeric options are out of valid ranges.
|
|
165
217
|
*/
|
|
166
218
|
constructor(options = {}) {
|
|
167
219
|
super();
|
|
220
|
+
if (!isValidObj(options)) {
|
|
221
|
+
throw new TypeError('Options must be a non-null object.');
|
|
222
|
+
}
|
|
168
223
|
// Volume configuration
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
224
|
+
if (options.persistVolume !== undefined) {
|
|
225
|
+
if (typeof options.persistVolume !== 'boolean') {
|
|
226
|
+
throw new TypeError('persistVolume must be a boolean.');
|
|
227
|
+
}
|
|
228
|
+
this.#persistVolume = options.persistVolume;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.#persistVolume = false;
|
|
232
|
+
}
|
|
233
|
+
if (options.volumeStorageKey !== undefined) {
|
|
234
|
+
if (typeof options.volumeStorageKey !== 'string')
|
|
235
|
+
throw new TypeError('volumeStorageKey must be a string.');
|
|
236
|
+
this.#volumeStorageKey = options.volumeStorageKey;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
this.#volumeStorageKey = 'tiny_media_player_volume';
|
|
240
|
+
}
|
|
241
|
+
// UX Options
|
|
242
|
+
if (options.repeatCurrentOnPrev !== undefined) {
|
|
243
|
+
if (typeof options.repeatCurrentOnPrev !== 'boolean') {
|
|
244
|
+
throw new TypeError('repeatCurrentOnPrev must be a boolean.');
|
|
245
|
+
}
|
|
246
|
+
this.#repeatCurrentOnPrev = options.repeatCurrentOnPrev;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
this.#repeatCurrentOnPrev = false;
|
|
250
|
+
}
|
|
251
|
+
if (options.smoothPlayPauseVolume !== undefined) {
|
|
252
|
+
if (typeof options.smoothPlayPauseVolume !== 'boolean') {
|
|
253
|
+
throw new TypeError('smoothPlayPauseVolume must be a boolean.');
|
|
254
|
+
}
|
|
255
|
+
this.#smoothPlayPauseVolume = options.smoothPlayPauseVolume;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
this.#smoothPlayPauseVolume = false;
|
|
259
|
+
}
|
|
260
|
+
if (options.smoothStopVolume !== undefined) {
|
|
261
|
+
if (typeof options.smoothStopVolume !== 'boolean') {
|
|
262
|
+
throw new TypeError('smoothStopVolume must be a boolean.');
|
|
263
|
+
}
|
|
264
|
+
this.#smoothStopVolume = options.smoothStopVolume;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
this.#smoothStopVolume = false;
|
|
268
|
+
}
|
|
174
269
|
if (this.#persistVolume)
|
|
175
270
|
this.#loadVolumeFromStorage();
|
|
176
271
|
}
|
|
@@ -248,6 +343,44 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
248
343
|
// Fallback in case of floating point inaccuracies
|
|
249
344
|
return this.#currentIndex === this.#playlist.length - 1 ? 0 : this.#playlist.length - 1;
|
|
250
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* Handles smooth volume transitions for the active adapter.
|
|
348
|
+
* @param {number} targetVolume - The volume to transition to.
|
|
349
|
+
* @returns {Promise<void>}
|
|
350
|
+
*/
|
|
351
|
+
async #fadeAdapterVolume(targetVolume) {
|
|
352
|
+
if (this.#fadeController) {
|
|
353
|
+
this.#fadeController.abort();
|
|
354
|
+
}
|
|
355
|
+
this.#fadeController = new AbortController();
|
|
356
|
+
const { signal } = this.#fadeController;
|
|
357
|
+
const adapter = this.#getActiveAdapter();
|
|
358
|
+
if (!adapter)
|
|
359
|
+
return;
|
|
360
|
+
const startVolume = adapter.getVolume();
|
|
361
|
+
const startTime = performance.now();
|
|
362
|
+
const duration = this.#fadeVolumeSpeed;
|
|
363
|
+
return new Promise((resolve) => {
|
|
364
|
+
/** @param {number} currentTime */
|
|
365
|
+
const step = (currentTime) => {
|
|
366
|
+
if (signal.aborted)
|
|
367
|
+
return resolve();
|
|
368
|
+
const elapsed = currentTime - startTime;
|
|
369
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
370
|
+
const calculatedVolume = startVolume + (targetVolume - startVolume) * progress;
|
|
371
|
+
const clampedVolume = Math.max(0, Math.min(1, calculatedVolume));
|
|
372
|
+
adapter.setVolume(clampedVolume);
|
|
373
|
+
if (progress < 1) {
|
|
374
|
+
requestAnimationFrame(step);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
adapter.setVolume(targetVolume);
|
|
378
|
+
resolve();
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
requestAnimationFrame(step);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
251
384
|
// ==========================================
|
|
252
385
|
// GETTERS & STRICT SETTERS
|
|
253
386
|
// ==========================================
|
|
@@ -408,6 +541,9 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
408
541
|
throw new TypeError('Volume must be a number.');
|
|
409
542
|
if (value < 0 || value > 1)
|
|
410
543
|
throw new RangeError('Volume must be tightly constrained between 0.0 and 1.0.');
|
|
544
|
+
// Abort any ongoing fade to ensure manual control is immediate
|
|
545
|
+
if (this.#fadeController)
|
|
546
|
+
this.#fadeController.abort();
|
|
411
547
|
this.#volume = value;
|
|
412
548
|
this.#saveVolumeToStorage();
|
|
413
549
|
this.emit('volumeChange', this.#volume);
|
|
@@ -475,21 +611,135 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
475
611
|
// ==========================================
|
|
476
612
|
// ADAPTER MANAGEMENT
|
|
477
613
|
// ==========================================
|
|
614
|
+
get adapters() {
|
|
615
|
+
return Array.from(this.#adapters);
|
|
616
|
+
}
|
|
617
|
+
get adaptersSize() {
|
|
618
|
+
return this.#adapters.size;
|
|
619
|
+
}
|
|
620
|
+
/** @type {string[]} */
|
|
621
|
+
static #defaultAdapterEventNames = ['timeupdate', 'ended'];
|
|
622
|
+
static get defaultAdapterEventNames() {
|
|
623
|
+
return structuredClone(TinyMediaPlayer.#defaultAdapterEventNames);
|
|
624
|
+
}
|
|
625
|
+
/** @param {string[]} value */
|
|
626
|
+
static set defaultAdapterEventNames(value) {
|
|
627
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === 'string'))
|
|
628
|
+
throw new TypeError('TinyMediaPlayer.defaultAdapterEventNames must be an array of strings.');
|
|
629
|
+
TinyMediaPlayer.#defaultAdapterEventNames = [...value];
|
|
630
|
+
}
|
|
631
|
+
#adapterEventNames = new Set(TinyMediaPlayer.defaultAdapterEventNames);
|
|
632
|
+
/**
|
|
633
|
+
* Removes the timeupdate on adapter emitter.
|
|
634
|
+
* @param {BaseMediaAdapter} adapter
|
|
635
|
+
*/
|
|
636
|
+
#removeHandler(adapter) {
|
|
637
|
+
const data = this.#adapterHandlers.get(adapter);
|
|
638
|
+
if (!data)
|
|
639
|
+
return;
|
|
640
|
+
this.#adapterEventNames.forEach((eventName) => {
|
|
641
|
+
const handler = data.get(eventName);
|
|
642
|
+
if (handler)
|
|
643
|
+
adapter.off(eventName, handler);
|
|
644
|
+
});
|
|
645
|
+
data.clear();
|
|
646
|
+
this.#adapterHandlers.delete(adapter);
|
|
647
|
+
}
|
|
478
648
|
/**
|
|
479
649
|
* Registers a new media API adapter.
|
|
480
|
-
* @param {string} id - Unique identifier for the platform (e.g., 'youtube', 'spotify').
|
|
481
650
|
* @param {BaseMediaAdapter} adapter - An instance extending BaseMediaAdapter.
|
|
482
|
-
* @throws {TypeError} If
|
|
651
|
+
* @throws {TypeError} If the adapter is invalid.
|
|
483
652
|
*/
|
|
484
|
-
registerAdapter(
|
|
653
|
+
registerAdapter(adapter) {
|
|
485
654
|
checkDestroy(this.#destroyed);
|
|
486
|
-
if (
|
|
487
|
-
throw new TypeError('Adapter
|
|
655
|
+
if (!(adapter instanceof BaseMediaAdapter)) {
|
|
656
|
+
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
488
657
|
}
|
|
658
|
+
const events = new Map();
|
|
659
|
+
this.#adapterHandlers.set(adapter, events);
|
|
660
|
+
// Use the controlled helper to ensure each event is registered
|
|
661
|
+
// and correctly mapped in #adapterHandlers from the start.
|
|
662
|
+
this.#adapterEventNames.forEach((eventName) => {
|
|
663
|
+
this.#attachEventToAdapter(adapter, eventName);
|
|
664
|
+
});
|
|
665
|
+
this.#adapters.add(adapter);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Removes a media API adapter.
|
|
669
|
+
* @param {BaseMediaAdapter} adapter - An instance extending BaseMediaAdapter.
|
|
670
|
+
* @throws {TypeError} If adapter is invalid.
|
|
671
|
+
*/
|
|
672
|
+
removeAdapter(adapter) {
|
|
673
|
+
checkDestroy(this.#destroyed);
|
|
489
674
|
if (!(adapter instanceof BaseMediaAdapter)) {
|
|
490
675
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
491
676
|
}
|
|
492
|
-
this.#
|
|
677
|
+
this.#removeHandler(adapter);
|
|
678
|
+
return this.#adapters.delete(adapter);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Destroys and removes a media API adapter.
|
|
682
|
+
* @param {BaseMediaAdapter} adapter - An instance extending BaseMediaAdapter.
|
|
683
|
+
* @throws {TypeError} If adapter is invalid.
|
|
684
|
+
*/
|
|
685
|
+
destroyAdapter(adapter) {
|
|
686
|
+
checkDestroy(this.#destroyed);
|
|
687
|
+
if (!(adapter instanceof BaseMediaAdapter)) {
|
|
688
|
+
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
689
|
+
}
|
|
690
|
+
this.#removeHandler(adapter);
|
|
691
|
+
const result = this.#adapters.delete(adapter);
|
|
692
|
+
if (result)
|
|
693
|
+
adapter.destroy();
|
|
694
|
+
return result;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* If exists a media API adapter.
|
|
698
|
+
* @param {BaseMediaAdapter} adapter - An instance extending BaseMediaAdapter.
|
|
699
|
+
* @throws {TypeError} If adapter is invalid.
|
|
700
|
+
*/
|
|
701
|
+
hasAdapter(adapter) {
|
|
702
|
+
checkDestroy(this.#destroyed);
|
|
703
|
+
if (!(adapter instanceof BaseMediaAdapter)) {
|
|
704
|
+
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
705
|
+
}
|
|
706
|
+
return this.#adapters.has(adapter);
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Destroys and removes all registered media adapters.
|
|
710
|
+
*/
|
|
711
|
+
destroyAllAdapters() {
|
|
712
|
+
checkDestroy(this.#destroyed);
|
|
713
|
+
this.#adapters.forEach((adapter) => {
|
|
714
|
+
this.#removeHandler(adapter);
|
|
715
|
+
adapter.destroy();
|
|
716
|
+
});
|
|
717
|
+
this.#adapters.clear();
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Clears all registered media adapters.
|
|
721
|
+
*/
|
|
722
|
+
clearAdapters() {
|
|
723
|
+
checkDestroy(this.#destroyed);
|
|
724
|
+
this.#adapters.forEach((adapter) => this.#removeHandler(adapter));
|
|
725
|
+
this.#adapters.clear();
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Helper to find the correct API wrapper for the content.
|
|
729
|
+
* @param {MediaContent} content - The content to check.
|
|
730
|
+
* @returns {BaseMediaAdapter | null} The compatible adapter, or null if empty.
|
|
731
|
+
* @throws {TypeError} If the content is invalid.
|
|
732
|
+
*/
|
|
733
|
+
getMediaAdapter(content) {
|
|
734
|
+
if (!isValidObj(content)) {
|
|
735
|
+
throw new TypeError('Content must be a valid object.');
|
|
736
|
+
}
|
|
737
|
+
for (const adapter of this.#adapters.values()) {
|
|
738
|
+
if (adapter.canHandle(content)) {
|
|
739
|
+
return adapter;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
493
743
|
}
|
|
494
744
|
/**
|
|
495
745
|
* Internal helper to find the correct API wrapper for the current content.
|
|
@@ -500,12 +750,106 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
500
750
|
if (this.#currentIndex === -1 || this.#playlist.length === 0)
|
|
501
751
|
return null;
|
|
502
752
|
const currentContent = this.#playlist[this.#currentIndex];
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
753
|
+
const adapter = this.getMediaAdapter(currentContent);
|
|
754
|
+
if (adapter)
|
|
755
|
+
return adapter;
|
|
756
|
+
throw new Error(`No compatible adapter found for content ID: ${currentContent.id}.`);
|
|
757
|
+
}
|
|
758
|
+
// ==========================================
|
|
759
|
+
// EVENT MANAGEMENT (CONTROLLED)
|
|
760
|
+
// ==========================================
|
|
761
|
+
/**
|
|
762
|
+
* Adds a new event to be listened to on the adapters.
|
|
763
|
+
* @param {string} eventName - The name of the event to add.
|
|
764
|
+
* @throws {TypeError} If the event name is not a string.
|
|
765
|
+
*/
|
|
766
|
+
addAdapterEvent(eventName) {
|
|
767
|
+
checkDestroy(this.#destroyed);
|
|
768
|
+
if (typeof eventName !== 'string') {
|
|
769
|
+
throw new TypeError('Event name must be a string.');
|
|
770
|
+
}
|
|
771
|
+
// If the event is already in the list, do nothing to avoid duplicates
|
|
772
|
+
if (this.#adapterEventNames.has(eventName))
|
|
773
|
+
return;
|
|
774
|
+
this.#adapterEventNames.add(eventName);
|
|
775
|
+
// Synchronize: Add the listener to all already registered adapters
|
|
776
|
+
this.#adapters.forEach((adapter) => {
|
|
777
|
+
this.#attachEventToAdapter(adapter, eventName);
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Removes an event from the list of listened events.
|
|
782
|
+
* @param {string} eventName - The name of the event to remove.
|
|
783
|
+
* @throws {TypeError} If the event name is not a string.
|
|
784
|
+
*/
|
|
785
|
+
removeAdapterEvent(eventName) {
|
|
786
|
+
checkDestroy(this.#destroyed);
|
|
787
|
+
if (typeof eventName !== 'string') {
|
|
788
|
+
throw new TypeError('Event name must be a string.');
|
|
789
|
+
}
|
|
790
|
+
if (!this.#adapterEventNames.has(eventName))
|
|
791
|
+
return;
|
|
792
|
+
this.#adapterEventNames.delete(eventName);
|
|
793
|
+
// Synchronize: Remove the listener from all registered adapters
|
|
794
|
+
this.#adapters.forEach((adapter) => {
|
|
795
|
+
this.#detachEventFromAdapter(adapter, eventName);
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Resets the event list to the factory default state: ['timeupdate', 'ended'].
|
|
800
|
+
* @throws {TypeError} If the event name is not a string.
|
|
801
|
+
*/
|
|
802
|
+
resetAdapterEvents() {
|
|
803
|
+
checkDestroy(this.#destroyed);
|
|
804
|
+
const defaultEvents = TinyMediaPlayer.defaultAdapterEventNames;
|
|
805
|
+
// Convert to Array to avoid issues when deleting items during iteration
|
|
806
|
+
const currentEvents = Array.from(this.#adapterEventNames);
|
|
807
|
+
// 1. Remove events that are not part of the factory default
|
|
808
|
+
for (const eventName of currentEvents) {
|
|
809
|
+
if (!defaultEvents.includes(eventName)) {
|
|
810
|
+
this.removeAdapterEvent(eventName);
|
|
506
811
|
}
|
|
507
812
|
}
|
|
508
|
-
|
|
813
|
+
// 2. Ensure default events exist (in case they were previously removed)
|
|
814
|
+
for (const defaultEvent of defaultEvents) {
|
|
815
|
+
if (!this.#adapterEventNames.has(defaultEvent)) {
|
|
816
|
+
this.addAdapterEvent(defaultEvent);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Private helper to attach an event to a specific adapter and save the handler.
|
|
822
|
+
* @param {BaseMediaAdapter} adapter - The destination adapter.
|
|
823
|
+
* @param {string} eventName - The event name.
|
|
824
|
+
*/
|
|
825
|
+
#attachEventToAdapter(adapter, eventName) {
|
|
826
|
+
const handlersMap = this.#adapterHandlers.get(adapter);
|
|
827
|
+
if (!handlersMap)
|
|
828
|
+
return;
|
|
829
|
+
// Create an encapsulated handler to maintain security (checkDestroy)
|
|
830
|
+
/** @type {(...args: any[]) => any} */
|
|
831
|
+
const handler = (...args) => {
|
|
832
|
+
if (!this.#destroyed) {
|
|
833
|
+
this.emit(eventName, ...args);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
adapter.on(eventName, handler);
|
|
837
|
+
handlersMap.set(eventName, handler); // Mandatory synchronization with the WeakMap
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Private helper to remove an event from a specific adapter using the saved handler.
|
|
841
|
+
* @param {BaseMediaAdapter} adapter - The destination adapter.
|
|
842
|
+
* @param {string} eventName - The event name.
|
|
843
|
+
*/
|
|
844
|
+
#detachEventFromAdapter(adapter, eventName) {
|
|
845
|
+
const handlersMap = this.#adapterHandlers.get(adapter);
|
|
846
|
+
if (!handlersMap)
|
|
847
|
+
return;
|
|
848
|
+
const handler = handlersMap.get(eventName);
|
|
849
|
+
if (handler) {
|
|
850
|
+
adapter.off(eventName, handler);
|
|
851
|
+
handlersMap.delete(eventName); // Mandatory synchronization with the WeakMap
|
|
852
|
+
}
|
|
509
853
|
}
|
|
510
854
|
// ==========================================
|
|
511
855
|
// PLAYLIST MANAGEMENT
|
|
@@ -518,8 +862,9 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
518
862
|
*/
|
|
519
863
|
addTrack(content) {
|
|
520
864
|
checkDestroy(this.#destroyed);
|
|
521
|
-
if (!content || typeof content
|
|
865
|
+
if (!isValidObj(content) || typeof content.url !== 'string') {
|
|
522
866
|
throw new TypeError('Track content must be a valid MediaContent object containing a URL.');
|
|
867
|
+
}
|
|
523
868
|
/** @type {MediaContent} */
|
|
524
869
|
const newContent = { ...getMediaContentBase(), ...getMediaContentMetadata(), ...content };
|
|
525
870
|
valMediaContentMetadata(newContent);
|
|
@@ -534,7 +879,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
534
879
|
/**
|
|
535
880
|
* Checks if a track exists at the specified index.
|
|
536
881
|
* @param {number} index - The target index.
|
|
537
|
-
* @returns {boolean} True if the track exists
|
|
882
|
+
* @returns {boolean} True if the track exists at the specified index.
|
|
538
883
|
* @throws {TypeError} If the index is not a number.
|
|
539
884
|
*/
|
|
540
885
|
existsTrack(index) {
|
|
@@ -549,7 +894,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
549
894
|
* @param {number} index - The target index.
|
|
550
895
|
* @returns {MediaContent} The media content object.
|
|
551
896
|
* @throws {TypeError} If the index is not a number.
|
|
552
|
-
* @throws {RangeError} If the index is out of bounds.
|
|
897
|
+
* @throws {RangeError} If the index is out of the playlist bounds.
|
|
553
898
|
*/
|
|
554
899
|
getTrack(index) {
|
|
555
900
|
checkDestroy(this.#destroyed);
|
|
@@ -585,7 +930,7 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
585
930
|
}
|
|
586
931
|
else if (this.#currentIndex >= this.#playlist.length) {
|
|
587
932
|
// If we removed the last item and others exist, reset index safely
|
|
588
|
-
this.#currentIndex = 0;
|
|
933
|
+
this.#currentIndex = 0; // Reset to start if out of bounds
|
|
589
934
|
}
|
|
590
935
|
this.emit('trackChange', this.#currentIndex);
|
|
591
936
|
}
|
|
@@ -655,7 +1000,12 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
655
1000
|
if (!adapter)
|
|
656
1001
|
return;
|
|
657
1002
|
// Ensure the adapter aligns with the global volume before playing
|
|
658
|
-
|
|
1003
|
+
if (this.#smoothPlayPauseVolume) {
|
|
1004
|
+
await this.#fadeAdapterVolume(this.#volume);
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
adapter.setVolume(this.#volume);
|
|
1008
|
+
}
|
|
659
1009
|
await adapter.play(this.#playlist[this.#currentIndex]);
|
|
660
1010
|
this.#isPlaying = true;
|
|
661
1011
|
this.emit('play', this.#currentIndex);
|
|
@@ -669,6 +1019,9 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
669
1019
|
const adapter = this.#getActiveAdapter();
|
|
670
1020
|
if (!adapter)
|
|
671
1021
|
return;
|
|
1022
|
+
if (this.#smoothPlayPauseVolume) {
|
|
1023
|
+
await this.#fadeAdapterVolume(0);
|
|
1024
|
+
}
|
|
672
1025
|
await adapter.pause();
|
|
673
1026
|
this.#isPlaying = false;
|
|
674
1027
|
this.emit('pause', this.#currentIndex);
|
|
@@ -682,6 +1035,9 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
682
1035
|
const adapter = this.#getActiveAdapter();
|
|
683
1036
|
if (!adapter)
|
|
684
1037
|
return;
|
|
1038
|
+
if (this.#smoothStopVolume) {
|
|
1039
|
+
await this.#fadeAdapterVolume(0);
|
|
1040
|
+
}
|
|
685
1041
|
await adapter.stop();
|
|
686
1042
|
this.#isPlaying = false;
|
|
687
1043
|
this.emit('stop', this.#currentIndex);
|
|
@@ -727,6 +1083,26 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
727
1083
|
checkDestroy(this.#destroyed);
|
|
728
1084
|
if (this.#playlist.length === 0)
|
|
729
1085
|
return;
|
|
1086
|
+
// UX: Repeat current track if "repeat on prev" is enabled and it's the first click
|
|
1087
|
+
if (this.#repeatCurrentOnPrev && !this.#prevClickedToRepeat) {
|
|
1088
|
+
this.#prevClickedToRepeat = true;
|
|
1089
|
+
// Reset the "repeat" state after 2 seconds of inactivity to prevent glitches
|
|
1090
|
+
if (this.#prevClickTimer)
|
|
1091
|
+
clearTimeout(this.#prevClickTimer);
|
|
1092
|
+
this.#prevClickTimer = setTimeout(() => {
|
|
1093
|
+
this.#prevClickedToRepeat = false;
|
|
1094
|
+
this.#prevClickTimer = null;
|
|
1095
|
+
}, 2000);
|
|
1096
|
+
await this.stop();
|
|
1097
|
+
await this.play();
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
// If we reached here, it's either the second click or repeat is disabled
|
|
1101
|
+
if (this.#prevClickTimer) {
|
|
1102
|
+
clearTimeout(this.#prevClickTimer);
|
|
1103
|
+
this.#prevClickTimer = null;
|
|
1104
|
+
}
|
|
1105
|
+
this.#prevClickedToRepeat = false;
|
|
730
1106
|
await this.stop();
|
|
731
1107
|
if (this.#isRandom) {
|
|
732
1108
|
this.#currentIndex = this._getWeightedRandomIndex();
|
|
@@ -750,9 +1126,9 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
750
1126
|
/**
|
|
751
1127
|
* Jumps to a specific absolute time in the timeline.
|
|
752
1128
|
* @param {number} timeMs - The target time in milliseconds.
|
|
1129
|
+
* @returns {Promise<void>}
|
|
753
1130
|
* @throws {TypeError} If timeMs is not a number.
|
|
754
1131
|
* @throws {RangeError} If timeMs is negative.
|
|
755
|
-
* @returns {Promise<void>}
|
|
756
1132
|
*/
|
|
757
1133
|
async seek(timeMs) {
|
|
758
1134
|
checkDestroy(this.#destroyed);
|
|
@@ -789,6 +1165,105 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
789
1165
|
targetTime = 0;
|
|
790
1166
|
await this.seek(targetTime);
|
|
791
1167
|
}
|
|
1168
|
+
// ==========================================
|
|
1169
|
+
// TIME UTILITIES (DELEGATED TO ADAPTERS)
|
|
1170
|
+
// ==========================================
|
|
1171
|
+
/**
|
|
1172
|
+
* Gets the current playback time.
|
|
1173
|
+
* @returns {number} The current time in milliseconds.
|
|
1174
|
+
*/
|
|
1175
|
+
getCurrentTime() {
|
|
1176
|
+
checkDestroy(this.#destroyed);
|
|
1177
|
+
try {
|
|
1178
|
+
const adapter = this.#getActiveAdapter();
|
|
1179
|
+
return adapter ? adapter.getCurrentTime() : 0;
|
|
1180
|
+
}
|
|
1181
|
+
catch {
|
|
1182
|
+
return 0;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Gets the total duration of the current track.
|
|
1187
|
+
* @returns {number} The total duration in milliseconds.
|
|
1188
|
+
*/
|
|
1189
|
+
getTotalDuration() {
|
|
1190
|
+
checkDestroy(this.#destroyed);
|
|
1191
|
+
try {
|
|
1192
|
+
const adapter = this.#getActiveAdapter();
|
|
1193
|
+
return adapter ? adapter.getTotalDuration() : 0;
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
return 0;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Gets the remaining time until the current track ends.
|
|
1201
|
+
* @returns {number} The remaining time in milliseconds.
|
|
1202
|
+
*/
|
|
1203
|
+
getRemainingTime() {
|
|
1204
|
+
checkDestroy(this.#destroyed);
|
|
1205
|
+
try {
|
|
1206
|
+
const adapter = this.#getActiveAdapter();
|
|
1207
|
+
return adapter ? adapter.getRemainingTime() : 0;
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
return 0;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Gets the percentage of the current track that has been played.
|
|
1215
|
+
* @returns {number} A value between 0 and 100.
|
|
1216
|
+
*/
|
|
1217
|
+
getPlaybackPercentage() {
|
|
1218
|
+
checkDestroy(this.#destroyed);
|
|
1219
|
+
try {
|
|
1220
|
+
const adapter = this.#getActiveAdapter();
|
|
1221
|
+
return adapter ? adapter.getPlaybackPercentage() : 0;
|
|
1222
|
+
}
|
|
1223
|
+
catch {
|
|
1224
|
+
return 0;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Retrieves the metadata of the currently loaded content, returning a structured
|
|
1229
|
+
* object containing details such as the content ID, title, author, and duration.
|
|
1230
|
+
* @returns {Promise<ContentData>}
|
|
1231
|
+
*/
|
|
1232
|
+
async getContentData() {
|
|
1233
|
+
checkDestroy(this.#destroyed);
|
|
1234
|
+
const defaultData = {
|
|
1235
|
+
id: '',
|
|
1236
|
+
createdAt: '',
|
|
1237
|
+
artistId: '',
|
|
1238
|
+
artistName: '',
|
|
1239
|
+
description: '',
|
|
1240
|
+
title: '',
|
|
1241
|
+
duration: 0,
|
|
1242
|
+
avatar: '',
|
|
1243
|
+
url: '',
|
|
1244
|
+
};
|
|
1245
|
+
try {
|
|
1246
|
+
const adapter = this.#getActiveAdapter();
|
|
1247
|
+
return adapter ? adapter.getContentData() : defaultData;
|
|
1248
|
+
}
|
|
1249
|
+
catch {
|
|
1250
|
+
return defaultData;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Asynchronously waits until the content media adapter is fully initialized and ready.
|
|
1255
|
+
* @returns {Promise<void>} A promise that resolves once the adapter is ready.
|
|
1256
|
+
*/
|
|
1257
|
+
async waitIsReady() {
|
|
1258
|
+
checkDestroy(this.#destroyed);
|
|
1259
|
+
const adapter = this.#getActiveAdapter();
|
|
1260
|
+
if (!adapter)
|
|
1261
|
+
return;
|
|
1262
|
+
return adapter.waitIsReady();
|
|
1263
|
+
}
|
|
1264
|
+
// ==========================================
|
|
1265
|
+
// DESTROYER
|
|
1266
|
+
// ==========================================
|
|
792
1267
|
/**
|
|
793
1268
|
* Safely destroys the TinyMediaPlayer instance.
|
|
794
1269
|
* This method stops active playback, clears the internal playlist,
|
|
@@ -809,15 +1284,20 @@ class TinyMediaPlayer extends EventEmitter {
|
|
|
809
1284
|
catch (error) {
|
|
810
1285
|
console.warn('[TinyMediaPlayer] Non-fatal error during playback termination:', error);
|
|
811
1286
|
}
|
|
1287
|
+
if (this.#prevClickTimer)
|
|
1288
|
+
clearTimeout(this.#prevClickTimer);
|
|
812
1289
|
// 2. Clear internal state variables
|
|
813
1290
|
this.#playlist = [];
|
|
814
1291
|
this.#currentIndex = -1;
|
|
815
1292
|
this.#isPlaying = false;
|
|
816
|
-
|
|
817
|
-
|
|
1293
|
+
this.#prevClickedToRepeat = false;
|
|
1294
|
+
// 3. Clears and destroys all registered API adapters
|
|
1295
|
+
this.destroyAllAdapters();
|
|
818
1296
|
// 4. Remove all event listeners inherited from EventEmitter
|
|
819
1297
|
this.removeAllListeners();
|
|
1298
|
+
this.#adapterEventNames.clear();
|
|
820
1299
|
this.#destroyed = true;
|
|
1300
|
+
this.emit('destroyed');
|
|
821
1301
|
}
|
|
822
1302
|
}
|
|
823
1303
|
export default TinyMediaPlayer;
|