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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
var events = require('events');
|
|
4
4
|
var mediaContent = require('../basics/mediaContent.cjs');
|
|
5
5
|
var utils = require('./utils.cjs');
|
|
6
|
+
var index = require('./TinyMediaPlayer/index.cjs');
|
|
7
|
+
var objChecker = require('../basics/objChecker.cjs');
|
|
6
8
|
|
|
7
9
|
const checkDestroy = utils.createCheckDestroyed('TinyMediaPlayer');
|
|
8
10
|
|
|
@@ -15,6 +17,8 @@ const checkDestroy = utils.createCheckDestroyed('TinyMediaPlayer');
|
|
|
15
17
|
* @typedef {import('../basics/mediaContent.mjs').LoadingMediaProgress} LoadingMediaProgress
|
|
16
18
|
* @typedef {import('../basics/mediaContent.mjs').MediaLoadingErrorData} MediaLoadingErrorData
|
|
17
19
|
* @typedef {import('../basics/mediaContent.mjs').UnknownArtistGetter} UnknownArtistGetter
|
|
20
|
+
* @typedef {import('./TinyMediaPlayer/index.mjs').ContentTimeData} ContentTimeData
|
|
21
|
+
* @typedef {import('./TinyMediaPlayer/index.mjs').ContentData} ContentData
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -23,85 +27,15 @@ const checkDestroy = utils.createCheckDestroyed('TinyMediaPlayer');
|
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
|
-
* Configuration options for initializing the
|
|
30
|
+
* Configuration options for initializing the TinyMediaPlayer.
|
|
27
31
|
* @typedef {Object} TinyMediaPlayerOptions
|
|
28
32
|
* @property {boolean} [persistVolume=false] - Whether to automatically save the volume in localStorage.
|
|
29
|
-
* @property {string} [volumeStorageKey='
|
|
33
|
+
* @property {string} [volumeStorageKey='tiny_media_player_volume'] - The specific key name used for localStorage cache.
|
|
34
|
+
* @property {boolean} [repeatCurrentOnPrev=false] - If true, clicking 'previous' repeats the current track on the first click.
|
|
35
|
+
* @property {boolean} [smoothPlayPauseVolume=false] - If true, volume fades smoothly during play/pause transitions.
|
|
36
|
+
* @property {boolean} [smoothStopVolume=false] - If true, volume fades smoothly to zero when stopping.
|
|
30
37
|
*/
|
|
31
38
|
|
|
32
|
-
/**
|
|
33
|
-
* Interface definition for a Media Provider Adapter.
|
|
34
|
-
* All specific API wrappers must extend and implement this class.
|
|
35
|
-
* @abstract
|
|
36
|
-
*/
|
|
37
|
-
class BaseMediaAdapter {
|
|
38
|
-
constructor() {
|
|
39
|
-
if (new.target === BaseMediaAdapter) {
|
|
40
|
-
throw new Error('BaseMediaAdapter is an abstract class and cannot be instantiated directly.');
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Determines if this adapter can play the provided content.
|
|
46
|
-
* @param {MediaContent} content - The media content to evaluate.
|
|
47
|
-
* @returns {boolean} True if the adapter can handle the content, false otherwise.
|
|
48
|
-
*/
|
|
49
|
-
canHandle(content) {
|
|
50
|
-
throw new Error('Method "canHandle" must be implemented by the subclass.');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Starts or resumes playback of the provided content.
|
|
55
|
-
* @param {MediaContent} content - The media content to play.
|
|
56
|
-
* @returns {Promise<void>}
|
|
57
|
-
*/
|
|
58
|
-
async play(content) {
|
|
59
|
-
throw new Error('Method "play" must be implemented by the subclass.');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Pauses the current playback.
|
|
64
|
-
* @returns {Promise<void>}
|
|
65
|
-
*/
|
|
66
|
-
async pause() {
|
|
67
|
-
throw new Error('Method "pause" must be implemented by the subclass.');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Stops the playback completely and resets the internal platform state.
|
|
72
|
-
* @returns {Promise<void>}
|
|
73
|
-
*/
|
|
74
|
-
async stop() {
|
|
75
|
-
throw new Error('Method "stop" must be implemented by the subclass.');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Seeks to a specific time in the media timeline.
|
|
80
|
-
* @param {number} timeMs - The target time in milliseconds.
|
|
81
|
-
* @returns {Promise<void>}
|
|
82
|
-
*/
|
|
83
|
-
async seek(timeMs) {
|
|
84
|
-
throw new Error('Method "seek" must be implemented by the subclass.');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Retrieves the current playback time from the underlying API.
|
|
89
|
-
* @returns {number} The current time in milliseconds.
|
|
90
|
-
*/
|
|
91
|
-
getCurrentTime() {
|
|
92
|
-
throw new Error('Method "getCurrentTime" must be implemented by the subclass.');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Sets the playback volume for the underlying API.
|
|
97
|
-
* @param {number} volume - The volume level from 0.0 to 1.0.
|
|
98
|
-
* @returns {void}
|
|
99
|
-
*/
|
|
100
|
-
setVolume(volume) {
|
|
101
|
-
throw new Error('Method "setVolume" must be implemented by the subclass.');
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
39
|
/**
|
|
106
40
|
* Represents a search match containing the media object and its exact playlist index.
|
|
107
41
|
* @typedef {Object} SearchResult
|
|
@@ -113,7 +47,7 @@ class BaseMediaAdapter {
|
|
|
113
47
|
* A universal media player manager capable of orchestrating multiple API adapters.
|
|
114
48
|
*/
|
|
115
49
|
class TinyMediaPlayer extends events.EventEmitter {
|
|
116
|
-
static BaseMediaAdapter = BaseMediaAdapter;
|
|
50
|
+
static BaseMediaAdapter = index.BaseMediaAdapter;
|
|
117
51
|
|
|
118
52
|
/**
|
|
119
53
|
* @type {UnknownArtistGetter}
|
|
@@ -136,7 +70,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
136
70
|
*/
|
|
137
71
|
static set unknownArtist(value) {
|
|
138
72
|
if (typeof value !== 'string' && typeof value !== 'function')
|
|
139
|
-
throw new TypeError('unknownArtist must
|
|
73
|
+
throw new TypeError('unknownArtist must be a string or a function.');
|
|
140
74
|
TinyMediaPlayer.#unknownArtist = value;
|
|
141
75
|
}
|
|
142
76
|
|
|
@@ -172,8 +106,11 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
172
106
|
return this.#destroyed;
|
|
173
107
|
}
|
|
174
108
|
|
|
175
|
-
/** @type {
|
|
176
|
-
#adapters = new
|
|
109
|
+
/** @type {Set<BaseMediaAdapter>} */
|
|
110
|
+
#adapters = new Set();
|
|
111
|
+
|
|
112
|
+
/** @type {WeakMap<BaseMediaAdapter, Map<string, (...args: any[]) => any>>} */
|
|
113
|
+
#adapterHandlers = new WeakMap();
|
|
177
114
|
|
|
178
115
|
/** @type {MediaContent[]} */
|
|
179
116
|
#playlist = [];
|
|
@@ -199,17 +136,183 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
199
136
|
/** @type {string} */
|
|
200
137
|
#volumeStorageKey;
|
|
201
138
|
|
|
139
|
+
/** @type {boolean} */
|
|
140
|
+
#repeatCurrentOnPrev;
|
|
141
|
+
|
|
142
|
+
/** @type {boolean} */
|
|
143
|
+
#prevClickedToRepeat = false;
|
|
144
|
+
|
|
145
|
+
/** @type {boolean} */
|
|
146
|
+
#smoothPlayPauseVolume;
|
|
147
|
+
|
|
148
|
+
/** @type {boolean} */
|
|
149
|
+
#smoothStopVolume;
|
|
150
|
+
|
|
151
|
+
/** @type {AbortController | null} */
|
|
152
|
+
#fadeController = null;
|
|
153
|
+
|
|
154
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
155
|
+
#prevClickTimer = null;
|
|
156
|
+
|
|
157
|
+
/** @type {number} */
|
|
158
|
+
#fadeVolumeSpeed = 300;
|
|
159
|
+
|
|
160
|
+
/** @returns {number} The duration (ms) of the volume fade speed. */
|
|
161
|
+
get fadeVolumeSpeed() {
|
|
162
|
+
checkDestroy(this.#destroyed);
|
|
163
|
+
return this.#fadeVolumeSpeed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {number} value - The new duration in milliseconds.
|
|
168
|
+
* @throws {TypeError} If the value is not a number.
|
|
169
|
+
* @throws {RangeError} If the value is negative.
|
|
170
|
+
*/
|
|
171
|
+
set fadeVolumeSpeed(value) {
|
|
172
|
+
checkDestroy(this.#destroyed);
|
|
173
|
+
if (typeof value !== 'number') throw new TypeError('fadeVolumeSpeed must be a number.');
|
|
174
|
+
if (value < 0) throw new RangeError('fadeVolumeSpeed cannot be negative.');
|
|
175
|
+
this.#fadeVolumeSpeed = value;
|
|
176
|
+
this.emit('fadeVolumeSpeedChange', this.#fadeVolumeSpeed);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @type {number} */
|
|
180
|
+
#prevClickTimeoutDuration = 2000;
|
|
181
|
+
|
|
182
|
+
/** @returns {number} The duration (ms) before the prev click state resets. */
|
|
183
|
+
get prevClickTimeoutDuration() {
|
|
184
|
+
checkDestroy(this.#destroyed);
|
|
185
|
+
return this.#prevClickTimeoutDuration;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {number} value - The new duration in milliseconds.
|
|
190
|
+
* @throws {TypeError} If the value is not a number.
|
|
191
|
+
* @throws {RangeError} If the value is negative.
|
|
192
|
+
*/
|
|
193
|
+
set prevClickTimeoutDuration(value) {
|
|
194
|
+
checkDestroy(this.#destroyed);
|
|
195
|
+
if (typeof value !== 'number')
|
|
196
|
+
throw new TypeError('prevClickTimeoutDuration must be a number.');
|
|
197
|
+
if (value < 0) throw new RangeError('prevClickTimeoutDuration cannot be negative.');
|
|
198
|
+
this.#prevClickTimeoutDuration = value;
|
|
199
|
+
this.emit('prevClickTimeoutDurationChange', this.#prevClickTimeoutDuration);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** @returns {boolean} True if clicking 'previous' repeats the current track on the first click. */
|
|
203
|
+
get repeatCurrentOnPrev() {
|
|
204
|
+
checkDestroy(this.#destroyed);
|
|
205
|
+
return this.#repeatCurrentOnPrev;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {boolean} value - The repeat mode state.
|
|
210
|
+
* @throws {TypeError} If the value is not a boolean.
|
|
211
|
+
*/
|
|
212
|
+
set repeatCurrentOnPrev(value) {
|
|
213
|
+
checkDestroy(this.#destroyed);
|
|
214
|
+
if (typeof value !== 'boolean') {
|
|
215
|
+
throw new TypeError('repeatCurrentOnPrev must be a boolean.');
|
|
216
|
+
}
|
|
217
|
+
this.#repeatCurrentOnPrev = value;
|
|
218
|
+
this.emit('repeatCurrentOnPrevChange', this.#repeatCurrentOnPrev);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** @returns {boolean} True if volume fades smoothly during play/pause transitions. */
|
|
222
|
+
get smoothPlayPauseVolume() {
|
|
223
|
+
checkDestroy(this.#destroyed);
|
|
224
|
+
return this.#smoothPlayPauseVolume;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {boolean} value - The smooth play/pause volume mode.
|
|
229
|
+
* @throws {TypeError} If the value is not a boolean.
|
|
230
|
+
*/
|
|
231
|
+
set smoothPlayPauseVolume(value) {
|
|
232
|
+
checkDestroy(this.#destroyed);
|
|
233
|
+
if (typeof value !== 'boolean') {
|
|
234
|
+
throw new TypeError('smoothPlayPauseVolume must be a boolean.');
|
|
235
|
+
}
|
|
236
|
+
this.#smoothPlayPauseVolume = value;
|
|
237
|
+
this.emit('smoothPlayPauseVolumeChange', this.#smoothPlayPauseVolume);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** @returns {boolean} True if volume fades smoothly to zero when stopping. */
|
|
241
|
+
get smoothStopVolume() {
|
|
242
|
+
checkDestroy(this.#destroyed);
|
|
243
|
+
return this.#smoothStopVolume;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {boolean} value - The smooth stop volume mode.
|
|
248
|
+
* @throws {TypeError} If the value is not a boolean.
|
|
249
|
+
*/
|
|
250
|
+
set smoothStopVolume(value) {
|
|
251
|
+
checkDestroy(this.#destroyed);
|
|
252
|
+
if (typeof value !== 'boolean') {
|
|
253
|
+
throw new TypeError('smoothStopVolume must be a boolean.');
|
|
254
|
+
}
|
|
255
|
+
this.#smoothStopVolume = value;
|
|
256
|
+
this.emit('smoothStopVolumeChange', this.#smoothStopVolume);
|
|
257
|
+
}
|
|
258
|
+
|
|
202
259
|
/**
|
|
203
260
|
* @param {TinyMediaPlayerOptions} [options={}] - Configuration parameters for the player.
|
|
261
|
+
* @throws {TypeError} If options is not a valid object.
|
|
262
|
+
* @throws {TypeError} If option values do not match their expected types.
|
|
263
|
+
* @throws {RangeError} If numeric options are out of valid ranges.
|
|
204
264
|
*/
|
|
205
265
|
constructor(options = {}) {
|
|
206
266
|
super();
|
|
267
|
+
if (!objChecker.isValidObj(options)) {
|
|
268
|
+
throw new TypeError('Options must be a non-null object.');
|
|
269
|
+
}
|
|
270
|
+
|
|
207
271
|
// Volume configuration
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
272
|
+
if (options.persistVolume !== undefined) {
|
|
273
|
+
if (typeof options.persistVolume !== 'boolean') {
|
|
274
|
+
throw new TypeError('persistVolume must be a boolean.');
|
|
275
|
+
}
|
|
276
|
+
this.#persistVolume = options.persistVolume;
|
|
277
|
+
} else {
|
|
278
|
+
this.#persistVolume = false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (options.volumeStorageKey !== undefined) {
|
|
282
|
+
if (typeof options.volumeStorageKey !== 'string')
|
|
283
|
+
throw new TypeError('volumeStorageKey must be a string.');
|
|
284
|
+
this.#volumeStorageKey = options.volumeStorageKey;
|
|
285
|
+
} else {
|
|
286
|
+
this.#volumeStorageKey = 'tiny_media_player_volume';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// UX Options
|
|
290
|
+
if (options.repeatCurrentOnPrev !== undefined) {
|
|
291
|
+
if (typeof options.repeatCurrentOnPrev !== 'boolean') {
|
|
292
|
+
throw new TypeError('repeatCurrentOnPrev must be a boolean.');
|
|
293
|
+
}
|
|
294
|
+
this.#repeatCurrentOnPrev = options.repeatCurrentOnPrev;
|
|
295
|
+
} else {
|
|
296
|
+
this.#repeatCurrentOnPrev = false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (options.smoothPlayPauseVolume !== undefined) {
|
|
300
|
+
if (typeof options.smoothPlayPauseVolume !== 'boolean') {
|
|
301
|
+
throw new TypeError('smoothPlayPauseVolume must be a boolean.');
|
|
302
|
+
}
|
|
303
|
+
this.#smoothPlayPauseVolume = options.smoothPlayPauseVolume;
|
|
304
|
+
} else {
|
|
305
|
+
this.#smoothPlayPauseVolume = false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (options.smoothStopVolume !== undefined) {
|
|
309
|
+
if (typeof options.smoothStopVolume !== 'boolean') {
|
|
310
|
+
throw new TypeError('smoothStopVolume must be a boolean.');
|
|
311
|
+
}
|
|
312
|
+
this.#smoothStopVolume = options.smoothStopVolume;
|
|
313
|
+
} else {
|
|
314
|
+
this.#smoothStopVolume = false;
|
|
315
|
+
}
|
|
213
316
|
|
|
214
317
|
if (this.#persistVolume) this.#loadVolumeFromStorage();
|
|
215
318
|
}
|
|
@@ -295,6 +398,48 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
295
398
|
return this.#currentIndex === this.#playlist.length - 1 ? 0 : this.#playlist.length - 1;
|
|
296
399
|
}
|
|
297
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Handles smooth volume transitions for the active adapter.
|
|
403
|
+
* @param {number} targetVolume - The volume to transition to.
|
|
404
|
+
* @returns {Promise<void>}
|
|
405
|
+
*/
|
|
406
|
+
async #fadeAdapterVolume(targetVolume) {
|
|
407
|
+
if (this.#fadeController) {
|
|
408
|
+
this.#fadeController.abort();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
this.#fadeController = new AbortController();
|
|
412
|
+
const { signal } = this.#fadeController;
|
|
413
|
+
|
|
414
|
+
const adapter = this.#getActiveAdapter();
|
|
415
|
+
if (!adapter) return;
|
|
416
|
+
|
|
417
|
+
const startVolume = adapter.getVolume();
|
|
418
|
+
const startTime = performance.now();
|
|
419
|
+
const duration = this.#fadeVolumeSpeed;
|
|
420
|
+
|
|
421
|
+
return new Promise((resolve) => {
|
|
422
|
+
/** @param {number} currentTime */
|
|
423
|
+
const step = (currentTime) => {
|
|
424
|
+
if (signal.aborted) return resolve();
|
|
425
|
+
|
|
426
|
+
const elapsed = currentTime - startTime;
|
|
427
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
428
|
+
const calculatedVolume = startVolume + (targetVolume - startVolume) * progress;
|
|
429
|
+
const clampedVolume = Math.max(0, Math.min(1, calculatedVolume));
|
|
430
|
+
adapter.setVolume(clampedVolume);
|
|
431
|
+
|
|
432
|
+
if (progress < 1) {
|
|
433
|
+
requestAnimationFrame(step);
|
|
434
|
+
} else {
|
|
435
|
+
adapter.setVolume(targetVolume);
|
|
436
|
+
resolve();
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
requestAnimationFrame(step);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
298
443
|
// ==========================================
|
|
299
444
|
// GETTERS & STRICT SETTERS
|
|
300
445
|
// ==========================================
|
|
@@ -466,6 +611,9 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
466
611
|
if (value < 0 || value > 1)
|
|
467
612
|
throw new RangeError('Volume must be tightly constrained between 0.0 and 1.0.');
|
|
468
613
|
|
|
614
|
+
// Abort any ongoing fade to ensure manual control is immediate
|
|
615
|
+
if (this.#fadeController) this.#fadeController.abort();
|
|
616
|
+
|
|
469
617
|
this.#volume = value;
|
|
470
618
|
this.#saveVolumeToStorage();
|
|
471
619
|
this.emit('volumeChange', this.#volume);
|
|
@@ -539,21 +687,148 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
539
687
|
// ADAPTER MANAGEMENT
|
|
540
688
|
// ==========================================
|
|
541
689
|
|
|
690
|
+
get adapters() {
|
|
691
|
+
return Array.from(this.#adapters);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
get adaptersSize() {
|
|
695
|
+
return this.#adapters.size;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** @type {string[]} */
|
|
699
|
+
static #defaultAdapterEventNames = ['timeupdate', 'ended'];
|
|
700
|
+
|
|
701
|
+
static get defaultAdapterEventNames() {
|
|
702
|
+
return structuredClone(TinyMediaPlayer.#defaultAdapterEventNames);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** @param {string[]} value */
|
|
706
|
+
static set defaultAdapterEventNames(value) {
|
|
707
|
+
if (!Array.isArray(value) || !value.every((v) => typeof v === 'string'))
|
|
708
|
+
throw new TypeError('TinyMediaPlayer.defaultAdapterEventNames must be an array of strings.');
|
|
709
|
+
TinyMediaPlayer.#defaultAdapterEventNames = [...value];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
#adapterEventNames = new Set(TinyMediaPlayer.defaultAdapterEventNames);
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Removes the timeupdate on adapter emitter.
|
|
716
|
+
* @param {BaseMediaAdapter} adapter
|
|
717
|
+
*/
|
|
718
|
+
#removeHandler(adapter) {
|
|
719
|
+
const data = this.#adapterHandlers.get(adapter);
|
|
720
|
+
if (!data) return;
|
|
721
|
+
this.#adapterEventNames.forEach((eventName) => {
|
|
722
|
+
const handler = data.get(eventName);
|
|
723
|
+
if (handler) adapter.off(eventName, handler);
|
|
724
|
+
});
|
|
725
|
+
data.clear();
|
|
726
|
+
this.#adapterHandlers.delete(adapter);
|
|
727
|
+
}
|
|
728
|
+
|
|
542
729
|
/**
|
|
543
730
|
* Registers a new media API adapter.
|
|
544
|
-
* @param {string} id - Unique identifier for the platform (e.g., 'youtube', 'spotify').
|
|
545
731
|
* @param {BaseMediaAdapter} adapter - An instance extending BaseMediaAdapter.
|
|
546
|
-
* @throws {TypeError} If
|
|
732
|
+
* @throws {TypeError} If the adapter is invalid.
|
|
733
|
+
*/
|
|
734
|
+
registerAdapter(adapter) {
|
|
735
|
+
checkDestroy(this.#destroyed);
|
|
736
|
+
if (!(adapter instanceof index.BaseMediaAdapter)) {
|
|
737
|
+
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const events = new Map();
|
|
741
|
+
this.#adapterHandlers.set(adapter, events);
|
|
742
|
+
|
|
743
|
+
// Use the controlled helper to ensure each event is registered
|
|
744
|
+
// and correctly mapped in #adapterHandlers from the start.
|
|
745
|
+
this.#adapterEventNames.forEach((eventName) => {
|
|
746
|
+
this.#attachEventToAdapter(adapter, eventName);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
this.#adapters.add(adapter);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Removes a media API adapter.
|
|
754
|
+
* @param {BaseMediaAdapter} adapter - An instance extending BaseMediaAdapter.
|
|
755
|
+
* @throws {TypeError} If adapter is invalid.
|
|
547
756
|
*/
|
|
548
|
-
|
|
757
|
+
removeAdapter(adapter) {
|
|
549
758
|
checkDestroy(this.#destroyed);
|
|
550
|
-
if (
|
|
551
|
-
throw new TypeError('Adapter
|
|
759
|
+
if (!(adapter instanceof index.BaseMediaAdapter)) {
|
|
760
|
+
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
552
761
|
}
|
|
553
|
-
|
|
762
|
+
this.#removeHandler(adapter);
|
|
763
|
+
return this.#adapters.delete(adapter);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Destroys and removes a media API adapter.
|
|
768
|
+
* @param {BaseMediaAdapter} adapter - An instance extending BaseMediaAdapter.
|
|
769
|
+
* @throws {TypeError} If adapter is invalid.
|
|
770
|
+
*/
|
|
771
|
+
destroyAdapter(adapter) {
|
|
772
|
+
checkDestroy(this.#destroyed);
|
|
773
|
+
if (!(adapter instanceof index.BaseMediaAdapter)) {
|
|
554
774
|
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
555
775
|
}
|
|
556
|
-
this.#
|
|
776
|
+
this.#removeHandler(adapter);
|
|
777
|
+
const result = this.#adapters.delete(adapter);
|
|
778
|
+
if (result) adapter.destroy();
|
|
779
|
+
return result;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* If exists a media API adapter.
|
|
784
|
+
* @param {BaseMediaAdapter} adapter - An instance extending BaseMediaAdapter.
|
|
785
|
+
* @throws {TypeError} If adapter is invalid.
|
|
786
|
+
*/
|
|
787
|
+
hasAdapter(adapter) {
|
|
788
|
+
checkDestroy(this.#destroyed);
|
|
789
|
+
if (!(adapter instanceof index.BaseMediaAdapter)) {
|
|
790
|
+
throw new TypeError('Adapter must be an instance of BaseMediaAdapter.');
|
|
791
|
+
}
|
|
792
|
+
return this.#adapters.has(adapter);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Destroys and removes all registered media adapters.
|
|
797
|
+
*/
|
|
798
|
+
destroyAllAdapters() {
|
|
799
|
+
checkDestroy(this.#destroyed);
|
|
800
|
+
this.#adapters.forEach((adapter) => {
|
|
801
|
+
this.#removeHandler(adapter);
|
|
802
|
+
adapter.destroy();
|
|
803
|
+
});
|
|
804
|
+
this.#adapters.clear();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Clears all registered media adapters.
|
|
809
|
+
*/
|
|
810
|
+
clearAdapters() {
|
|
811
|
+
checkDestroy(this.#destroyed);
|
|
812
|
+
this.#adapters.forEach((adapter) => this.#removeHandler(adapter));
|
|
813
|
+
this.#adapters.clear();
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Helper to find the correct API wrapper for the content.
|
|
818
|
+
* @param {MediaContent} content - The content to check.
|
|
819
|
+
* @returns {BaseMediaAdapter | null} The compatible adapter, or null if empty.
|
|
820
|
+
* @throws {TypeError} If the content is invalid.
|
|
821
|
+
*/
|
|
822
|
+
getMediaAdapter(content) {
|
|
823
|
+
if (!objChecker.isValidObj(content)) {
|
|
824
|
+
throw new TypeError('Content must be a valid object.');
|
|
825
|
+
}
|
|
826
|
+
for (const adapter of this.#adapters.values()) {
|
|
827
|
+
if (adapter.canHandle(content)) {
|
|
828
|
+
return adapter;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return null;
|
|
557
832
|
}
|
|
558
833
|
|
|
559
834
|
/**
|
|
@@ -564,13 +839,119 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
564
839
|
#getActiveAdapter() {
|
|
565
840
|
if (this.#currentIndex === -1 || this.#playlist.length === 0) return null;
|
|
566
841
|
const currentContent = this.#playlist[this.#currentIndex];
|
|
842
|
+
const adapter = this.getMediaAdapter(currentContent);
|
|
843
|
+
if (adapter) return adapter;
|
|
844
|
+
throw new Error(`No compatible adapter found for content ID: ${currentContent.id}.`);
|
|
845
|
+
}
|
|
567
846
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
847
|
+
// ==========================================
|
|
848
|
+
// EVENT MANAGEMENT (CONTROLLED)
|
|
849
|
+
// ==========================================
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Adds a new event to be listened to on the adapters.
|
|
853
|
+
* @param {string} eventName - The name of the event to add.
|
|
854
|
+
* @throws {TypeError} If the event name is not a string.
|
|
855
|
+
*/
|
|
856
|
+
addAdapterEvent(eventName) {
|
|
857
|
+
checkDestroy(this.#destroyed);
|
|
858
|
+
if (typeof eventName !== 'string') {
|
|
859
|
+
throw new TypeError('Event name must be a string.');
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// If the event is already in the list, do nothing to avoid duplicates
|
|
863
|
+
if (this.#adapterEventNames.has(eventName)) return;
|
|
864
|
+
|
|
865
|
+
this.#adapterEventNames.add(eventName);
|
|
866
|
+
|
|
867
|
+
// Synchronize: Add the listener to all already registered adapters
|
|
868
|
+
this.#adapters.forEach((adapter) => {
|
|
869
|
+
this.#attachEventToAdapter(adapter, eventName);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Removes an event from the list of listened events.
|
|
875
|
+
* @param {string} eventName - The name of the event to remove.
|
|
876
|
+
* @throws {TypeError} If the event name is not a string.
|
|
877
|
+
*/
|
|
878
|
+
removeAdapterEvent(eventName) {
|
|
879
|
+
checkDestroy(this.#destroyed);
|
|
880
|
+
if (typeof eventName !== 'string') {
|
|
881
|
+
throw new TypeError('Event name must be a string.');
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!this.#adapterEventNames.has(eventName)) return;
|
|
885
|
+
|
|
886
|
+
this.#adapterEventNames.delete(eventName);
|
|
887
|
+
|
|
888
|
+
// Synchronize: Remove the listener from all registered adapters
|
|
889
|
+
this.#adapters.forEach((adapter) => {
|
|
890
|
+
this.#detachEventFromAdapter(adapter, eventName);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Resets the event list to the factory default state: ['timeupdate', 'ended'].
|
|
896
|
+
* @throws {TypeError} If the event name is not a string.
|
|
897
|
+
*/
|
|
898
|
+
resetAdapterEvents() {
|
|
899
|
+
checkDestroy(this.#destroyed);
|
|
900
|
+
const defaultEvents = TinyMediaPlayer.defaultAdapterEventNames;
|
|
901
|
+
|
|
902
|
+
// Convert to Array to avoid issues when deleting items during iteration
|
|
903
|
+
const currentEvents = Array.from(this.#adapterEventNames);
|
|
904
|
+
|
|
905
|
+
// 1. Remove events that are not part of the factory default
|
|
906
|
+
for (const eventName of currentEvents) {
|
|
907
|
+
if (!defaultEvents.includes(eventName)) {
|
|
908
|
+
this.removeAdapterEvent(eventName);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// 2. Ensure default events exist (in case they were previously removed)
|
|
913
|
+
for (const defaultEvent of defaultEvents) {
|
|
914
|
+
if (!this.#adapterEventNames.has(defaultEvent)) {
|
|
915
|
+
this.addAdapterEvent(defaultEvent);
|
|
571
916
|
}
|
|
572
917
|
}
|
|
573
|
-
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Private helper to attach an event to a specific adapter and save the handler.
|
|
922
|
+
* @param {BaseMediaAdapter} adapter - The destination adapter.
|
|
923
|
+
* @param {string} eventName - The event name.
|
|
924
|
+
*/
|
|
925
|
+
#attachEventToAdapter(adapter, eventName) {
|
|
926
|
+
const handlersMap = this.#adapterHandlers.get(adapter);
|
|
927
|
+
if (!handlersMap) return;
|
|
928
|
+
|
|
929
|
+
// Create an encapsulated handler to maintain security (checkDestroy)
|
|
930
|
+
/** @type {(...args: any[]) => any} */
|
|
931
|
+
const handler = (...args) => {
|
|
932
|
+
if (!this.#destroyed) {
|
|
933
|
+
this.emit(eventName, ...args);
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
adapter.on(eventName, handler);
|
|
938
|
+
handlersMap.set(eventName, handler); // Mandatory synchronization with the WeakMap
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Private helper to remove an event from a specific adapter using the saved handler.
|
|
943
|
+
* @param {BaseMediaAdapter} adapter - The destination adapter.
|
|
944
|
+
* @param {string} eventName - The event name.
|
|
945
|
+
*/
|
|
946
|
+
#detachEventFromAdapter(adapter, eventName) {
|
|
947
|
+
const handlersMap = this.#adapterHandlers.get(adapter);
|
|
948
|
+
if (!handlersMap) return;
|
|
949
|
+
|
|
950
|
+
const handler = handlersMap.get(eventName);
|
|
951
|
+
if (handler) {
|
|
952
|
+
adapter.off(eventName, handler);
|
|
953
|
+
handlersMap.delete(eventName); // Mandatory synchronization with the WeakMap
|
|
954
|
+
}
|
|
574
955
|
}
|
|
575
956
|
|
|
576
957
|
// ==========================================
|
|
@@ -585,8 +966,9 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
585
966
|
*/
|
|
586
967
|
addTrack(content) {
|
|
587
968
|
checkDestroy(this.#destroyed);
|
|
588
|
-
if (!content || typeof content
|
|
969
|
+
if (!objChecker.isValidObj(content) || typeof content.url !== 'string') {
|
|
589
970
|
throw new TypeError('Track content must be a valid MediaContent object containing a URL.');
|
|
971
|
+
}
|
|
590
972
|
|
|
591
973
|
/** @type {MediaContent} */
|
|
592
974
|
const newContent = { ...mediaContent.getMediaContentBase(), ...mediaContent.getMediaContentMetadata(), ...content };
|
|
@@ -604,7 +986,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
604
986
|
/**
|
|
605
987
|
* Checks if a track exists at the specified index.
|
|
606
988
|
* @param {number} index - The target index.
|
|
607
|
-
* @returns {boolean} True if the track exists
|
|
989
|
+
* @returns {boolean} True if the track exists at the specified index.
|
|
608
990
|
* @throws {TypeError} If the index is not a number.
|
|
609
991
|
*/
|
|
610
992
|
existsTrack(index) {
|
|
@@ -620,7 +1002,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
620
1002
|
* @param {number} index - The target index.
|
|
621
1003
|
* @returns {MediaContent} The media content object.
|
|
622
1004
|
* @throws {TypeError} If the index is not a number.
|
|
623
|
-
* @throws {RangeError} If the index is out of bounds.
|
|
1005
|
+
* @throws {RangeError} If the index is out of the playlist bounds.
|
|
624
1006
|
*/
|
|
625
1007
|
getTrack(index) {
|
|
626
1008
|
checkDestroy(this.#destroyed);
|
|
@@ -658,7 +1040,7 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
658
1040
|
this.#currentIndex = -1;
|
|
659
1041
|
} else if (this.#currentIndex >= this.#playlist.length) {
|
|
660
1042
|
// If we removed the last item and others exist, reset index safely
|
|
661
|
-
this.#currentIndex = 0;
|
|
1043
|
+
this.#currentIndex = 0; // Reset to start if out of bounds
|
|
662
1044
|
}
|
|
663
1045
|
this.emit('trackChange', this.#currentIndex);
|
|
664
1046
|
} else {
|
|
@@ -736,7 +1118,12 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
736
1118
|
if (!adapter) return;
|
|
737
1119
|
|
|
738
1120
|
// Ensure the adapter aligns with the global volume before playing
|
|
739
|
-
|
|
1121
|
+
if (this.#smoothPlayPauseVolume) {
|
|
1122
|
+
await this.#fadeAdapterVolume(this.#volume);
|
|
1123
|
+
} else {
|
|
1124
|
+
adapter.setVolume(this.#volume);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
740
1127
|
await adapter.play(this.#playlist[this.#currentIndex]);
|
|
741
1128
|
this.#isPlaying = true;
|
|
742
1129
|
this.emit('play', this.#currentIndex);
|
|
@@ -751,6 +1138,10 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
751
1138
|
const adapter = this.#getActiveAdapter();
|
|
752
1139
|
if (!adapter) return;
|
|
753
1140
|
|
|
1141
|
+
if (this.#smoothPlayPauseVolume) {
|
|
1142
|
+
await this.#fadeAdapterVolume(0);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
754
1145
|
await adapter.pause();
|
|
755
1146
|
this.#isPlaying = false;
|
|
756
1147
|
this.emit('pause', this.#currentIndex);
|
|
@@ -765,6 +1156,10 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
765
1156
|
const adapter = this.#getActiveAdapter();
|
|
766
1157
|
if (!adapter) return;
|
|
767
1158
|
|
|
1159
|
+
if (this.#smoothStopVolume) {
|
|
1160
|
+
await this.#fadeAdapterVolume(0);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
768
1163
|
await adapter.stop();
|
|
769
1164
|
this.#isPlaying = false;
|
|
770
1165
|
this.emit('stop', this.#currentIndex);
|
|
@@ -813,6 +1208,29 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
813
1208
|
checkDestroy(this.#destroyed);
|
|
814
1209
|
if (this.#playlist.length === 0) return;
|
|
815
1210
|
|
|
1211
|
+
// UX: Repeat current track if "repeat on prev" is enabled and it's the first click
|
|
1212
|
+
if (this.#repeatCurrentOnPrev && !this.#prevClickedToRepeat) {
|
|
1213
|
+
this.#prevClickedToRepeat = true;
|
|
1214
|
+
|
|
1215
|
+
// Reset the "repeat" state after 2 seconds of inactivity to prevent glitches
|
|
1216
|
+
if (this.#prevClickTimer) clearTimeout(this.#prevClickTimer);
|
|
1217
|
+
this.#prevClickTimer = setTimeout(() => {
|
|
1218
|
+
this.#prevClickedToRepeat = false;
|
|
1219
|
+
this.#prevClickTimer = null;
|
|
1220
|
+
}, 2000);
|
|
1221
|
+
|
|
1222
|
+
await this.stop();
|
|
1223
|
+
await this.play();
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// If we reached here, it's either the second click or repeat is disabled
|
|
1228
|
+
if (this.#prevClickTimer) {
|
|
1229
|
+
clearTimeout(this.#prevClickTimer);
|
|
1230
|
+
this.#prevClickTimer = null;
|
|
1231
|
+
}
|
|
1232
|
+
this.#prevClickedToRepeat = false;
|
|
1233
|
+
|
|
816
1234
|
await this.stop();
|
|
817
1235
|
|
|
818
1236
|
if (this.#isRandom) {
|
|
@@ -837,9 +1255,9 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
837
1255
|
/**
|
|
838
1256
|
* Jumps to a specific absolute time in the timeline.
|
|
839
1257
|
* @param {number} timeMs - The target time in milliseconds.
|
|
1258
|
+
* @returns {Promise<void>}
|
|
840
1259
|
* @throws {TypeError} If timeMs is not a number.
|
|
841
1260
|
* @throws {RangeError} If timeMs is negative.
|
|
842
|
-
* @returns {Promise<void>}
|
|
843
1261
|
*/
|
|
844
1262
|
async seek(timeMs) {
|
|
845
1263
|
checkDestroy(this.#destroyed);
|
|
@@ -881,6 +1299,107 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
881
1299
|
await this.seek(targetTime);
|
|
882
1300
|
}
|
|
883
1301
|
|
|
1302
|
+
// ==========================================
|
|
1303
|
+
// TIME UTILITIES (DELEGATED TO ADAPTERS)
|
|
1304
|
+
// ==========================================
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Gets the current playback time.
|
|
1308
|
+
* @returns {number} The current time in milliseconds.
|
|
1309
|
+
*/
|
|
1310
|
+
getCurrentTime() {
|
|
1311
|
+
checkDestroy(this.#destroyed);
|
|
1312
|
+
try {
|
|
1313
|
+
const adapter = this.#getActiveAdapter();
|
|
1314
|
+
return adapter ? adapter.getCurrentTime() : 0;
|
|
1315
|
+
} catch {
|
|
1316
|
+
return 0;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Gets the total duration of the current track.
|
|
1322
|
+
* @returns {number} The total duration in milliseconds.
|
|
1323
|
+
*/
|
|
1324
|
+
getTotalDuration() {
|
|
1325
|
+
checkDestroy(this.#destroyed);
|
|
1326
|
+
try {
|
|
1327
|
+
const adapter = this.#getActiveAdapter();
|
|
1328
|
+
return adapter ? adapter.getTotalDuration() : 0;
|
|
1329
|
+
} catch {
|
|
1330
|
+
return 0;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Gets the remaining time until the current track ends.
|
|
1336
|
+
* @returns {number} The remaining time in milliseconds.
|
|
1337
|
+
*/
|
|
1338
|
+
getRemainingTime() {
|
|
1339
|
+
checkDestroy(this.#destroyed);
|
|
1340
|
+
try {
|
|
1341
|
+
const adapter = this.#getActiveAdapter();
|
|
1342
|
+
return adapter ? adapter.getRemainingTime() : 0;
|
|
1343
|
+
} catch {
|
|
1344
|
+
return 0;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Gets the percentage of the current track that has been played.
|
|
1350
|
+
* @returns {number} A value between 0 and 100.
|
|
1351
|
+
*/
|
|
1352
|
+
getPlaybackPercentage() {
|
|
1353
|
+
checkDestroy(this.#destroyed);
|
|
1354
|
+
try {
|
|
1355
|
+
const adapter = this.#getActiveAdapter();
|
|
1356
|
+
return adapter ? adapter.getPlaybackPercentage() : 0;
|
|
1357
|
+
} catch {
|
|
1358
|
+
return 0;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Retrieves the metadata of the currently loaded content, returning a structured
|
|
1364
|
+
* object containing details such as the content ID, title, author, and duration.
|
|
1365
|
+
* @returns {Promise<ContentData>}
|
|
1366
|
+
*/
|
|
1367
|
+
async getContentData() {
|
|
1368
|
+
checkDestroy(this.#destroyed);
|
|
1369
|
+
const defaultData = {
|
|
1370
|
+
id: '',
|
|
1371
|
+
createdAt: '',
|
|
1372
|
+
artistId: '',
|
|
1373
|
+
artistName: '',
|
|
1374
|
+
description: '',
|
|
1375
|
+
title: '',
|
|
1376
|
+
duration: 0,
|
|
1377
|
+
avatar: '',
|
|
1378
|
+
url: '',
|
|
1379
|
+
};
|
|
1380
|
+
try {
|
|
1381
|
+
const adapter = this.#getActiveAdapter();
|
|
1382
|
+
return adapter ? adapter.getContentData() : defaultData;
|
|
1383
|
+
} catch {
|
|
1384
|
+
return defaultData;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Asynchronously waits until the content media adapter is fully initialized and ready.
|
|
1390
|
+
* @returns {Promise<void>} A promise that resolves once the adapter is ready.
|
|
1391
|
+
*/
|
|
1392
|
+
async waitIsReady() {
|
|
1393
|
+
checkDestroy(this.#destroyed);
|
|
1394
|
+
const adapter = this.#getActiveAdapter();
|
|
1395
|
+
if (!adapter) return;
|
|
1396
|
+
return adapter.waitIsReady();
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// ==========================================
|
|
1400
|
+
// DESTROYER
|
|
1401
|
+
// ==========================================
|
|
1402
|
+
|
|
884
1403
|
/**
|
|
885
1404
|
* Safely destroys the TinyMediaPlayer instance.
|
|
886
1405
|
* This method stops active playback, clears the internal playlist,
|
|
@@ -901,17 +1420,22 @@ class TinyMediaPlayer extends events.EventEmitter {
|
|
|
901
1420
|
console.warn('[TinyMediaPlayer] Non-fatal error during playback termination:', error);
|
|
902
1421
|
}
|
|
903
1422
|
|
|
1423
|
+
if (this.#prevClickTimer) clearTimeout(this.#prevClickTimer);
|
|
1424
|
+
|
|
904
1425
|
// 2. Clear internal state variables
|
|
905
1426
|
this.#playlist = [];
|
|
906
1427
|
this.#currentIndex = -1;
|
|
907
1428
|
this.#isPlaying = false;
|
|
1429
|
+
this.#prevClickedToRepeat = false;
|
|
908
1430
|
|
|
909
|
-
// 3.
|
|
910
|
-
this
|
|
1431
|
+
// 3. Clears and destroys all registered API adapters
|
|
1432
|
+
this.destroyAllAdapters();
|
|
911
1433
|
|
|
912
1434
|
// 4. Remove all event listeners inherited from EventEmitter
|
|
913
1435
|
this.removeAllListeners();
|
|
1436
|
+
this.#adapterEventNames.clear();
|
|
914
1437
|
this.#destroyed = true;
|
|
1438
|
+
this.emit('destroyed');
|
|
915
1439
|
}
|
|
916
1440
|
}
|
|
917
1441
|
|