tiny-essentials 1.28.5 → 1.28.7
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/6.md +14 -0
- package/changelog/1/28/7.md +7 -0
- package/dist/v1/TinyAdvancedRaffle.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/TinyUploadClicker.min.js +1 -1
- package/dist/v1/css/TinyLoadingScreen.min.css +1 -1
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.cjs +80 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.d.mts +40 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.mjs +73 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.cjs +80 -3
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.d.mts +40 -0
- package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.mjs +73 -3
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.cjs +846 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.d.mts +315 -0
- package/dist/v1/libs/TinyMediaPlayer/SoundCloud.mjs +745 -0
- package/dist/v1/libs/TinyMediaPlayer/Youtube.cjs +151 -12
- package/dist/v1/libs/TinyMediaPlayer/Youtube.d.mts +111 -0
- package/dist/v1/libs/TinyMediaPlayer/Youtube.mjs +131 -10
- 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/index.cjs +79 -1
- package/dist/v1/libs/TinyMediaPlayer/index.d.mts +75 -0
- package/dist/v1/libs/TinyMediaPlayer/index.mjs +71 -1
- package/dist/v1/libs/TinyMediaPlayer.cjs +75 -0
- package/dist/v1/libs/TinyMediaPlayer.d.mts +22 -0
- package/dist/v1/libs/TinyMediaPlayer.mjs +71 -0
- package/docs/v1/libs/TinyMediaPlayer.md +56 -40
- package/package.json +55 -10
|
@@ -10,6 +10,7 @@ var objChecker = require('../../basics/objChecker.cjs');
|
|
|
10
10
|
* @typedef {import('../../basics/mediaContent.mjs').PictureDataType} PictureDataType
|
|
11
11
|
* @typedef {import('../../basics/mediaContent.mjs').MediaContent<PictureDataType>} MediaContent
|
|
12
12
|
* @typedef {import('./index.mjs').ContentTimeData} ContentTimeData
|
|
13
|
+
* @typedef {import('./index.mjs').ContentData} ContentData
|
|
13
14
|
*
|
|
14
15
|
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlayerState} YTPlayerState
|
|
15
16
|
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlayerErrors} YTPlayerErrors
|
|
@@ -187,7 +188,30 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
187
188
|
* @throws {TypeError} If the `videoId` is not a valid string.
|
|
188
189
|
*/
|
|
189
190
|
static createIframeContainer(options) {
|
|
190
|
-
const {
|
|
191
|
+
const {
|
|
192
|
+
videoId,
|
|
193
|
+
width = 640,
|
|
194
|
+
height = 360,
|
|
195
|
+
hidden = true,
|
|
196
|
+
autoplay = false,
|
|
197
|
+
ivLoadPolicy = true,
|
|
198
|
+
list = null,
|
|
199
|
+
listType = null,
|
|
200
|
+
ccLangPref = null,
|
|
201
|
+
ccLoadPolicy = false,
|
|
202
|
+
color = null,
|
|
203
|
+
controls = true,
|
|
204
|
+
disableKb = false,
|
|
205
|
+
loop = false,
|
|
206
|
+
end = null,
|
|
207
|
+
start = null,
|
|
208
|
+
playlist = null,
|
|
209
|
+
playsinline = false,
|
|
210
|
+
widgetReferrer = null,
|
|
211
|
+
rel = true,
|
|
212
|
+
fs = true,
|
|
213
|
+
hl = null,
|
|
214
|
+
} = options;
|
|
191
215
|
|
|
192
216
|
if (typeof videoId !== 'string' || videoId.trim() === '') {
|
|
193
217
|
throw new TypeError('The "videoId" property is required and must be a valid string.');
|
|
@@ -196,7 +220,7 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
196
220
|
const iframe = document.createElement('iframe');
|
|
197
221
|
|
|
198
222
|
// enablejsapi=1 is required to allow control via the YouTube API
|
|
199
|
-
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=${autoplay ? 1 : 0}&enablejsapi=1`;
|
|
223
|
+
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=${autoplay ? 1 : 0}&rel=${rel ? 1 : 0}&iv_load_policy=${ivLoadPolicy ? 1 : 3}&loop=${loop ? 1 : 0}&playsinline=${playsinline ? 1 : 0}&fs=${fs ? 1 : 0}${typeof playlist === 'string' || Array.isArray(playlist) ? `&playlist=${!Array.isArray(playlist) ? playlist : playlist.join(',')}` : ''}${typeof ccLangPref === 'string' ? `&cc_lang_pref=${ccLangPref}` : ''}${typeof end === 'string' || typeof end === 'number' ? `&end=${end}` : ''}${typeof start === 'string' || typeof start === 'number' ? `&start=${start}` : ''}&cc_load_policy=${ccLoadPolicy ? 1 : 0}${typeof list === 'string' ? `&list=${list}` : ''}${typeof listType === 'string' ? `&listType=${listType}` : ''}${typeof widgetReferrer === 'string' ? `&widget_referrer=${widgetReferrer}` : ''}${typeof color === 'string' ? `&color=${color}` : ''}${typeof hl === 'string' ? `&hl=${hl}` : ''}&controls=${controls ? 1 : 0}&disablekb=${disableKb ? 1 : 0}&enablejsapi=1`;
|
|
200
224
|
|
|
201
225
|
iframe.style.width = `${width}px`;
|
|
202
226
|
iframe.style.height = `${height}px`;
|
|
@@ -243,6 +267,8 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
243
267
|
/** @type {HTMLIFrameElement} */
|
|
244
268
|
#container;
|
|
245
269
|
|
|
270
|
+
#isReady = false;
|
|
271
|
+
|
|
246
272
|
/** @type {Promise<void>} */
|
|
247
273
|
#apiLoadedPromise;
|
|
248
274
|
|
|
@@ -265,9 +291,26 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
265
291
|
return this.#currentContentId;
|
|
266
292
|
}
|
|
267
293
|
|
|
294
|
+
/** @type {number} */
|
|
295
|
+
#lastReportedTime = 0;
|
|
296
|
+
|
|
268
297
|
/** @type {ReturnType<typeof setInterval>|null} */
|
|
269
298
|
#timeUpdateInterval = null;
|
|
270
299
|
|
|
300
|
+
/** @type {number} */
|
|
301
|
+
#pollingTimeUpdateValue = 250;
|
|
302
|
+
|
|
303
|
+
get pollingTimeUpdateValue() {
|
|
304
|
+
checkDestroy(this.#destroyed);
|
|
305
|
+
return this.#pollingTimeUpdateValue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
set pollingTimeUpdateValue(timeMs) {
|
|
309
|
+
checkDestroy(this.#destroyed);
|
|
310
|
+
if (typeof timeMs !== 'number') throw new TypeError('Time must be a number.');
|
|
311
|
+
this.#pollingTimeUpdateValue = timeMs;
|
|
312
|
+
}
|
|
313
|
+
|
|
271
314
|
/**
|
|
272
315
|
* Gets whether the adapter has been destroyed.
|
|
273
316
|
* @returns {boolean}
|
|
@@ -336,6 +379,24 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
336
379
|
return this.#currentVolume;
|
|
337
380
|
}
|
|
338
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Synchronously checks whether the YouTube media adapter is fully initialized and ready.
|
|
384
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
385
|
+
*/
|
|
386
|
+
isReady() {
|
|
387
|
+
checkDestroy(this.#destroyed);
|
|
388
|
+
return this.#isReady;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Asynchronously waits until the YouTube media adapter is fully initialized and ready.
|
|
393
|
+
* @returns {Promise<void>} A promise that resolves once the adapter is ready.
|
|
394
|
+
*/
|
|
395
|
+
async waitIsReady() {
|
|
396
|
+
checkDestroy(this.#destroyed);
|
|
397
|
+
return promiseUtils.waitForTrue(() => this.#isReady);
|
|
398
|
+
}
|
|
399
|
+
|
|
339
400
|
/**
|
|
340
401
|
* Retrieves a consolidated object containing all time-related metrics for the current media content.
|
|
341
402
|
* @returns {ContentTimeData} An object containing total, current, remaining time, and playback percentage.
|
|
@@ -353,8 +414,7 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
353
414
|
* @param {MediaContent} content - The media content to evaluate.
|
|
354
415
|
* @returns {boolean} True if the content is a YouTube link/ID, false otherwise.
|
|
355
416
|
*/
|
|
356
|
-
canHandle(content) {
|
|
357
|
-
checkDestroy(this.#destroyed);
|
|
417
|
+
static canHandle(content) {
|
|
358
418
|
if (!objChecker.isValidObj(content)) {
|
|
359
419
|
throw new TypeError('Content must be a valid object.');
|
|
360
420
|
}
|
|
@@ -364,9 +424,21 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
364
424
|
return youtubeRegex.test(content.url);
|
|
365
425
|
}
|
|
366
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Determines if the content is a valid YouTube URL or ID.
|
|
429
|
+
* @param {MediaContent} content - The media content to evaluate.
|
|
430
|
+
* @returns {boolean} True if the content is a YouTube link/ID, false otherwise.
|
|
431
|
+
*/
|
|
432
|
+
canHandle(content) {
|
|
433
|
+
checkDestroy(this.#destroyed);
|
|
434
|
+
return YoutubeMediaAdapter.canHandle(content);
|
|
435
|
+
}
|
|
436
|
+
|
|
367
437
|
/**
|
|
368
438
|
* Plays the media content. If the player is already initialized with a different video,
|
|
369
439
|
* it will load the new video automatically.
|
|
440
|
+
*
|
|
441
|
+
* Example: https://www.youtube.com/watch?v=XXXXXXXXXXX
|
|
370
442
|
* @param {MediaContent} content - The media content to play.
|
|
371
443
|
* @returns {Promise<void>}
|
|
372
444
|
* @throws {TypeError} If the content data is invalid.
|
|
@@ -394,6 +466,9 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
394
466
|
if (this.#currentContentId !== targetVideoId) {
|
|
395
467
|
this.#currentContentId = targetVideoId;
|
|
396
468
|
this.#player?.loadVideoById(targetVideoId);
|
|
469
|
+
// Reset the last reported time to 0 to prevent a false 'seek' event
|
|
470
|
+
// when the new video starts playing.
|
|
471
|
+
this.#lastReportedTime = 0;
|
|
397
472
|
} else {
|
|
398
473
|
this.#player?.playVideo();
|
|
399
474
|
}
|
|
@@ -460,7 +535,9 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
460
535
|
|
|
461
536
|
// Resolve the initialization promise when the player is ready
|
|
462
537
|
this.#masterEmitter?.once('onReady', () => {
|
|
538
|
+
this.#lastReportedTime = this.getCurrentTime();
|
|
463
539
|
this.#startPollingTimeUpdate();
|
|
540
|
+
this.#isReady = true;
|
|
464
541
|
resolve();
|
|
465
542
|
});
|
|
466
543
|
|
|
@@ -470,16 +547,31 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
470
547
|
|
|
471
548
|
/**
|
|
472
549
|
* Starts a polling interval to emit 'timeupdate' events, matching the HTML5 Audio interface.
|
|
550
|
+
* Also detects 'seek' events by monitoring large jumps in the playback time.
|
|
473
551
|
*/
|
|
474
552
|
#startPollingTimeUpdate() {
|
|
475
553
|
this.#timeUpdateInterval = setInterval(() => {
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
)
|
|
554
|
+
if (!this.#player) return;
|
|
555
|
+
|
|
556
|
+
const currentTime = this.getCurrentTime();
|
|
557
|
+
const isPlaying = this.#player.getPlayerState() === YoutubeMediaAdapter.PlayerState.PLAYING;
|
|
558
|
+
|
|
559
|
+
if (isPlaying) {
|
|
560
|
+
const diff = Math.abs(currentTime - this.#lastReportedTime);
|
|
561
|
+
|
|
562
|
+
// If the time jump is greater than 1 second, we interpret it as a 'seek' event.
|
|
563
|
+
// This captures both programmatic seeks and user interactions with the YouTube UI.
|
|
564
|
+
if (diff > 1000) {
|
|
565
|
+
this.emit('seek', currentTime / 1000);
|
|
566
|
+
}
|
|
567
|
+
|
|
480
568
|
this.#masterEmitter?.emit('timeupdate', this.getTimeData());
|
|
481
569
|
}
|
|
482
|
-
|
|
570
|
+
|
|
571
|
+
// Always update the last reported time to keep it in sync with the actual player position.
|
|
572
|
+
// This prevents false 'seek' events when resuming playback after a pause or a seek while paused.
|
|
573
|
+
this.#lastReportedTime = currentTime;
|
|
574
|
+
}, this.#pollingTimeUpdateValue);
|
|
483
575
|
}
|
|
484
576
|
|
|
485
577
|
/**
|
|
@@ -537,9 +629,35 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
537
629
|
async seek(timeMs) {
|
|
538
630
|
checkDestroy(this.#destroyed);
|
|
539
631
|
if (typeof timeMs !== 'number') throw new TypeError('Time must be a number.');
|
|
540
|
-
const
|
|
541
|
-
this.#player?.seekTo(
|
|
542
|
-
|
|
632
|
+
const targetTimeInSeconds = timeMs / 1000;
|
|
633
|
+
this.#player?.seekTo(targetTimeInSeconds, true);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Checks whether the player is currently muted.
|
|
638
|
+
* @returns {boolean|null} True if muted, false otherwise.
|
|
639
|
+
*/
|
|
640
|
+
isMuted() {
|
|
641
|
+
checkDestroy(this.#destroyed);
|
|
642
|
+
return this.#player?.isMuted() ?? null;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Mutes the current playback.
|
|
647
|
+
* @returns {Promise<void>}
|
|
648
|
+
*/
|
|
649
|
+
async mute() {
|
|
650
|
+
checkDestroy(this.#destroyed);
|
|
651
|
+
this.#player?.mute();
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Unmutes the current playback.
|
|
656
|
+
* @returns {Promise<void>}
|
|
657
|
+
*/
|
|
658
|
+
async unMute() {
|
|
659
|
+
checkDestroy(this.#destroyed);
|
|
660
|
+
this.#player?.unMute();
|
|
543
661
|
}
|
|
544
662
|
|
|
545
663
|
/**
|
|
@@ -613,6 +731,27 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
|
|
|
613
731
|
: null;
|
|
614
732
|
}
|
|
615
733
|
|
|
734
|
+
/**
|
|
735
|
+
* Retrieves the metadata of the currently loaded YouTube video, returning a structured
|
|
736
|
+
* object containing details such as the video ID, title, author, and duration.
|
|
737
|
+
* @returns {Promise<ContentData>}
|
|
738
|
+
*/
|
|
739
|
+
async getContentData() {
|
|
740
|
+
const video = this.#player?.getVideoData();
|
|
741
|
+
if (!video) throw new Error('No video data available from the YouTube player.');
|
|
742
|
+
return {
|
|
743
|
+
id: String(video.video_id ?? ''),
|
|
744
|
+
createdAt: '',
|
|
745
|
+
artistId: '',
|
|
746
|
+
artistName: String(video.author ?? ''),
|
|
747
|
+
description: '',
|
|
748
|
+
title: String(video.title ?? ''),
|
|
749
|
+
duration: Number(video.progressBarEndPositionUtcTimeMillis ?? 0),
|
|
750
|
+
avatar: this.getThumbnailUrl() ?? '',
|
|
751
|
+
url: `https://youtu.be/${this.#currentContentId}`,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
616
755
|
/**
|
|
617
756
|
* Cleans up the instance and disconnects from the shared player.
|
|
618
757
|
*/
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @typedef {import('../../basics/mediaContent.mjs').PictureDataType} PictureDataType
|
|
3
3
|
* @typedef {import('../../basics/mediaContent.mjs').MediaContent<PictureDataType>} MediaContent
|
|
4
4
|
* @typedef {import('./index.mjs').ContentTimeData} ContentTimeData
|
|
5
|
+
* @typedef {import('./index.mjs').ContentData} ContentData
|
|
5
6
|
*
|
|
6
7
|
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlayerState} YTPlayerState
|
|
7
8
|
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlayerErrors} YTPlayerErrors
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
export type PictureDataType = import('../../basics/mediaContent.mjs').PictureDataType;
|
|
27
28
|
export type MediaContent = import('../../basics/mediaContent.mjs').MediaContent<PictureDataType>;
|
|
28
29
|
export type ContentTimeData = import('./index.mjs').ContentTimeData;
|
|
30
|
+
export type ContentData = import('./index.mjs').ContentData;
|
|
29
31
|
export type YTPlayerState = import('./docs/YouTubePlayer.mjs').YTPlayerState;
|
|
30
32
|
export type YTPlayerErrors = import('./docs/YouTubePlayer.mjs').YTPlayerErrors;
|
|
31
33
|
export type YTQuality = import('./docs/YouTubePlayer.mjs').YTQuality;
|
|
@@ -66,6 +68,74 @@ export type IframeContainerOptions = {
|
|
|
66
68
|
* - If true, the video will start automatically upon loading.
|
|
67
69
|
*/
|
|
68
70
|
autoplay?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* - The ID of a playlist to load.
|
|
73
|
+
*/
|
|
74
|
+
list?: string | null;
|
|
75
|
+
/**
|
|
76
|
+
* - The type of list to load (e.g., 'playlist').
|
|
77
|
+
*/
|
|
78
|
+
listType?: string | null;
|
|
79
|
+
/**
|
|
80
|
+
* - The preferred language for closed captions.
|
|
81
|
+
*/
|
|
82
|
+
ccLangPref?: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* - Whether to load closed captions by default (0 for off, 1 for on).
|
|
85
|
+
*/
|
|
86
|
+
ccLoadPolicy?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* - The color of the player controls (e.g., 'red', 'white').
|
|
89
|
+
*/
|
|
90
|
+
color?: string | null;
|
|
91
|
+
/**
|
|
92
|
+
* - Whether to display the player controls.
|
|
93
|
+
*/
|
|
94
|
+
controls?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* - Whether to disable keyboard controls.
|
|
97
|
+
*/
|
|
98
|
+
disableKb?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* - Whether the video should loop.
|
|
101
|
+
*/
|
|
102
|
+
loop?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* - The time in seconds when the video should end.
|
|
105
|
+
*/
|
|
106
|
+
end?: number | string | null;
|
|
107
|
+
/**
|
|
108
|
+
* - The time in seconds when the video should start.
|
|
109
|
+
*/
|
|
110
|
+
start?: number | string | null;
|
|
111
|
+
/**
|
|
112
|
+
* - A comma-separated list of video IDs to be played as a playlist.
|
|
113
|
+
*/
|
|
114
|
+
playlist?: string | string[] | null;
|
|
115
|
+
/**
|
|
116
|
+
* - Whether to play the video inline on mobile devices.
|
|
117
|
+
*/
|
|
118
|
+
playsinline?: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* - The URL of the page where the player is embedded.
|
|
121
|
+
*/
|
|
122
|
+
widgetReferrer?: string | null;
|
|
123
|
+
/**
|
|
124
|
+
* - Whether to show related videos from the same channel.
|
|
125
|
+
*/
|
|
126
|
+
rel?: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* - The language of the player interface.
|
|
129
|
+
*/
|
|
130
|
+
hl?: string | null;
|
|
131
|
+
/**
|
|
132
|
+
* - Whether to allow the player to enter fullscreen mode.
|
|
133
|
+
*/
|
|
134
|
+
fs?: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* - The policy for showing video annotations (1 to show, 3 to hide).
|
|
137
|
+
*/
|
|
138
|
+
ivLoadPolicy?: boolean;
|
|
69
139
|
};
|
|
70
140
|
import { BaseMediaAdapter } from './index.mjs';
|
|
71
141
|
import YouTubePlayer from './docs/YouTubePlayer.mjs';
|
|
@@ -140,6 +210,8 @@ declare class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
140
210
|
static getThumbnailUrl(videoId: string, quality?: string): string;
|
|
141
211
|
get id(): string;
|
|
142
212
|
get currentContentId(): string | null;
|
|
213
|
+
get pollingTimeUpdateValue(): number;
|
|
214
|
+
set pollingTimeUpdateValue(timeMs: number);
|
|
143
215
|
/**
|
|
144
216
|
* Gets whether the adapter has been destroyed.
|
|
145
217
|
* @returns {boolean}
|
|
@@ -164,11 +236,27 @@ declare class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
164
236
|
* @returns {number} The current volume level from 0.0 to 1.0.
|
|
165
237
|
*/
|
|
166
238
|
get volume(): number;
|
|
239
|
+
/**
|
|
240
|
+
* Synchronously checks whether the YouTube media adapter is fully initialized and ready.
|
|
241
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
242
|
+
*/
|
|
243
|
+
isReady(): boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Asynchronously waits until the YouTube media adapter is fully initialized and ready.
|
|
246
|
+
* @returns {Promise<void>} A promise that resolves once the adapter is ready.
|
|
247
|
+
*/
|
|
248
|
+
waitIsReady(): Promise<void>;
|
|
167
249
|
/**
|
|
168
250
|
* Retrieves a consolidated object containing all time-related metrics for the current media content.
|
|
169
251
|
* @returns {ContentTimeData} An object containing total, current, remaining time, and playback percentage.
|
|
170
252
|
*/
|
|
171
253
|
getTimeData(): ContentTimeData;
|
|
254
|
+
/**
|
|
255
|
+
* Determines if the content is a valid YouTube URL or ID.
|
|
256
|
+
* @param {MediaContent} content - The media content to evaluate.
|
|
257
|
+
* @returns {boolean} True if the content is a YouTube link/ID, false otherwise.
|
|
258
|
+
*/
|
|
259
|
+
static canHandle(content: MediaContent): boolean;
|
|
172
260
|
/**
|
|
173
261
|
* Determines if the content is a valid YouTube URL or ID.
|
|
174
262
|
* @param {MediaContent} content - The media content to evaluate.
|
|
@@ -178,6 +266,8 @@ declare class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
178
266
|
/**
|
|
179
267
|
* Plays the media content. If the player is already initialized with a different video,
|
|
180
268
|
* it will load the new video automatically.
|
|
269
|
+
*
|
|
270
|
+
* Example: https://www.youtube.com/watch?v=XXXXXXXXXXX
|
|
181
271
|
* @param {MediaContent} content - The media content to play.
|
|
182
272
|
* @returns {Promise<void>}
|
|
183
273
|
* @throws {TypeError} If the content data is invalid.
|
|
@@ -200,6 +290,21 @@ declare class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
200
290
|
* @throws {TypeError} If timeMs is not a number.
|
|
201
291
|
*/
|
|
202
292
|
seek(timeMs: number): Promise<void>;
|
|
293
|
+
/**
|
|
294
|
+
* Checks whether the player is currently muted.
|
|
295
|
+
* @returns {boolean|null} True if muted, false otherwise.
|
|
296
|
+
*/
|
|
297
|
+
isMuted(): boolean | null;
|
|
298
|
+
/**
|
|
299
|
+
* Mutes the current playback.
|
|
300
|
+
* @returns {Promise<void>}
|
|
301
|
+
*/
|
|
302
|
+
mute(): Promise<void>;
|
|
303
|
+
/**
|
|
304
|
+
* Unmutes the current playback.
|
|
305
|
+
* @returns {Promise<void>}
|
|
306
|
+
*/
|
|
307
|
+
unMute(): Promise<void>;
|
|
203
308
|
/**
|
|
204
309
|
* Retrieves the current playback time.
|
|
205
310
|
* @returns {number} The current time in milliseconds.
|
|
@@ -239,6 +344,12 @@ declare class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
239
344
|
* @returns {string|null} The thumbnail URL or null if no video is loaded.
|
|
240
345
|
*/
|
|
241
346
|
getThumbnailUrl(quality?: string): string | null;
|
|
347
|
+
/**
|
|
348
|
+
* Retrieves the metadata of the currently loaded YouTube video, returning a structured
|
|
349
|
+
* object containing details such as the video ID, title, author, and duration.
|
|
350
|
+
* @returns {Promise<ContentData>}
|
|
351
|
+
*/
|
|
352
|
+
getContentData(): Promise<ContentData>;
|
|
242
353
|
/**
|
|
243
354
|
* Cleans up the instance and disconnects from the shared player.
|
|
244
355
|
*/
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @typedef {import('../../basics/mediaContent.mjs').PictureDataType} PictureDataType
|
|
3
3
|
* @typedef {import('../../basics/mediaContent.mjs').MediaContent<PictureDataType>} MediaContent
|
|
4
4
|
* @typedef {import('./index.mjs').ContentTimeData} ContentTimeData
|
|
5
|
+
* @typedef {import('./index.mjs').ContentData} ContentData
|
|
5
6
|
*
|
|
6
7
|
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlayerState} YTPlayerState
|
|
7
8
|
* @typedef {import('./docs/YouTubePlayer.mjs').YTPlayerErrors} YTPlayerErrors
|
|
@@ -34,6 +35,23 @@
|
|
|
34
35
|
* @property {number|string} [height=360] - The iframe height in pixels or a string (e.g., '100%').
|
|
35
36
|
* @property {boolean} [hidden=true] - If true, the iframe will be invisible (opacity 0) and will not respond to clicks (pointer-events none) and appends it to the document body.
|
|
36
37
|
* @property {boolean} [autoplay=false] - If true, the video will start automatically upon loading.
|
|
38
|
+
* @property {string|null} [list=null] - The ID of a playlist to load.
|
|
39
|
+
* @property {string|null} [listType=null] - The type of list to load (e.g., 'playlist').
|
|
40
|
+
* @property {string|null} [ccLangPref=null] - The preferred language for closed captions.
|
|
41
|
+
* @property {boolean} [ccLoadPolicy=false] - Whether to load closed captions by default (0 for off, 1 for on).
|
|
42
|
+
* @property {string|null} [color=null] - The color of the player controls (e.g., 'red', 'white').
|
|
43
|
+
* @property {boolean} [controls=true] - Whether to display the player controls.
|
|
44
|
+
* @property {boolean} [disableKb=false] - Whether to disable keyboard controls.
|
|
45
|
+
* @property {boolean} [loop=false] - Whether the video should loop.
|
|
46
|
+
* @property {number|string|null} [end=null] - The time in seconds when the video should end.
|
|
47
|
+
* @property {number|string|null} [start=null] - The time in seconds when the video should start.
|
|
48
|
+
* @property {string|string[]|null} [playlist=null] - A comma-separated list of video IDs to be played as a playlist.
|
|
49
|
+
* @property {boolean} [playsinline=false] - Whether to play the video inline on mobile devices.
|
|
50
|
+
* @property {string|null} [widgetReferrer=null] - The URL of the page where the player is embedded.
|
|
51
|
+
* @property {boolean} [rel=true] - Whether to show related videos from the same channel.
|
|
52
|
+
* @property {string|null} [hl=null] - The language of the player interface.
|
|
53
|
+
* @property {boolean} [fs=true] - Whether to allow the player to enter fullscreen mode.
|
|
54
|
+
* @property {boolean} [ivLoadPolicy=true] - The policy for showing video annotations (1 to show, 3 to hide).
|
|
37
55
|
*/
|
|
38
56
|
import { EventEmitter } from 'events';
|
|
39
57
|
import { BaseMediaAdapter } from './index.mjs';
|
|
@@ -181,13 +199,13 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
181
199
|
* @throws {TypeError} If the `videoId` is not a valid string.
|
|
182
200
|
*/
|
|
183
201
|
static createIframeContainer(options) {
|
|
184
|
-
const { videoId, width = 640, height = 360, hidden = true, autoplay = false } = options;
|
|
202
|
+
const { videoId, width = 640, height = 360, hidden = true, autoplay = false, ivLoadPolicy = true, list = null, listType = null, ccLangPref = null, ccLoadPolicy = false, color = null, controls = true, disableKb = false, loop = false, end = null, start = null, playlist = null, playsinline = false, widgetReferrer = null, rel = true, fs = true, hl = null, } = options;
|
|
185
203
|
if (typeof videoId !== 'string' || videoId.trim() === '') {
|
|
186
204
|
throw new TypeError('The "videoId" property is required and must be a valid string.');
|
|
187
205
|
}
|
|
188
206
|
const iframe = document.createElement('iframe');
|
|
189
207
|
// enablejsapi=1 is required to allow control via the YouTube API
|
|
190
|
-
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=${autoplay ? 1 : 0}&enablejsapi=1`;
|
|
208
|
+
iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=${autoplay ? 1 : 0}&rel=${rel ? 1 : 0}&iv_load_policy=${ivLoadPolicy ? 1 : 3}&loop=${loop ? 1 : 0}&playsinline=${playsinline ? 1 : 0}&fs=${fs ? 1 : 0}${typeof playlist === 'string' || Array.isArray(playlist) ? `&playlist=${!Array.isArray(playlist) ? playlist : playlist.join(',')}` : ''}${typeof ccLangPref === 'string' ? `&cc_lang_pref=${ccLangPref}` : ''}${typeof end === 'string' || typeof end === 'number' ? `&end=${end}` : ''}${typeof start === 'string' || typeof start === 'number' ? `&start=${start}` : ''}&cc_load_policy=${ccLoadPolicy ? 1 : 0}${typeof list === 'string' ? `&list=${list}` : ''}${typeof listType === 'string' ? `&listType=${listType}` : ''}${typeof widgetReferrer === 'string' ? `&widget_referrer=${widgetReferrer}` : ''}${typeof color === 'string' ? `&color=${color}` : ''}${typeof hl === 'string' ? `&hl=${hl}` : ''}&controls=${controls ? 1 : 0}&disablekb=${disableKb ? 1 : 0}&enablejsapi=1`;
|
|
191
209
|
iframe.style.width = `${width}px`;
|
|
192
210
|
iframe.style.height = `${height}px`;
|
|
193
211
|
iframe.setAttribute('frameborder', '0');
|
|
@@ -226,6 +244,7 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
226
244
|
#player = null;
|
|
227
245
|
/** @type {HTMLIFrameElement} */
|
|
228
246
|
#container;
|
|
247
|
+
#isReady = false;
|
|
229
248
|
/** @type {Promise<void>} */
|
|
230
249
|
#apiLoadedPromise;
|
|
231
250
|
/** @type {number} */
|
|
@@ -241,8 +260,22 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
241
260
|
get currentContentId() {
|
|
242
261
|
return this.#currentContentId;
|
|
243
262
|
}
|
|
263
|
+
/** @type {number} */
|
|
264
|
+
#lastReportedTime = 0;
|
|
244
265
|
/** @type {ReturnType<typeof setInterval>|null} */
|
|
245
266
|
#timeUpdateInterval = null;
|
|
267
|
+
/** @type {number} */
|
|
268
|
+
#pollingTimeUpdateValue = 250;
|
|
269
|
+
get pollingTimeUpdateValue() {
|
|
270
|
+
checkDestroy(this.#destroyed);
|
|
271
|
+
return this.#pollingTimeUpdateValue;
|
|
272
|
+
}
|
|
273
|
+
set pollingTimeUpdateValue(timeMs) {
|
|
274
|
+
checkDestroy(this.#destroyed);
|
|
275
|
+
if (typeof timeMs !== 'number')
|
|
276
|
+
throw new TypeError('Time must be a number.');
|
|
277
|
+
this.#pollingTimeUpdateValue = timeMs;
|
|
278
|
+
}
|
|
246
279
|
/**
|
|
247
280
|
* Gets whether the adapter has been destroyed.
|
|
248
281
|
* @returns {boolean}
|
|
@@ -300,6 +333,22 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
300
333
|
checkDestroy(this.#destroyed);
|
|
301
334
|
return this.#currentVolume;
|
|
302
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Synchronously checks whether the YouTube media adapter is fully initialized and ready.
|
|
338
|
+
* @returns {boolean} True if the adapter is ready, false otherwise.
|
|
339
|
+
*/
|
|
340
|
+
isReady() {
|
|
341
|
+
checkDestroy(this.#destroyed);
|
|
342
|
+
return this.#isReady;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Asynchronously waits until the YouTube media adapter is fully initialized and ready.
|
|
346
|
+
* @returns {Promise<void>} A promise that resolves once the adapter is ready.
|
|
347
|
+
*/
|
|
348
|
+
async waitIsReady() {
|
|
349
|
+
checkDestroy(this.#destroyed);
|
|
350
|
+
return waitForTrue(() => this.#isReady);
|
|
351
|
+
}
|
|
303
352
|
/**
|
|
304
353
|
* Retrieves a consolidated object containing all time-related metrics for the current media content.
|
|
305
354
|
* @returns {ContentTimeData} An object containing total, current, remaining time, and playback percentage.
|
|
@@ -316,8 +365,7 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
316
365
|
* @param {MediaContent} content - The media content to evaluate.
|
|
317
366
|
* @returns {boolean} True if the content is a YouTube link/ID, false otherwise.
|
|
318
367
|
*/
|
|
319
|
-
canHandle(content) {
|
|
320
|
-
checkDestroy(this.#destroyed);
|
|
368
|
+
static canHandle(content) {
|
|
321
369
|
if (!isValidObj(content)) {
|
|
322
370
|
throw new TypeError('Content must be a valid object.');
|
|
323
371
|
}
|
|
@@ -326,9 +374,20 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
326
374
|
const youtubeRegex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/;
|
|
327
375
|
return youtubeRegex.test(content.url);
|
|
328
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Determines if the content is a valid YouTube URL or ID.
|
|
379
|
+
* @param {MediaContent} content - The media content to evaluate.
|
|
380
|
+
* @returns {boolean} True if the content is a YouTube link/ID, false otherwise.
|
|
381
|
+
*/
|
|
382
|
+
canHandle(content) {
|
|
383
|
+
checkDestroy(this.#destroyed);
|
|
384
|
+
return YoutubeMediaAdapter.canHandle(content);
|
|
385
|
+
}
|
|
329
386
|
/**
|
|
330
387
|
* Plays the media content. If the player is already initialized with a different video,
|
|
331
388
|
* it will load the new video automatically.
|
|
389
|
+
*
|
|
390
|
+
* Example: https://www.youtube.com/watch?v=XXXXXXXXXXX
|
|
332
391
|
* @param {MediaContent} content - The media content to play.
|
|
333
392
|
* @returns {Promise<void>}
|
|
334
393
|
* @throws {TypeError} If the content data is invalid.
|
|
@@ -351,6 +410,9 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
351
410
|
if (this.#currentContentId !== targetVideoId) {
|
|
352
411
|
this.#currentContentId = targetVideoId;
|
|
353
412
|
this.#player?.loadVideoById(targetVideoId);
|
|
413
|
+
// Reset the last reported time to 0 to prevent a false 'seek' event
|
|
414
|
+
// when the new video starts playing.
|
|
415
|
+
this.#lastReportedTime = 0;
|
|
354
416
|
}
|
|
355
417
|
else {
|
|
356
418
|
this.#player?.playVideo();
|
|
@@ -412,7 +474,9 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
412
474
|
this.#masterEmitter = masterEmitter;
|
|
413
475
|
// Resolve the initialization promise when the player is ready
|
|
414
476
|
this.#masterEmitter?.once('onReady', () => {
|
|
477
|
+
this.#lastReportedTime = this.getCurrentTime();
|
|
415
478
|
this.#startPollingTimeUpdate();
|
|
479
|
+
this.#isReady = true;
|
|
416
480
|
resolve();
|
|
417
481
|
});
|
|
418
482
|
this.#attachToMaster();
|
|
@@ -420,14 +484,27 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
420
484
|
}
|
|
421
485
|
/**
|
|
422
486
|
* Starts a polling interval to emit 'timeupdate' events, matching the HTML5 Audio interface.
|
|
487
|
+
* Also detects 'seek' events by monitoring large jumps in the playback time.
|
|
423
488
|
*/
|
|
424
489
|
#startPollingTimeUpdate() {
|
|
425
490
|
this.#timeUpdateInterval = setInterval(() => {
|
|
426
|
-
if (this.#player
|
|
427
|
-
|
|
491
|
+
if (!this.#player)
|
|
492
|
+
return;
|
|
493
|
+
const currentTime = this.getCurrentTime();
|
|
494
|
+
const isPlaying = this.#player.getPlayerState() === YoutubeMediaAdapter.PlayerState.PLAYING;
|
|
495
|
+
if (isPlaying) {
|
|
496
|
+
const diff = Math.abs(currentTime - this.#lastReportedTime);
|
|
497
|
+
// If the time jump is greater than 1 second, we interpret it as a 'seek' event.
|
|
498
|
+
// This captures both programmatic seeks and user interactions with the YouTube UI.
|
|
499
|
+
if (diff > 1000) {
|
|
500
|
+
this.emit('seek', currentTime / 1000);
|
|
501
|
+
}
|
|
428
502
|
this.#masterEmitter?.emit('timeupdate', this.getTimeData());
|
|
429
503
|
}
|
|
430
|
-
|
|
504
|
+
// Always update the last reported time to keep it in sync with the actual player position.
|
|
505
|
+
// This prevents false 'seek' events when resuming playback after a pause or a seek while paused.
|
|
506
|
+
this.#lastReportedTime = currentTime;
|
|
507
|
+
}, this.#pollingTimeUpdateValue);
|
|
431
508
|
}
|
|
432
509
|
/**
|
|
433
510
|
* Connects this instance to the master emitter of the shared player.
|
|
@@ -480,9 +557,32 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
480
557
|
checkDestroy(this.#destroyed);
|
|
481
558
|
if (typeof timeMs !== 'number')
|
|
482
559
|
throw new TypeError('Time must be a number.');
|
|
483
|
-
const
|
|
484
|
-
this.#player?.seekTo(
|
|
485
|
-
|
|
560
|
+
const targetTimeInSeconds = timeMs / 1000;
|
|
561
|
+
this.#player?.seekTo(targetTimeInSeconds, true);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Checks whether the player is currently muted.
|
|
565
|
+
* @returns {boolean|null} True if muted, false otherwise.
|
|
566
|
+
*/
|
|
567
|
+
isMuted() {
|
|
568
|
+
checkDestroy(this.#destroyed);
|
|
569
|
+
return this.#player?.isMuted() ?? null;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Mutes the current playback.
|
|
573
|
+
* @returns {Promise<void>}
|
|
574
|
+
*/
|
|
575
|
+
async mute() {
|
|
576
|
+
checkDestroy(this.#destroyed);
|
|
577
|
+
this.#player?.mute();
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Unmutes the current playback.
|
|
581
|
+
* @returns {Promise<void>}
|
|
582
|
+
*/
|
|
583
|
+
async unMute() {
|
|
584
|
+
checkDestroy(this.#destroyed);
|
|
585
|
+
this.#player?.unMute();
|
|
486
586
|
}
|
|
487
587
|
/**
|
|
488
588
|
* Retrieves the current playback time.
|
|
@@ -548,6 +648,27 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
|
|
|
548
648
|
? YoutubeMediaAdapter.getThumbnailUrl(this.#currentContentId, quality)
|
|
549
649
|
: null;
|
|
550
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* Retrieves the metadata of the currently loaded YouTube video, returning a structured
|
|
653
|
+
* object containing details such as the video ID, title, author, and duration.
|
|
654
|
+
* @returns {Promise<ContentData>}
|
|
655
|
+
*/
|
|
656
|
+
async getContentData() {
|
|
657
|
+
const video = this.#player?.getVideoData();
|
|
658
|
+
if (!video)
|
|
659
|
+
throw new Error('No video data available from the YouTube player.');
|
|
660
|
+
return {
|
|
661
|
+
id: String(video.video_id ?? ''),
|
|
662
|
+
createdAt: '',
|
|
663
|
+
artistId: '',
|
|
664
|
+
artistName: String(video.author ?? ''),
|
|
665
|
+
description: '',
|
|
666
|
+
title: String(video.title ?? ''),
|
|
667
|
+
duration: Number(video.progressBarEndPositionUtcTimeMillis ?? 0),
|
|
668
|
+
avatar: this.getThumbnailUrl() ?? '',
|
|
669
|
+
url: `https://youtu.be/${this.#currentContentId}`,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
551
672
|
/**
|
|
552
673
|
* Cleans up the instance and disconnects from the shared player.
|
|
553
674
|
*/
|