tiny-essentials 1.28.5 → 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.
Files changed (35) hide show
  1. package/README.md +18 -1
  2. package/changelog/1/28/6.md +11 -0
  3. package/dist/v1/TinyAdvancedRaffle.min.js +1 -1
  4. package/dist/v1/TinyEssentials.min.js +1 -1
  5. package/dist/v1/TinyIframeEvents.min.js +1 -1
  6. package/dist/v1/TinyLocalStorage.min.js +1 -1
  7. package/dist/v1/TinyMediaPlayer.min.js +1 -1
  8. package/dist/v1/TinyNewWinEvents.min.js +1 -1
  9. package/dist/v1/TinyRadioFm.min.js +1 -1
  10. package/dist/v1/TinySmartScroller.min.js +1 -1
  11. package/dist/v1/TinyUploadClicker.min.js +1 -1
  12. package/dist/v1/css/TinyLoadingScreen.min.css +1 -1
  13. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.cjs +53 -3
  14. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.d.mts +25 -0
  15. package/dist/v1/libs/TinyMediaPlayer/HtmlAudio.mjs +49 -3
  16. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.cjs +53 -3
  17. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.d.mts +25 -0
  18. package/dist/v1/libs/TinyMediaPlayer/HtmlVideo.mjs +49 -3
  19. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.cjs +792 -0
  20. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.d.mts +300 -0
  21. package/dist/v1/libs/TinyMediaPlayer/SoundCloud.mjs +697 -0
  22. package/dist/v1/libs/TinyMediaPlayer/Youtube.cjs +124 -12
  23. package/dist/v1/libs/TinyMediaPlayer/Youtube.d.mts +96 -0
  24. package/dist/v1/libs/TinyMediaPlayer/Youtube.mjs +107 -10
  25. package/dist/v1/libs/TinyMediaPlayer/docs/SoundCloudWidget.cjs +381 -0
  26. package/dist/v1/libs/TinyMediaPlayer/docs/SoundCloudWidget.d.mts +704 -0
  27. package/dist/v1/libs/TinyMediaPlayer/docs/SoundCloudWidget.mjs +343 -0
  28. package/dist/v1/libs/TinyMediaPlayer/index.cjs +52 -1
  29. package/dist/v1/libs/TinyMediaPlayer/index.d.mts +60 -0
  30. package/dist/v1/libs/TinyMediaPlayer/index.mjs +47 -1
  31. package/dist/v1/libs/TinyMediaPlayer.cjs +38 -0
  32. package/dist/v1/libs/TinyMediaPlayer.d.mts +13 -0
  33. package/dist/v1/libs/TinyMediaPlayer.mjs +38 -0
  34. package/docs/v1/libs/TinyMediaPlayer.md +31 -9
  35. package/package.json +51 -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 { videoId, width = 640, height = 360, hidden = true, autoplay = false } = options;
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
- this.#player &&
478
- this.#player.getPlayerState() === YoutubeMediaAdapter.PlayerState.PLAYING
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
- }, 250);
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,8 @@ 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 currentTime = timeMs / 1000;
541
- this.#player?.seekTo(currentTime, true);
542
- this.emit('seek', currentTime);
632
+ const targetTimeInSeconds = timeMs / 1000;
633
+ this.#player?.seekTo(targetTimeInSeconds, true);
543
634
  }
544
635
 
545
636
  /**
@@ -613,6 +704,27 @@ class YoutubeMediaAdapter extends index.BaseMediaAdapter {
613
704
  : null;
614
705
  }
615
706
 
707
+ /**
708
+ * Retrieves the metadata of the currently loaded YouTube video, returning a structured
709
+ * object containing details such as the video ID, title, author, and duration.
710
+ * @returns {Promise<ContentData>}
711
+ */
712
+ async getContentData() {
713
+ const video = this.#player?.getVideoData();
714
+ if (!video) throw new Error('No video data available from the YouTube player.');
715
+ return {
716
+ id: String(video.video_id ?? ''),
717
+ createdAt: '',
718
+ artistId: '',
719
+ artistName: String(video.author ?? ''),
720
+ description: '',
721
+ title: String(video.title ?? ''),
722
+ duration: Number(video.progressBarEndPositionUtcTimeMillis ?? 0),
723
+ avatar: this.getThumbnailUrl() ?? '',
724
+ url: `https://youtu.be/${this.#currentContentId}`,
725
+ };
726
+ }
727
+
616
728
  /**
617
729
  * Cleans up the instance and disconnects from the shared player.
618
730
  */
@@ -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.
@@ -239,6 +329,12 @@ declare class YoutubeMediaAdapter extends BaseMediaAdapter {
239
329
  * @returns {string|null} The thumbnail URL or null if no video is loaded.
240
330
  */
241
331
  getThumbnailUrl(quality?: string): string | null;
332
+ /**
333
+ * Retrieves the metadata of the currently loaded YouTube video, returning a structured
334
+ * object containing details such as the video ID, title, author, and duration.
335
+ * @returns {Promise<ContentData>}
336
+ */
337
+ getContentData(): Promise<ContentData>;
242
338
  /**
243
339
  * Cleans up the instance and disconnects from the shared player.
244
340
  */
@@ -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
- this.#player.getPlayerState() === YoutubeMediaAdapter.PlayerState.PLAYING) {
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
- }, 250);
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,8 @@ 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 currentTime = timeMs / 1000;
484
- this.#player?.seekTo(currentTime, true);
485
- this.emit('seek', currentTime);
560
+ const targetTimeInSeconds = timeMs / 1000;
561
+ this.#player?.seekTo(targetTimeInSeconds, true);
486
562
  }
487
563
  /**
488
564
  * Retrieves the current playback time.
@@ -548,6 +624,27 @@ class YoutubeMediaAdapter extends BaseMediaAdapter {
548
624
  ? YoutubeMediaAdapter.getThumbnailUrl(this.#currentContentId, quality)
549
625
  : null;
550
626
  }
627
+ /**
628
+ * Retrieves the metadata of the currently loaded YouTube video, returning a structured
629
+ * object containing details such as the video ID, title, author, and duration.
630
+ * @returns {Promise<ContentData>}
631
+ */
632
+ async getContentData() {
633
+ const video = this.#player?.getVideoData();
634
+ if (!video)
635
+ throw new Error('No video data available from the YouTube player.');
636
+ return {
637
+ id: String(video.video_id ?? ''),
638
+ createdAt: '',
639
+ artistId: '',
640
+ artistName: String(video.author ?? ''),
641
+ description: '',
642
+ title: String(video.title ?? ''),
643
+ duration: Number(video.progressBarEndPositionUtcTimeMillis ?? 0),
644
+ avatar: this.getThumbnailUrl() ?? '',
645
+ url: `https://youtu.be/${this.#currentContentId}`,
646
+ };
647
+ }
551
648
  /**
552
649
  * Cleans up the instance and disconnects from the shared player.
553
650
  */